208 lines
6.0 KiB
TypeScript
208 lines
6.0 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Animated, Easing, Text, useWindowDimensions, View } from 'react-native';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import * as Notifications from 'expo-notifications';
|
|
import { useColors } from '../../../lib/theme';
|
|
import { OnboardingShell } from '../OnboardingShell';
|
|
import { LyraBubble } from '../LyraBubble';
|
|
import { CTABar } from '../CTABar';
|
|
import { FaqAccordion, type FaqItem } from '../../FaqAccordion';
|
|
import { useNotificationPrefsStore } from '../../../stores/notificationPrefs';
|
|
|
|
// Top-5 (kuratiert für Onboarding-Ende) — alle 8 sind unter app/help/faq.tsx.
|
|
const ONBOARDING_FAQ_IDS = [1, 2, 4, 5, 8] as const;
|
|
|
|
export function DoneSlide({
|
|
onEnter,
|
|
current,
|
|
total,
|
|
}: {
|
|
onEnter: () => void;
|
|
current: number;
|
|
total: number;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const colors = useColors();
|
|
const scale = useRef(new Animated.Value(0.6)).current;
|
|
const opacity = useRef(new Animated.Value(0)).current;
|
|
const setPushEnabled = useNotificationPrefsStore((s) => s.setPushEnabled);
|
|
|
|
const faqItems: FaqItem[] = ONBOARDING_FAQ_IDS.map((id) => ({
|
|
q: t(`help.faq_q${id}`),
|
|
a: t(`help.faq_a${id}`),
|
|
}));
|
|
|
|
useEffect(() => {
|
|
Animated.parallel([
|
|
Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 5, tension: 90 }),
|
|
Animated.timing(opacity, {
|
|
toValue: 1,
|
|
duration: 500,
|
|
useNativeDriver: true,
|
|
easing: Easing.out(Easing.cubic),
|
|
}),
|
|
]).start();
|
|
|
|
(async () => {
|
|
try {
|
|
const { status } = await Notifications.requestPermissionsAsync();
|
|
if (status !== 'granted') {
|
|
await setPushEnabled(false);
|
|
}
|
|
} catch {}
|
|
})();
|
|
}, [setPushEnabled]);
|
|
|
|
return (
|
|
<OnboardingShell
|
|
current={current}
|
|
total={total}
|
|
cta={<CTABar primaryLabel={t('onboarding.done.cta_primary')} onPrimary={onEnter} />}
|
|
>
|
|
<ConfettiOverlay />
|
|
|
|
<LyraBubble text={t('onboarding.lyra.done.body')} emotion="happy" />
|
|
|
|
<Animated.View
|
|
style={{
|
|
marginTop: 24,
|
|
alignItems: 'center',
|
|
opacity,
|
|
transform: [{ scale }],
|
|
}}
|
|
>
|
|
<View
|
|
style={{
|
|
width: 96,
|
|
height: 96,
|
|
borderRadius: 48,
|
|
backgroundColor: 'rgba(34,197,94,0.14)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderWidth: 2,
|
|
borderColor: 'rgba(34,197,94,0.40)',
|
|
}}
|
|
>
|
|
<Ionicons name="checkmark" size={52} color={colors.success} />
|
|
</View>
|
|
|
|
<Text
|
|
style={{
|
|
marginTop: 18,
|
|
fontFamily: 'Nunito_800ExtraBold',
|
|
fontSize: 24,
|
|
color: colors.text,
|
|
textAlign: 'center',
|
|
letterSpacing: -0.3,
|
|
}}
|
|
>
|
|
{t('onboarding.done.headline')}
|
|
</Text>
|
|
<Text
|
|
style={{
|
|
marginTop: 6,
|
|
fontFamily: 'Nunito_400Regular',
|
|
fontSize: 14,
|
|
lineHeight: 20,
|
|
color: colors.textMuted,
|
|
textAlign: 'center',
|
|
paddingHorizontal: 16,
|
|
}}
|
|
>
|
|
{t('onboarding.done.subhead')}
|
|
</Text>
|
|
</Animated.View>
|
|
|
|
{/* Inline Top-5-FAQ Accordion (pills-Variante) */}
|
|
<View style={{ marginTop: 28 }}>
|
|
<Text
|
|
style={{
|
|
fontFamily: 'Nunito_700Bold',
|
|
fontSize: 12,
|
|
letterSpacing: 0.8,
|
|
color: colors.textMuted,
|
|
textTransform: 'uppercase',
|
|
marginBottom: 10,
|
|
}}
|
|
>
|
|
{t('onboarding.done.faq_section_title')}
|
|
</Text>
|
|
<FaqAccordion items={faqItems} variant="pills" />
|
|
</View>
|
|
</OnboardingShell>
|
|
);
|
|
}
|
|
|
|
// ─── Confetti-Overlay (Reanimated Particles) ─────────────────────────────────
|
|
|
|
const CONFETTI_COUNT = 22;
|
|
const CONFETTI_COLORS = ['#fbbf24', '#34d399', '#60a5fa', '#a78bfa', '#f472b6'];
|
|
|
|
function ConfettiOverlay() {
|
|
const { width: screenW } = useWindowDimensions();
|
|
// Stabile Particle-Definitionen — Math.random nur einmal beim Mount
|
|
const particles = useRef(
|
|
Array.from({ length: CONFETTI_COUNT }).map((_, i) => ({
|
|
key: i,
|
|
anim: new Animated.Value(0),
|
|
startX: Math.random() * screenW,
|
|
drift: (Math.random() - 0.5) * 80,
|
|
color: CONFETTI_COLORS[i % CONFETTI_COLORS.length],
|
|
rotateStart: Math.random() * 360,
|
|
size: 6 + Math.random() * 6,
|
|
delay: Math.random() * 600,
|
|
duration: 2200 + Math.random() * 1300,
|
|
})),
|
|
).current;
|
|
|
|
useEffect(() => {
|
|
Animated.stagger(
|
|
40,
|
|
particles.map((p) =>
|
|
Animated.timing(p.anim, {
|
|
toValue: 1,
|
|
duration: p.duration,
|
|
delay: p.delay,
|
|
useNativeDriver: true,
|
|
easing: Easing.out(Easing.quad),
|
|
}),
|
|
),
|
|
).start();
|
|
}, []);
|
|
|
|
return (
|
|
<View
|
|
pointerEvents="none"
|
|
style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 600, overflow: 'hidden' }}
|
|
>
|
|
{particles.map((p) => {
|
|
const translateY = p.anim.interpolate({ inputRange: [0, 1], outputRange: [-30, 580] });
|
|
const translateX = p.anim.interpolate({ inputRange: [0, 1], outputRange: [0, p.drift] });
|
|
const rotate = p.anim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [`${p.rotateStart}deg`, `${p.rotateStart + 540}deg`],
|
|
});
|
|
const opacity = p.anim.interpolate({ inputRange: [0, 0.85, 1], outputRange: [1, 1, 0] });
|
|
return (
|
|
<Animated.View
|
|
key={p.key}
|
|
style={{
|
|
position: 'absolute',
|
|
left: p.startX - p.size / 2,
|
|
top: 0,
|
|
width: p.size,
|
|
height: p.size * 1.6,
|
|
backgroundColor: p.color,
|
|
borderRadius: 2,
|
|
opacity,
|
|
transform: [{ translateY }, { translateX }, { rotate }],
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
}
|
|
|