Files
Greenlens/app/onboarding/slides.tsx
2026-07-06 22:25:52 +02:00

498 lines
14 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { Image, StyleSheet, Text, TouchableOpacity, View, useWindowDimensions } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAnalytics } from '../../services/analytics';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
import { Language } from '../../types';
type ColorsType = ReturnType<typeof useColors>;
const getSlidesCopy = (language: Language) => {
if (language === 'de') {
return {
slides: [
{
title: 'Scanne jede Pflanze',
body: 'Richte die Kamera auf eine Pflanze und GreenLens erkennt sie in Sekunden.',
},
{
title: 'Health Check & Pflegeplan',
body: 'GreenLens erkennt Probleme früh und erstellt deinen Rettungsplan.',
},
{
title: 'Nie mehr Gießen vergessen',
body: 'Smarte Erinnerungen und deine Pflanzen-Bibliothek halten alles im Blick.',
},
],
resultChip: 'Monstera · 98%',
healthCheckLabel: 'Health Check',
overwateringDetected: 'Überwässerung erkannt',
rescuePlanReady: '7-Tage-Rettungsplan bereit',
waterReminder: 'Monstera gießen — heute',
fertilizeReminder: 'Basilikum düngen — in 3 Tagen',
continueLabel: 'Weiter',
};
}
if (language === 'es') {
return {
slides: [
{
title: 'Escanea cualquier planta',
body: 'Apunta la cámara a una planta y GreenLens la identifica en segundos.',
},
{
title: 'Chequeo de salud y plan de cuidados',
body: 'GreenLens detecta problemas a tiempo y crea tu plan de rescate.',
},
{
title: 'No olvides regar nunca más',
body: 'Recordatorios inteligentes y tu biblioteca de plantas lo mantienen todo al día.',
},
],
resultChip: 'Monstera · 98%',
healthCheckLabel: 'Chequeo de salud',
overwateringDetected: 'Exceso de riego detectado',
rescuePlanReady: 'Plan de rescate de 7 días listo',
waterReminder: 'Regar Monstera — hoy',
fertilizeReminder: 'Abonar albahaca — en 3 días',
continueLabel: 'Continuar',
};
}
return {
slides: [
{
title: 'Scan Any Plant',
body: 'Point your camera at a plant and GreenLens identifies it in seconds.',
},
{
title: 'Health Check & Care Plan',
body: 'GreenLens spots problems early and builds a rescue plan for you.',
},
{
title: 'Never Forget Watering',
body: 'Smart reminders and your personal plant library keep everything on track.',
},
],
resultChip: 'Monstera · 98%',
healthCheckLabel: 'Health Check',
overwateringDetected: 'Overwatering detected',
rescuePlanReady: '7-day rescue plan ready',
waterReminder: 'Water Monstera — today',
fertilizeReminder: 'Fertilize Basil — in 3 days',
continueLabel: 'Continue',
};
};
function ScanFrameOverlay({ resultChip, colors }: { resultChip: string; colors: ColorsType }) {
return (
<>
<View style={styles.scanFrameWrap} pointerEvents="none">
<View style={[styles.cornerTL, { borderColor: colors.primary }]} />
<View style={[styles.cornerTR, { borderColor: colors.primary }]} />
<View style={[styles.cornerBL, { borderColor: colors.primary }]} />
<View style={[styles.cornerBR, { borderColor: colors.primary }]} />
</View>
<View style={styles.resultChip}>
<Ionicons name="leaf" size={15} color={colors.primary} />
<Text style={styles.resultChipText}>{resultChip}</Text>
</View>
</>
);
}
function HealthCardOverlay({
label,
overwateringDetected,
rescuePlanReady,
}: {
label: string;
overwateringDetected: string;
rescuePlanReady: string;
}) {
return (
<View style={styles.healthCard}>
<View style={styles.healthCardHeader}>
<View style={styles.healthCardIcon}>
<Ionicons name="medkit" size={16} color="#C62828" />
</View>
<Text style={styles.healthCardTitle}>{label}</Text>
</View>
<View style={[styles.healthRow, styles.healthRowWarning]}>
<Ionicons name="warning" size={15} color="#C62828" />
<Text style={styles.healthRowWarningText}>{overwateringDetected}</Text>
</View>
<View style={[styles.healthRow, styles.healthRowSuccess]}>
<Ionicons name="checkmark-circle" size={15} color="#2e7d32" />
<Text style={styles.healthRowSuccessText}>{rescuePlanReady}</Text>
</View>
</View>
);
}
function ReminderChipsOverlay({ waterReminder, fertilizeReminder }: { waterReminder: string; fertilizeReminder: string }) {
const [waterLabel, waterMeta] = splitReminder(waterReminder);
const [fertilizeLabel, fertilizeMeta] = splitReminder(fertilizeReminder);
return (
<>
<View style={[styles.reminderChip, styles.reminderChipTop]}>
<View style={[styles.reminderIcon, { backgroundColor: '#dff2e6' }]}>
<Ionicons name="water" size={16} color="#2e7d32" />
</View>
<View>
<Text style={styles.reminderLabel}>{waterLabel}</Text>
<Text style={styles.reminderMeta}>{waterMeta}</Text>
</View>
</View>
<View style={[styles.reminderChip, styles.reminderChipBottom]}>
<View style={[styles.reminderIcon, { backgroundColor: '#e3f3c8' }]}>
<Ionicons name="leaf" size={16} color="#558b2f" />
</View>
<View>
<Text style={styles.reminderLabel}>{fertilizeLabel}</Text>
<Text style={styles.reminderMeta}>{fertilizeMeta}</Text>
</View>
</View>
</>
);
}
// Splits "Water Monstera — today" into ["Water Monstera", "today"] for a two-line chip.
function splitReminder(text: string): [string, string] {
const parts = text.split('—').map((part) => part.trim());
if (parts.length === 2) return [parts[0], parts[1]];
return [text, ''];
}
export default function OnboardingSlidesScreen() {
const router = useRouter();
const { height } = useWindowDimensions();
const compact = height < 700;
const { language, isDarkMode, colorPalette } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const posthog = useSafeAnalytics();
const [page, setPage] = useState(0);
const copy = getSlidesCopy(language);
const slide = copy.slides[page];
useEffect(() => {
posthog.capture('onboarding_slide_viewed', { index: page });
}, [page, posthog]);
const next = () => {
if (page < copy.slides.length - 1) {
setPage(page + 1);
} else {
router.replace('/onboarding/source');
}
};
const back = () => {
if (page > 0) {
setPage(page - 1);
} else {
router.back();
}
};
return (
<View style={[styles.container, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]}>
<View style={[styles.imageArea, { height: compact ? '58%' : '65%' }]}>
<Image
source={
page === 0
? require('../../assets/paywall_scan_background.png')
: page === 1
? require('../../assets/onboarding_health_scan_mockup_vertical.png')
: require('../../assets/welcome_botanical_header.png')
}
style={styles.image}
resizeMode="cover"
/>
<SafeAreaView style={styles.imageSafeArea} edges={['top']}>
<TouchableOpacity onPress={back} style={styles.backBtn} activeOpacity={0.85}>
<Ionicons name="arrow-back" size={20} color="#1f2520" />
</TouchableOpacity>
</SafeAreaView>
{page === 0 && <ScanFrameOverlay resultChip={copy.resultChip} colors={colors} />}
{page === 1 && (
<HealthCardOverlay
label={copy.healthCheckLabel}
overwateringDetected={copy.overwateringDetected}
rescuePlanReady={copy.rescuePlanReady}
/>
)}
{page === 2 && (
<ReminderChipsOverlay waterReminder={copy.waterReminder} fertilizeReminder={copy.fertilizeReminder} />
)}
</View>
<View style={[styles.sheet, { backgroundColor: colors.surface }]}>
<Text style={[styles.title, { color: colors.text }]}>{slide.title}</Text>
<Text style={[styles.body, { color: colors.textSecondary }]}>{slide.body}</Text>
<View style={styles.dots}>
{copy.slides.map((_, index) => (
<View
key={index}
style={[
styles.dot,
index === page
? [styles.dotActive, { backgroundColor: colors.primary }]
: { backgroundColor: colors.border },
]}
/>
))}
</View>
<TouchableOpacity
style={[styles.cta, { backgroundColor: colors.primary }]}
onPress={next}
activeOpacity={0.86}
>
<Text style={[styles.ctaText, { color: colors.onPrimary }]}>{copy.continueLabel}</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
imageArea: {
height: '58%',
position: 'relative',
overflow: 'hidden',
},
image: {
width: '100%',
height: '100%',
position: 'absolute',
},
imageSafeArea: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
},
backBtn: {
marginLeft: 16,
marginTop: 8,
width: 38,
height: 38,
borderRadius: 19,
backgroundColor: 'rgba(255,255,255,0.85)',
alignItems: 'center',
justifyContent: 'center',
},
// Scan frame overlay (slide 1)
scanFrameWrap: {
position: 'absolute',
top: '22%',
left: '20%',
right: '20%',
bottom: '26%',
},
cornerTL: {
position: 'absolute',
top: 0,
left: 0,
width: 30,
height: 30,
borderTopWidth: 4,
borderLeftWidth: 4,
borderTopLeftRadius: 8,
},
cornerTR: {
position: 'absolute',
top: 0,
right: 0,
width: 30,
height: 30,
borderTopWidth: 4,
borderRightWidth: 4,
borderTopRightRadius: 8,
},
cornerBL: {
position: 'absolute',
bottom: 0,
left: 0,
width: 30,
height: 30,
borderBottomWidth: 4,
borderLeftWidth: 4,
borderBottomLeftRadius: 8,
},
cornerBR: {
position: 'absolute',
bottom: 0,
right: 0,
width: 30,
height: 30,
borderBottomWidth: 4,
borderRightWidth: 4,
borderBottomRightRadius: 8,
},
resultChip: {
position: 'absolute',
bottom: '10%',
left: 20,
right: 20,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
backgroundColor: 'rgba(255,255,255,0.94)',
borderRadius: 999,
paddingHorizontal: 16,
paddingVertical: 12,
},
resultChipText: {
fontSize: 15,
fontWeight: '700',
color: '#1f2520',
},
// Health card overlay (slide 2)
healthCard: {
position: 'absolute',
bottom: 34,
left: 16,
right: 16,
backgroundColor: 'rgba(255,255,255,0.96)',
borderRadius: 18,
padding: 14,
gap: 8,
},
healthCardHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 2,
},
healthCardIcon: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#fdeaea',
alignItems: 'center',
justifyContent: 'center',
},
healthCardTitle: {
fontSize: 16,
fontWeight: '800',
color: '#1f2520',
},
healthRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
borderRadius: 10,
paddingHorizontal: 10,
paddingVertical: 8,
},
healthRowWarning: {
backgroundColor: '#fdeaea',
},
healthRowWarningText: {
fontSize: 13.5,
fontWeight: '700',
color: '#C62828',
},
healthRowSuccess: {
backgroundColor: '#e8f3e3',
},
healthRowSuccessText: {
fontSize: 13.5,
fontWeight: '700',
color: '#2e7d32',
},
// Reminder chips overlay (slide 3)
reminderChip: {
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
gap: 10,
backgroundColor: 'rgba(255,255,255,0.96)',
borderRadius: 999,
paddingVertical: 8,
paddingRight: 18,
paddingLeft: 8,
},
reminderChipTop: {
top: '24%',
right: 20,
},
reminderChipBottom: {
top: '42%',
left: 20,
},
reminderIcon: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
reminderLabel: {
fontSize: 14,
fontWeight: '800',
color: '#1f2520',
},
reminderMeta: {
fontSize: 12,
fontWeight: '600',
color: '#5a8a3d',
},
// Bottom sheet
sheet: {
flex: 1,
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
marginTop: -24,
paddingHorizontal: 24,
paddingTop: 32,
alignItems: 'center',
},
title: {
fontSize: 30,
fontWeight: '900',
textAlign: 'center',
marginBottom: 10,
},
body: {
fontSize: 15.5,
lineHeight: 22,
textAlign: 'center',
maxWidth: 320,
marginBottom: 20,
},
dots: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 'auto',
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
dotActive: {
width: 26,
height: 8,
borderRadius: 4,
},
cta: {
alignSelf: 'stretch',
height: 58,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
},
ctaText: {
fontSize: 17,
fontWeight: '800',
},
});