Slefhostet und postgres
This commit is contained in:
@@ -1,286 +1,286 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useCoachMarks } from '../context/CoachMarksContext';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { useColors } from '../constants/Colors';
|
||||
|
||||
const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window');
|
||||
const HIGHLIGHT_PADDING = 10;
|
||||
const TOOLTIP_VERTICAL_OFFSET = 32;
|
||||
|
||||
export const CoachMarksOverlay: React.FC = () => {
|
||||
const { isActive, currentStep, steps, layouts, next, skip } = useCoachMarks();
|
||||
const { isDarkMode, colorPalette, t } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const scaleAnim = useRef(new Animated.Value(0.88)).current;
|
||||
const pulseAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
fadeAnim.setValue(0);
|
||||
scaleAnim.setValue(0.88);
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, { toValue: 1, duration: 320, useNativeDriver: true }),
|
||||
Animated.spring(scaleAnim, { toValue: 1, tension: 80, friction: 9, useNativeDriver: true }),
|
||||
]).start();
|
||||
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, { toValue: 1.06, duration: 900, useNativeDriver: true }),
|
||||
Animated.timing(pulseAnim, { toValue: 1, duration: 900, useNativeDriver: true }),
|
||||
]),
|
||||
).start();
|
||||
} else {
|
||||
pulseAnim.stopAnimation();
|
||||
}
|
||||
}, [currentStep, fadeAnim, isActive, pulseAnim, scaleAnim]);
|
||||
|
||||
if (!isActive || steps.length === 0) return null;
|
||||
|
||||
const step = steps[currentStep];
|
||||
const layout = layouts[step.elementKey];
|
||||
|
||||
const highlight = layout
|
||||
? {
|
||||
x: layout.x - HIGHLIGHT_PADDING,
|
||||
y: layout.y - HIGHLIGHT_PADDING,
|
||||
w: layout.width + HIGHLIGHT_PADDING * 2,
|
||||
h: layout.height + HIGHLIGHT_PADDING * 2,
|
||||
r: Math.min(layout.width, layout.height) / 2 + HIGHLIGHT_PADDING,
|
||||
}
|
||||
: { x: SCREEN_W / 2 - 40, y: SCREEN_H / 2 - 40, w: 80, h: 80, r: 40 };
|
||||
|
||||
const tooltipW = 260;
|
||||
const tooltipMaxH = 140;
|
||||
const tooltipX = Math.max(
|
||||
12,
|
||||
Math.min(SCREEN_W - tooltipW - 12, highlight.x + highlight.w / 2 - tooltipW / 2),
|
||||
);
|
||||
|
||||
let tooltipY: number;
|
||||
const spaceBelow = SCREEN_H - (highlight.y + highlight.h);
|
||||
const spaceAbove = highlight.y;
|
||||
|
||||
if (step.tooltipSide === 'above' || (step.tooltipSide !== 'below' && spaceAbove > spaceBelow)) {
|
||||
tooltipY = highlight.y - tooltipMaxH - 24;
|
||||
if (tooltipY < 60) tooltipY = highlight.y + highlight.h + 24;
|
||||
} else {
|
||||
tooltipY = highlight.y + highlight.h + 24;
|
||||
if (tooltipY + tooltipMaxH > SCREEN_H - 60) tooltipY = highlight.y - tooltipMaxH - 24;
|
||||
}
|
||||
|
||||
tooltipY -= TOOLTIP_VERTICAL_OFFSET;
|
||||
tooltipY = Math.max(24, Math.min(SCREEN_H - tooltipMaxH - 24, tooltipY));
|
||||
|
||||
const arrowPointsUp = tooltipY > highlight.y;
|
||||
|
||||
return (
|
||||
<Animated.View style={[StyleSheet.absoluteFill, styles.root, { opacity: fadeAnim }]} pointerEvents="box-none">
|
||||
<View style={[styles.overlay, { top: 0, left: 0, right: 0, height: Math.max(0, highlight.y) }]} />
|
||||
<View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{ top: highlight.y, left: 0, width: Math.max(0, highlight.x), height: highlight.h },
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{
|
||||
top: highlight.y,
|
||||
left: highlight.x + highlight.w,
|
||||
right: 0,
|
||||
height: highlight.h,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{
|
||||
top: highlight.y + highlight.h,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.highlightRing,
|
||||
{
|
||||
left: highlight.x - 4,
|
||||
top: highlight.y - 4,
|
||||
width: highlight.w + 8,
|
||||
height: highlight.h + 8,
|
||||
borderRadius: highlight.r + 4,
|
||||
borderColor: colors.primary,
|
||||
transform: [{ scale: pulseAnim }],
|
||||
},
|
||||
]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.tooltip,
|
||||
{
|
||||
left: tooltipX,
|
||||
top: tooltipY,
|
||||
width: tooltipW,
|
||||
backgroundColor: isDarkMode ? colors.surface : '#ffffff',
|
||||
borderColor: colors.border,
|
||||
shadowColor: '#000',
|
||||
transform: [{ scale: scaleAnim }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.arrow,
|
||||
arrowPointsUp ? styles.arrowUp : styles.arrowDown,
|
||||
{
|
||||
left: Math.max(12, Math.min(tooltipW - 28, highlight.x + highlight.w / 2 - tooltipX - 8)),
|
||||
borderBottomColor: arrowPointsUp ? (isDarkMode ? colors.surface : '#ffffff') : undefined,
|
||||
borderTopColor: !arrowPointsUp ? (isDarkMode ? colors.surface : '#ffffff') : undefined,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={styles.stepRow}>
|
||||
{steps.map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
styles.stepDot,
|
||||
{ backgroundColor: i === currentStep ? colors.primary : colors.border },
|
||||
i === currentStep && { width: 16 },
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={[styles.tooltipTitle, { color: colors.text }]}>{step.title}</Text>
|
||||
<Text style={[styles.tooltipDesc, { color: colors.textSecondary }]}>{step.description}</Text>
|
||||
|
||||
<View style={styles.tooltipFooter}>
|
||||
<TouchableOpacity onPress={skip} style={styles.skipBtn}>
|
||||
<Text style={[styles.skipText, { color: colors.textMuted }]}>{t.coachSkip}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={next}
|
||||
style={[styles.nextBtn, { backgroundColor: colors.primary }]}
|
||||
activeOpacity={0.82}
|
||||
>
|
||||
<Text style={[styles.nextText, { color: colors.onPrimary }]}>
|
||||
{currentStep === steps.length - 1 ? t.coachDone : t.coachNext}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={currentStep === steps.length - 1 ? 'checkmark' : 'arrow-forward'}
|
||||
size={14}
|
||||
color={colors.onPrimary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
zIndex: 9999,
|
||||
elevation: 9999,
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0,0,0,0.72)',
|
||||
},
|
||||
highlightRing: {
|
||||
position: 'absolute',
|
||||
borderWidth: 2.5,
|
||||
},
|
||||
tooltip: {
|
||||
position: 'absolute',
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
padding: 16,
|
||||
gap: 8,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 20,
|
||||
elevation: 12,
|
||||
},
|
||||
stepRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
marginBottom: 2,
|
||||
},
|
||||
stepDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
tooltipTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
lineHeight: 20,
|
||||
},
|
||||
tooltipDesc: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
tooltipFooter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 4,
|
||||
},
|
||||
skipBtn: {
|
||||
padding: 4,
|
||||
},
|
||||
skipText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
nextBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 9,
|
||||
borderRadius: 20,
|
||||
},
|
||||
nextText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
arrow: {
|
||||
position: 'absolute',
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 8,
|
||||
borderRightWidth: 8,
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
},
|
||||
arrowUp: {
|
||||
top: -8,
|
||||
borderBottomWidth: 8,
|
||||
},
|
||||
arrowDown: {
|
||||
bottom: -8,
|
||||
borderTopWidth: 8,
|
||||
},
|
||||
});
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useCoachMarks } from '../context/CoachMarksContext';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { useColors } from '../constants/Colors';
|
||||
|
||||
const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window');
|
||||
const HIGHLIGHT_PADDING = 10;
|
||||
const TOOLTIP_VERTICAL_OFFSET = 32;
|
||||
|
||||
export const CoachMarksOverlay: React.FC = () => {
|
||||
const { isActive, currentStep, steps, layouts, next, skip } = useCoachMarks();
|
||||
const { isDarkMode, colorPalette, t } = useApp();
|
||||
const colors = useColors(isDarkMode, colorPalette);
|
||||
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const scaleAnim = useRef(new Animated.Value(0.88)).current;
|
||||
const pulseAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
fadeAnim.setValue(0);
|
||||
scaleAnim.setValue(0.88);
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, { toValue: 1, duration: 320, useNativeDriver: true }),
|
||||
Animated.spring(scaleAnim, { toValue: 1, tension: 80, friction: 9, useNativeDriver: true }),
|
||||
]).start();
|
||||
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, { toValue: 1.06, duration: 900, useNativeDriver: true }),
|
||||
Animated.timing(pulseAnim, { toValue: 1, duration: 900, useNativeDriver: true }),
|
||||
]),
|
||||
).start();
|
||||
} else {
|
||||
pulseAnim.stopAnimation();
|
||||
}
|
||||
}, [currentStep, fadeAnim, isActive, pulseAnim, scaleAnim]);
|
||||
|
||||
if (!isActive || steps.length === 0) return null;
|
||||
|
||||
const step = steps[currentStep];
|
||||
const layout = layouts[step.elementKey];
|
||||
|
||||
const highlight = layout
|
||||
? {
|
||||
x: layout.x - HIGHLIGHT_PADDING,
|
||||
y: layout.y - HIGHLIGHT_PADDING,
|
||||
w: layout.width + HIGHLIGHT_PADDING * 2,
|
||||
h: layout.height + HIGHLIGHT_PADDING * 2,
|
||||
r: Math.min(layout.width, layout.height) / 2 + HIGHLIGHT_PADDING,
|
||||
}
|
||||
: { x: SCREEN_W / 2 - 40, y: SCREEN_H / 2 - 40, w: 80, h: 80, r: 40 };
|
||||
|
||||
const tooltipW = 260;
|
||||
const tooltipMaxH = 140;
|
||||
const tooltipX = Math.max(
|
||||
12,
|
||||
Math.min(SCREEN_W - tooltipW - 12, highlight.x + highlight.w / 2 - tooltipW / 2),
|
||||
);
|
||||
|
||||
let tooltipY: number;
|
||||
const spaceBelow = SCREEN_H - (highlight.y + highlight.h);
|
||||
const spaceAbove = highlight.y;
|
||||
|
||||
if (step.tooltipSide === 'above' || (step.tooltipSide !== 'below' && spaceAbove > spaceBelow)) {
|
||||
tooltipY = highlight.y - tooltipMaxH - 24;
|
||||
if (tooltipY < 60) tooltipY = highlight.y + highlight.h + 24;
|
||||
} else {
|
||||
tooltipY = highlight.y + highlight.h + 24;
|
||||
if (tooltipY + tooltipMaxH > SCREEN_H - 60) tooltipY = highlight.y - tooltipMaxH - 24;
|
||||
}
|
||||
|
||||
tooltipY -= TOOLTIP_VERTICAL_OFFSET;
|
||||
tooltipY = Math.max(24, Math.min(SCREEN_H - tooltipMaxH - 24, tooltipY));
|
||||
|
||||
const arrowPointsUp = tooltipY > highlight.y;
|
||||
|
||||
return (
|
||||
<Animated.View style={[StyleSheet.absoluteFill, styles.root, { opacity: fadeAnim }]} pointerEvents="box-none">
|
||||
<View style={[styles.overlay, { top: 0, left: 0, right: 0, height: Math.max(0, highlight.y) }]} />
|
||||
<View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{ top: highlight.y, left: 0, width: Math.max(0, highlight.x), height: highlight.h },
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{
|
||||
top: highlight.y,
|
||||
left: highlight.x + highlight.w,
|
||||
right: 0,
|
||||
height: highlight.h,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.overlay,
|
||||
{
|
||||
top: highlight.y + highlight.h,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.highlightRing,
|
||||
{
|
||||
left: highlight.x - 4,
|
||||
top: highlight.y - 4,
|
||||
width: highlight.w + 8,
|
||||
height: highlight.h + 8,
|
||||
borderRadius: highlight.r + 4,
|
||||
borderColor: colors.primary,
|
||||
transform: [{ scale: pulseAnim }],
|
||||
},
|
||||
]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.tooltip,
|
||||
{
|
||||
left: tooltipX,
|
||||
top: tooltipY,
|
||||
width: tooltipW,
|
||||
backgroundColor: isDarkMode ? colors.surface : '#ffffff',
|
||||
borderColor: colors.border,
|
||||
shadowColor: '#000',
|
||||
transform: [{ scale: scaleAnim }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.arrow,
|
||||
arrowPointsUp ? styles.arrowUp : styles.arrowDown,
|
||||
{
|
||||
left: Math.max(12, Math.min(tooltipW - 28, highlight.x + highlight.w / 2 - tooltipX - 8)),
|
||||
borderBottomColor: arrowPointsUp ? (isDarkMode ? colors.surface : '#ffffff') : undefined,
|
||||
borderTopColor: !arrowPointsUp ? (isDarkMode ? colors.surface : '#ffffff') : undefined,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={styles.stepRow}>
|
||||
{steps.map((_, i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
styles.stepDot,
|
||||
{ backgroundColor: i === currentStep ? colors.primary : colors.border },
|
||||
i === currentStep && { width: 16 },
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={[styles.tooltipTitle, { color: colors.text }]}>{step.title}</Text>
|
||||
<Text style={[styles.tooltipDesc, { color: colors.textSecondary }]}>{step.description}</Text>
|
||||
|
||||
<View style={styles.tooltipFooter}>
|
||||
<TouchableOpacity onPress={skip} style={styles.skipBtn}>
|
||||
<Text style={[styles.skipText, { color: colors.textMuted }]}>{t.coachSkip}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={next}
|
||||
style={[styles.nextBtn, { backgroundColor: colors.primary }]}
|
||||
activeOpacity={0.82}
|
||||
>
|
||||
<Text style={[styles.nextText, { color: colors.onPrimary }]}>
|
||||
{currentStep === steps.length - 1 ? t.coachDone : t.coachNext}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={currentStep === steps.length - 1 ? 'checkmark' : 'arrow-forward'}
|
||||
size={14}
|
||||
color={colors.onPrimary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
zIndex: 9999,
|
||||
elevation: 9999,
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0,0,0,0.72)',
|
||||
},
|
||||
highlightRing: {
|
||||
position: 'absolute',
|
||||
borderWidth: 2.5,
|
||||
},
|
||||
tooltip: {
|
||||
position: 'absolute',
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
padding: 16,
|
||||
gap: 8,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 20,
|
||||
elevation: 12,
|
||||
},
|
||||
stepRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
marginBottom: 2,
|
||||
},
|
||||
stepDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
tooltipTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
lineHeight: 20,
|
||||
},
|
||||
tooltipDesc: {
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
},
|
||||
tooltipFooter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 4,
|
||||
},
|
||||
skipBtn: {
|
||||
padding: 4,
|
||||
},
|
||||
skipText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
nextBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 9,
|
||||
borderRadius: 20,
|
||||
},
|
||||
nextText: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
arrow: {
|
||||
position: 'absolute',
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 8,
|
||||
borderRightWidth: 8,
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
},
|
||||
arrowUp: {
|
||||
top: -8,
|
||||
borderBottomWidth: 8,
|
||||
},
|
||||
arrowDown: {
|
||||
bottom: -8,
|
||||
borderTopWidth: 8,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user