feat(rebreak-native): Nuxt-style splash, domain normalization on blur, app-wide keyboard fix
- 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>
This commit is contained in:
parent
0bad2185ec
commit
5b12f14a90
@ -1,32 +1,241 @@
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
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 { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function HomeScreen() {
|
||||
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 (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
<View className="flex-1 items-center justify-center px-6">
|
||||
<Text className="text-4xl text-neutral-900 mb-3" style={{ fontFamily: 'Nunito_700Bold' }}>{t('landing.appName')}</Text>
|
||||
<Text className="text-base text-neutral-500 text-center mb-12" style={{ fontFamily: 'Nunito_400Regular' }}>
|
||||
{t('landing.tagline')}
|
||||
</Text>
|
||||
<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>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push('/signin')}
|
||||
activeOpacity={0.8}
|
||||
className="bg-rebreak-500 px-8 py-4 rounded-full"
|
||||
{/* 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 }],
|
||||
}}
|
||||
>
|
||||
<Text className="text-white text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('landing.start')}</Text>
|
||||
</TouchableOpacity>
|
||||
{t('appHeader.appName')}
|
||||
</Animated.Text>
|
||||
|
||||
<Text className="text-xs text-neutral-400 mt-8" style={{ fontFamily: 'Nunito_400Regular' }}>
|
||||
{t('landing.version')}
|
||||
</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>
|
||||
</SafeAreaView>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Easing,
|
||||
Keyboard,
|
||||
Modal,
|
||||
@ -121,13 +122,14 @@ export function KeyboardAwareSheet({
|
||||
|
||||
// Sheet-Höhe wächst/schrumpft mit Tastatur
|
||||
useEffect(() => {
|
||||
const maxHeight = Dimensions.get('window').height - insets.top - 20;
|
||||
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
||||
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
||||
const showSub = Keyboard.addListener(showEvent, (e) => {
|
||||
const h = e.endCoordinates.height;
|
||||
setKeyboardHeight(h);
|
||||
Animated.timing(sheetHeight, {
|
||||
toValue: collapsedHeight + h,
|
||||
toValue: Math.min(collapsedHeight + h, maxHeight),
|
||||
duration: Platform.OS === 'ios' ? (e.duration ?? 250) : 220,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
useNativeDriver: false,
|
||||
@ -146,7 +148,7 @@ export function KeyboardAwareSheet({
|
||||
showSub.remove();
|
||||
hideSub.remove();
|
||||
};
|
||||
}, [sheetHeight, collapsedHeight]);
|
||||
}, [sheetHeight, collapsedHeight, insets.top]);
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="none" onRequestClose={onClose}>
|
||||
|
||||
@ -3,7 +3,6 @@ import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
Pressable,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
@ -82,11 +81,11 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={close} hitSlop={10}>
|
||||
<TouchableOpacity onPress={close} hitSlop={10} activeOpacity={0.6}>
|
||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
|
||||
{t('common.cancel')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</TouchableOpacity>
|
||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||
{t('blocker.add_sheet_title')}
|
||||
</Text>
|
||||
@ -121,6 +120,10 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
||||
setInput(v);
|
||||
setError(null);
|
||||
}}
|
||||
onBlur={() => {
|
||||
const n = normalizeDomain(input);
|
||||
if (n !== input) setInput(n);
|
||||
}}
|
||||
placeholder={t('blocker.add_sheet_placeholder')}
|
||||
placeholderTextColor={colors.textMuted}
|
||||
autoCapitalize="none"
|
||||
@ -215,8 +218,9 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
||||
|
||||
{/* Confirm-Checkbox */}
|
||||
{valid && (
|
||||
<Pressable
|
||||
<TouchableOpacity
|
||||
onPress={() => setConfirmPermanent((v) => !v)}
|
||||
activeOpacity={0.7}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
@ -250,7 +254,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
||||
>
|
||||
{t('blocker.add_sheet_confirm_permanent')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Text,
|
||||
@ -337,6 +338,7 @@ function FormView({
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={{ padding: 20, gap: 14 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
automaticallyAdjustKeyboardInsets={Platform.OS === 'ios'}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* App-Password-Guide-Hinweis */}
|
||||
|
||||
@ -81,8 +81,7 @@
|
||||
"landing": {
|
||||
"appName": "Rebreak",
|
||||
"tagline": "Du gehst nicht allein.",
|
||||
"start": "Loslegen",
|
||||
"version": "v0.1.0 — RN Migration Phase 1 Skeleton"
|
||||
"start": "Registrieren"
|
||||
},
|
||||
"splash": {
|
||||
"tagline": "You will never walk alone!",
|
||||
|
||||
@ -81,8 +81,7 @@
|
||||
"landing": {
|
||||
"appName": "Rebreak",
|
||||
"tagline": "You're not walking alone.",
|
||||
"start": "Get started",
|
||||
"version": "v0.1.0 — RN Migration Phase 1 Skeleton"
|
||||
"start": "Sign up"
|
||||
},
|
||||
"splash": {
|
||||
"tagline": "You will never walk alone!",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user