- app/index.tsx: replaced the placeholder landing with the BrandSplash look (#0f172a bg, SVG radial glows, breathing animation, staggered fade/bounce-ins for app name / logo / tagline / CTAs, "Made in Germany" footer). Dropped the "v0.1.0 RN Migration Phase 1 Skeleton" line; landing.version removed from locales. - AddDomainSheet: onBlur runs normalizeDomain() (strips scheme/www./path/query and email local-part) so the user sees the cleaned registrable domain before adding; also swapped the two leftover Pressables → TouchableOpacity (no-Pressable rule). - KeyboardAwareSheet: clamp the sheet height to (screenHeight - insets.top - 20) while the keyboard is up, so tall sheets (e.g. AddDomainSheet's 600px) don't grow off-screen and clip the inputs at the top. - ConnectMailSheet: automaticallyAdjustKeyboardInsets on iOS so focused inputs scroll into view. Covered sheets: AddDomainSheet, ConnectMailSheet, EditMailAccountSheet, AddMacSheet, AddWindowsSheet. JS-only (hot-reloadable). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
242 lines
8.0 KiB
TypeScript
242 lines
8.0 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { Animated, Dimensions, Image, Text, TouchableOpacity, View } from 'react-native';
|
|
import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg';
|
|
import { useRouter } from 'expo-router';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
const { width: SW, height: SH } = Dimensions.get('window');
|
|
|
|
export default function LandingScreen() {
|
|
const router = useRouter();
|
|
const insets = useSafeAreaInsets();
|
|
const { t } = useTranslation();
|
|
|
|
const glowTopOpacity = useRef(new Animated.Value(0.5)).current;
|
|
const glowCenterOpacity = useRef(new Animated.Value(0)).current;
|
|
const glowCenterScale = useRef(new Animated.Value(0.6)).current;
|
|
|
|
const nameOpacity = useRef(new Animated.Value(0)).current;
|
|
const nameTranslateY = useRef(new Animated.Value(12)).current;
|
|
|
|
const logoOpacity = useRef(new Animated.Value(0)).current;
|
|
const logoScale = useRef(new Animated.Value(0.82)).current;
|
|
const logoTranslateY = useRef(new Animated.Value(8)).current;
|
|
const logoPulse = useRef(new Animated.Value(1)).current;
|
|
|
|
const taglineOpacity = useRef(new Animated.Value(0)).current;
|
|
const taglineTranslateY = useRef(new Animated.Value(8)).current;
|
|
|
|
const ctaOpacity = useRef(new Animated.Value(0)).current;
|
|
const ctaTranslateY = useRef(new Animated.Value(10)).current;
|
|
|
|
const footerOpacity = useRef(new Animated.Value(0)).current;
|
|
|
|
useEffect(() => {
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(glowTopOpacity, { toValue: 0.9, duration: 2000, useNativeDriver: true }),
|
|
Animated.timing(glowTopOpacity, { toValue: 0.5, duration: 2000, useNativeDriver: true }),
|
|
]),
|
|
).start();
|
|
|
|
const ease = (toValue: number, duration: number) => ({ toValue, duration, useNativeDriver: true });
|
|
|
|
Animated.parallel([
|
|
Animated.timing(glowCenterOpacity, ease(1, 900)),
|
|
Animated.timing(glowCenterScale, ease(1, 900)),
|
|
]).start();
|
|
|
|
setTimeout(() => {
|
|
Animated.parallel([
|
|
Animated.timing(nameOpacity, ease(1, 600)),
|
|
Animated.timing(nameTranslateY, ease(0, 600)),
|
|
]).start();
|
|
}, 300);
|
|
|
|
setTimeout(() => {
|
|
Animated.parallel([
|
|
Animated.timing(logoOpacity, ease(1, 650)),
|
|
Animated.spring(logoScale, { toValue: 1, useNativeDriver: true, friction: 6, tension: 80 }),
|
|
Animated.timing(logoTranslateY, ease(0, 650)),
|
|
]).start();
|
|
}, 700);
|
|
|
|
setTimeout(() => {
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(logoPulse, { toValue: 1.04, duration: 1300, useNativeDriver: true }),
|
|
Animated.timing(logoPulse, { toValue: 1, duration: 1300, useNativeDriver: true }),
|
|
]),
|
|
).start();
|
|
}, 1100);
|
|
|
|
setTimeout(() => {
|
|
Animated.parallel([
|
|
Animated.timing(taglineOpacity, ease(1, 550)),
|
|
Animated.timing(taglineTranslateY, ease(0, 550)),
|
|
]).start();
|
|
}, 1300);
|
|
|
|
setTimeout(() => {
|
|
Animated.parallel([
|
|
Animated.timing(ctaOpacity, ease(1, 500)),
|
|
Animated.timing(ctaTranslateY, ease(0, 500)),
|
|
Animated.timing(footerOpacity, ease(1, 600)),
|
|
]).start();
|
|
}, 1700);
|
|
}, [
|
|
glowTopOpacity, glowCenterOpacity, glowCenterScale,
|
|
nameOpacity, nameTranslateY, logoOpacity, logoScale, logoTranslateY,
|
|
logoPulse, taglineOpacity, taglineTranslateY, ctaOpacity, ctaTranslateY, footerOpacity,
|
|
]);
|
|
|
|
return (
|
|
<View style={{ flex: 1, backgroundColor: '#0f172a', overflow: 'hidden' }}>
|
|
{/* Top breathing glow */}
|
|
<Animated.View
|
|
pointerEvents="none"
|
|
style={{ position: 'absolute', top: 0, left: 0, right: 0, height: SH * 0.5, opacity: glowTopOpacity }}
|
|
>
|
|
<Svg width="100%" height="100%">
|
|
<Defs>
|
|
<RadialGradient id="topGlowL" cx="50%" cy="0%" rx="70%" ry="100%" fx="50%" fy="0%">
|
|
<Stop offset="0%" stopColor="#1e3a8a" stopOpacity="1" />
|
|
<Stop offset="100%" stopColor="#1e3a8a" stopOpacity="0" />
|
|
</RadialGradient>
|
|
</Defs>
|
|
<Rect width="100%" height="100%" fill="url(#topGlowL)" />
|
|
</Svg>
|
|
</Animated.View>
|
|
|
|
{/* Center indigo halo */}
|
|
<Animated.View
|
|
pointerEvents="none"
|
|
style={{
|
|
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
opacity: glowCenterOpacity, transform: [{ scale: glowCenterScale }],
|
|
}}
|
|
>
|
|
<Svg width="100%" height="100%">
|
|
<Defs>
|
|
<RadialGradient id="centerHaloL" cx="50%" cy="45%" rx="55%" ry="55%">
|
|
<Stop offset="0%" stopColor="#6366f1" stopOpacity="0.22" />
|
|
<Stop offset="100%" stopColor="#6366f1" stopOpacity="0" />
|
|
</RadialGradient>
|
|
</Defs>
|
|
<Rect width="100%" height="100%" fill="url(#centerHaloL)" />
|
|
</Svg>
|
|
</Animated.View>
|
|
|
|
{/* Content */}
|
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 20, paddingHorizontal: 24 }}>
|
|
<Animated.Text
|
|
style={{
|
|
fontFamily: 'Nunito_800ExtraBold',
|
|
fontSize: 48,
|
|
letterSpacing: -1,
|
|
color: '#ffffff',
|
|
textAlign: 'center',
|
|
marginBottom: 8,
|
|
opacity: nameOpacity,
|
|
transform: [{ translateY: nameTranslateY }],
|
|
}}
|
|
>
|
|
{t('appHeader.appName')}
|
|
</Animated.Text>
|
|
|
|
<Animated.View
|
|
style={{
|
|
opacity: logoOpacity,
|
|
transform: [
|
|
{ scale: Animated.multiply(logoScale, logoPulse) as any },
|
|
{ translateY: logoTranslateY },
|
|
],
|
|
}}
|
|
>
|
|
<Image
|
|
source={require('../assets/icon.png')}
|
|
style={{ width: 160, height: 160, borderRadius: 28 }}
|
|
resizeMode="contain"
|
|
/>
|
|
</Animated.View>
|
|
|
|
<Animated.Text
|
|
style={{
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
fontSize: 20,
|
|
letterSpacing: 0.2,
|
|
color: 'rgba(255,255,255,0.90)',
|
|
textAlign: 'center',
|
|
marginTop: 4,
|
|
opacity: taglineOpacity,
|
|
transform: [{ translateY: taglineTranslateY }],
|
|
}}
|
|
>
|
|
{t('splash.tagline')}
|
|
</Animated.Text>
|
|
|
|
<Animated.View
|
|
style={{
|
|
alignSelf: 'stretch',
|
|
gap: 12,
|
|
marginTop: 16,
|
|
opacity: ctaOpacity,
|
|
transform: [{ translateY: ctaTranslateY }],
|
|
}}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={() => router.push('/signin')}
|
|
activeOpacity={0.85}
|
|
style={{
|
|
backgroundColor: '#6366f1',
|
|
borderRadius: 16,
|
|
paddingVertical: 16,
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 16, color: '#ffffff' }}>
|
|
{t('auth.signin')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => router.push('/signup')}
|
|
activeOpacity={0.85}
|
|
style={{
|
|
borderWidth: 1.5,
|
|
borderColor: 'rgba(255,255,255,0.25)',
|
|
borderRadius: 16,
|
|
paddingVertical: 16,
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Text style={{ fontFamily: 'Nunito_600SemiBold', fontSize: 16, color: 'rgba(255,255,255,0.85)' }}>
|
|
{t('landing.start')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</Animated.View>
|
|
</View>
|
|
|
|
{/* Footer */}
|
|
<Animated.Text
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: insets.bottom + 16,
|
|
left: 0,
|
|
right: 0,
|
|
fontFamily: 'Nunito_400Regular',
|
|
fontSize: 11,
|
|
letterSpacing: 1.5,
|
|
textTransform: 'uppercase',
|
|
color: 'rgba(255,255,255,0.28)',
|
|
textAlign: 'center',
|
|
opacity: footerOpacity,
|
|
}}
|
|
>
|
|
{t('splash.madeInGermany')}
|
|
</Animated.Text>
|
|
</View>
|
|
);
|
|
}
|