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>
);
}