Press
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import {
|
||||
Image,
|
||||
ImageBackground,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
@@ -103,10 +104,14 @@ const getProofPoints = (language: Language): string[] => {
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
const { language } = useApp();
|
||||
const { height } = useWindowDimensions();
|
||||
const compact = height < 700;
|
||||
const { height, width } = useWindowDimensions();
|
||||
const compact = height < 700 || width < 380;
|
||||
const posthog = useSafeAnalytics();
|
||||
const insets = useSafeAreaInsets();
|
||||
// Der Hero bekommt den Rest der Hoehe, faellt aber nie unter diese Grenze.
|
||||
// Vorher war er fest auf 52% - dadurch hat die laengere Headline-Variante
|
||||
// den dritten Button aus dem Bildschirm geschoben.
|
||||
const heroMinHeight = Math.round(height * (compact ? 0.34 : 0.40));
|
||||
|
||||
const [variant, setVariant] = React.useState<WelcomeHeadlineVariant | null>(null);
|
||||
const copy = getWelcomeCopy(language, variant ?? 'symptom');
|
||||
@@ -132,104 +137,138 @@ export default function OnboardingScreen() {
|
||||
style={[{ position: 'absolute', top: 0, left: 0, right: 0, width: '100%' }, { height: compact ? '65%' : '70%' }]}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<View style={[{ width: '100%' }, { height: compact ? '48%' : '52%' }]}>
|
||||
<View style={[styles.heroSafe, { paddingTop: Math.max(insets.top, 16) }]}>
|
||||
<View style={styles.heroTopRow}>
|
||||
<View style={styles.brandRow}>
|
||||
<Image
|
||||
source={require('../assets/icon.png')}
|
||||
style={styles.logo}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<Text style={styles.brandName}>
|
||||
Green<Text style={styles.brandAccent}>Lens</Text>
|
||||
</Text>
|
||||
</View>
|
||||
{/* Rating und Anzahl immer gemeinsam: 5,0 aus 6 Bewertungen ist
|
||||
ehrlich und ueberpruefbar - eine nackte Sternezahl waere es nicht. */}
|
||||
<View style={styles.ratingPill}>
|
||||
<Ionicons name="star" size={13} color="#f5c04e" />
|
||||
<Text style={styles.ratingText}>{copy.ratingValue}</Text>
|
||||
<Text style={styles.ratingCount}>· {copy.ratingCount}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{testimonial ? (
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
<Text style={styles.testimonialText} numberOfLines={compact ? 3 : 4}>
|
||||
„{testimonial.quote}"
|
||||
</Text>
|
||||
<View style={styles.testimonialMeta}>
|
||||
<Text style={styles.testimonialAuthor}>
|
||||
{testimonial.author}
|
||||
{testimonial.isTranslated ? ` · ${translatedNote(language)}` : ''}
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={false}
|
||||
alwaysBounceVertical={false}
|
||||
>
|
||||
<View style={[styles.heroBlock, { minHeight: heroMinHeight }]}>
|
||||
<View style={[styles.heroSafe, { paddingTop: Math.max(insets.top, 16) }]}>
|
||||
<View style={styles.heroTopRow}>
|
||||
<View style={styles.brandRow}>
|
||||
<Image
|
||||
source={require('../assets/icon.png')}
|
||||
style={styles.logo}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<Text
|
||||
style={styles.brandName}
|
||||
maxFontSizeMultiplier={1.2}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.82}
|
||||
>
|
||||
Green<Text style={styles.brandAccent}>Lens</Text>
|
||||
</Text>
|
||||
</View>
|
||||
{/* Rating und Anzahl immer gemeinsam: 5,0 aus 6 Bewertungen ist
|
||||
ehrlich und ueberpruefbar - eine nackte Sternezahl waere es nicht. */}
|
||||
<View style={styles.ratingPill}>
|
||||
<Ionicons name="star" size={13} color="#f5c04e" />
|
||||
<Text style={styles.ratingText} maxFontSizeMultiplier={1.2} numberOfLines={1}>
|
||||
{copy.ratingValue}
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.ratingCount}
|
||||
maxFontSizeMultiplier={1.2}
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
minimumFontScale={0.72}
|
||||
>
|
||||
· {copy.ratingCount}
|
||||
</Text>
|
||||
<View style={styles.starsRow}>
|
||||
{Array.from({ length: testimonial.stars }).map((_, i) => (
|
||||
<Ionicons key={i} name="star" size={14} color="#f5c04e" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
/* Kein belegtes Zitat vorhanden → Argument statt erfundenem Social Proof. */
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
{proofPoints.map((point) => (
|
||||
<View key={point} style={styles.proofRow}>
|
||||
<Ionicons name="checkmark-circle" size={15} color="#a6d66f" />
|
||||
<Text style={styles.proofText}>{point}</Text>
|
||||
|
||||
{testimonial ? (
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
<Text
|
||||
style={styles.testimonialText}
|
||||
numberOfLines={compact ? 3 : 4}
|
||||
maxFontSizeMultiplier={1.3}
|
||||
>
|
||||
„{testimonial.quote}"
|
||||
</Text>
|
||||
<View style={styles.testimonialMeta}>
|
||||
<Text style={styles.testimonialAuthor} maxFontSizeMultiplier={1.2}>
|
||||
{testimonial.author}
|
||||
{testimonial.isTranslated ? ` · ${translatedNote(language)}` : ''}
|
||||
</Text>
|
||||
<View style={styles.starsRow}>
|
||||
{Array.from({ length: testimonial.stars }).map((_, i) => (
|
||||
<Ionicons key={i} name="star" size={14} color="#f5c04e" />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.sheet}>
|
||||
<View style={styles.sheetHandle} />
|
||||
<View style={styles.sheetContent}>
|
||||
<View style={styles.topSpacer} />
|
||||
{/* Erst rendern, wenn die Variante feststeht. Sonst blitzt fuer die
|
||||
Haelfte der Nutzer kurz die falsche Headline auf und wechselt dann -
|
||||
das sieht kaputt aus und verfaelscht den Test. */}
|
||||
<View style={{ opacity: variant ? 1 : 0 }}>
|
||||
<Text style={[styles.headline, compact && styles.headlineCompact]}>{copy.headline}</Text>
|
||||
<Text style={styles.subline}>{copy.subline}</Text>
|
||||
</View>
|
||||
) : (
|
||||
/* Kein belegtes Zitat vorhanden → Argument statt erfundenem Social Proof. */
|
||||
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
|
||||
{proofPoints.map((point) => (
|
||||
<View key={point} style={styles.proofRow}>
|
||||
<Ionicons name="checkmark-circle" size={15} color="#a6d66f" />
|
||||
<Text style={styles.proofText} maxFontSizeMultiplier={1.3}>{point}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.cta}
|
||||
onPress={() => {
|
||||
posthog.capture('onboarding_started', { welcome_variant: variant ?? 'unassigned' });
|
||||
router.push('/onboarding/slides');
|
||||
}}
|
||||
activeOpacity={0.86}
|
||||
>
|
||||
<Text style={styles.ctaText}>{copy.cta}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => router.push('/auth/login')} style={styles.loginLink}>
|
||||
<Text style={styles.loginText}>{copy.login}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
posthog.capture('onboarding_demo_scan_started', {
|
||||
welcome_variant: variant ?? 'unassigned',
|
||||
});
|
||||
router.push('/scanner');
|
||||
}}
|
||||
style={styles.demoLink}
|
||||
>
|
||||
<Ionicons name="scan-outline" size={18} color="#a6d66f" />
|
||||
<Text style={styles.demoText}>{copy.demoScan}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={styles.legal}>{copy.legal}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.sheet, { paddingBottom: Math.max(insets.bottom, 12) }]}>
|
||||
<View style={styles.sheetHandle} />
|
||||
<View style={styles.sheetContent}>
|
||||
{/* Erst rendern, wenn die Variante feststeht. Sonst blitzt fuer die
|
||||
Haelfte der Nutzer kurz die falsche Headline auf und wechselt dann -
|
||||
das sieht kaputt aus und verfaelscht den Test. */}
|
||||
<View style={{ opacity: variant ? 1 : 0 }}>
|
||||
{/* maxFontSizeMultiplier: groessere Systemschrift darf die Headline
|
||||
wachsen lassen, aber nicht die CTAs aus dem Screen schieben. */}
|
||||
<Text
|
||||
style={[styles.headline, compact && styles.headlineCompact]}
|
||||
maxFontSizeMultiplier={1.25}
|
||||
>
|
||||
{copy.headline}
|
||||
</Text>
|
||||
<Text style={styles.subline} maxFontSizeMultiplier={1.3}>{copy.subline}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.cta}
|
||||
onPress={() => {
|
||||
posthog.capture('onboarding_started', { welcome_variant: variant ?? 'unassigned' });
|
||||
router.push('/onboarding/slides');
|
||||
}}
|
||||
activeOpacity={0.86}
|
||||
>
|
||||
<Text style={styles.ctaText} maxFontSizeMultiplier={1.2}>{copy.cta}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity onPress={() => router.push('/auth/login')} style={styles.loginLink}>
|
||||
<Text style={styles.loginText} maxFontSizeMultiplier={1.2}>{copy.login}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
posthog.capture('onboarding_demo_scan_started', {
|
||||
welcome_variant: variant ?? 'unassigned',
|
||||
});
|
||||
router.push('/scanner');
|
||||
}}
|
||||
style={styles.demoLink}
|
||||
>
|
||||
<Ionicons name="scan-outline" size={18} color="#a6d66f" />
|
||||
<Text style={styles.demoText} maxFontSizeMultiplier={1.2}>{copy.demoScan}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={styles.legal} maxFontSizeMultiplier={1.3}>{copy.legal}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -239,6 +278,22 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: '#0a110b',
|
||||
},
|
||||
scroll: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
// Fuellt mindestens den Bildschirm. Wird der Inhalt hoeher (lange
|
||||
// Headline-Variante, grosse Systemschrift, kleines Geraet), scrollt der
|
||||
// Screen - statt die unteren Buttons abzuschneiden.
|
||||
flexGrow: 1,
|
||||
},
|
||||
heroBlock: {
|
||||
width: '100%',
|
||||
// Nimmt den Platz, den das Sheet uebrig laesst, faellt aber nie unter
|
||||
// heroMinHeight. Vorher fest 52% - das hat nur bei kurzer Copy gepasst.
|
||||
flexGrow: 1,
|
||||
flexShrink: 0,
|
||||
},
|
||||
hero: {
|
||||
width: '100%',
|
||||
},
|
||||
@@ -269,6 +324,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
flexShrink: 1,
|
||||
},
|
||||
logo: {
|
||||
width: 36,
|
||||
@@ -277,6 +333,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
brandName: {
|
||||
flexShrink: 1,
|
||||
color: '#ffffff',
|
||||
fontSize: 24,
|
||||
fontWeight: '900',
|
||||
@@ -285,9 +342,11 @@ const styles = StyleSheet.create({
|
||||
color: '#8ba885',
|
||||
},
|
||||
ratingPill: {
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
marginLeft: 10,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.55)',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.25)',
|
||||
@@ -301,6 +360,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
},
|
||||
ratingCount: {
|
||||
flexShrink: 1,
|
||||
color: '#c9d3c2',
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
@@ -347,6 +407,9 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
testimonialAuthor: {
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
marginRight: 8,
|
||||
color: '#a6d66f',
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
@@ -356,7 +419,7 @@ const styles = StyleSheet.create({
|
||||
gap: 1,
|
||||
},
|
||||
sheet: {
|
||||
flex: 1,
|
||||
flexShrink: 0,
|
||||
backgroundColor: '#0c160d',
|
||||
borderTopLeftRadius: 28,
|
||||
borderTopRightRadius: 28,
|
||||
@@ -374,14 +437,10 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
sheetContent: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 8,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
},
|
||||
topSpacer: {
|
||||
height: 12,
|
||||
},
|
||||
headline: {
|
||||
color: '#ffffff',
|
||||
fontSize: 28,
|
||||
@@ -402,15 +461,16 @@ const styles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
},
|
||||
spacer: {
|
||||
height: 18,
|
||||
height: 14,
|
||||
},
|
||||
cta: {
|
||||
height: 60,
|
||||
minHeight: 58,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#437824',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 6,
|
||||
marginBottom: 4,
|
||||
},
|
||||
ctaText: {
|
||||
color: '#f8f7ef',
|
||||
@@ -419,7 +479,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
loginLink: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
paddingVertical: 9,
|
||||
},
|
||||
loginText: {
|
||||
color: '#a6d66f',
|
||||
@@ -431,13 +491,14 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
height: 52,
|
||||
minHeight: 50,
|
||||
paddingVertical: 13,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1.5,
|
||||
borderColor: '#a6d66f',
|
||||
backgroundColor: 'rgba(166, 214, 111, 0.05)',
|
||||
marginVertical: 8,
|
||||
marginBottom: 12,
|
||||
marginTop: 6,
|
||||
marginBottom: 10,
|
||||
},
|
||||
demoText: {
|
||||
color: '#a6d66f',
|
||||
|
||||
Reference in New Issue
Block a user