feat: Implement mobile application and lead processing utilities.

This commit is contained in:
2026-02-19 14:21:51 +01:00
parent fca42db4d2
commit c53a71a5f9
120 changed files with 24080 additions and 851 deletions

View File

@@ -1,44 +1,106 @@
import { View, Text, TouchableOpacity } from 'react-native'
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
import { useRouter, useLocalSearchParams } from 'expo-router'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Ionicons } from '@expo/vector-icons'
export default function CheckEmailScreen() {
const router = useRouter()
const { email } = useLocalSearchParams<{ email: string }>()
return (
<SafeAreaView className="flex-1 bg-white">
<View className="flex-1 justify-center items-center px-6">
{/* Envelope Illustration */}
<View className="w-24 h-24 bg-brand-50 rounded-full items-center justify-center mb-6">
<Text className="text-5xl">📧</Text>
<SafeAreaView style={styles.safeArea}>
<View style={styles.content}>
<View style={styles.iconBox}>
<Ionicons name="checkmark-circle" size={46} color="#15803D" />
</View>
<Text className="text-2xl font-bold text-gray-900 text-center mb-3">
Schau in dein Postfach
</Text>
<Text className="text-gray-500 text-center leading-6 mb-2">
Wir haben einen Login-Link an
</Text>
<Text className="font-semibold text-gray-900 text-center mb-6">
{email}
</Text>
<Text className="text-gray-500 text-center leading-6">
Klicken Sie auf den Link in der E-Mail, um sich einzuloggen.
Der Link ist 24 Stunden gültig.
<Text style={styles.title}>E-Mail pruefen</Text>
<Text style={styles.body}>Wir haben einen Login-Link gesendet an:</Text>
<View style={styles.emailBox}>
<Text style={styles.emailText} numberOfLines={1}>{email}</Text>
</View>
<Text style={styles.hint}>
Bitte oeffnen Sie den Link in Ihrer E-Mail, um sich anzumelden.
</Text>
<View className="mt-10 space-y-3 w-full">
<TouchableOpacity
onPress={() => router.back()}
className="py-3 items-center"
>
<Text className="text-brand-500 font-medium">
Andere E-Mail verwenden
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
onPress={() => router.back()}
style={styles.backBtn}
activeOpacity={0.8}
>
<Ionicons name="chevron-back" size={16} color="#003B7E" />
<Text style={styles.backText}>Andere E-Mail verwenden</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#FFFFFF',
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 28,
},
iconBox: {
width: 88,
height: 88,
backgroundColor: '#DCFCE7',
borderRadius: 44,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
},
title: {
fontSize: 24,
fontWeight: '800',
color: '#0F172A',
textAlign: 'center',
marginBottom: 10,
},
body: {
fontSize: 14,
color: '#64748B',
textAlign: 'center',
marginBottom: 6,
},
emailBox: {
backgroundColor: '#F8FAFC',
borderWidth: 1,
borderColor: '#E2E8F0',
borderRadius: 14,
paddingHorizontal: 16,
paddingVertical: 10,
marginBottom: 16,
},
emailText: {
fontSize: 15,
fontWeight: '700',
color: '#0F172A',
},
hint: {
fontSize: 13,
color: '#64748B',
textAlign: 'center',
lineHeight: 19,
paddingHorizontal: 8,
},
backBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginTop: 28,
},
backText: {
fontSize: 14,
fontWeight: '700',
color: '#003B7E',
},
})

View File

@@ -1,16 +1,12 @@
import {
View,
Text,
TextInput,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
View, Text, TextInput, TouchableOpacity,
KeyboardAvoidingView, Platform, ActivityIndicator, StyleSheet,
} from 'react-native'
import { useState } from 'react'
import { useRouter } from 'expo-router'
import { authClient } from '@/lib/auth-client'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Ionicons } from '@expo/vector-icons'
import { authClient } from '@/lib/auth-client'
export default function LoginScreen() {
const router = useRouter()
@@ -18,16 +14,16 @@ export default function LoginScreen() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const canSubmit = email.trim().length > 0 && !loading
async function handleSendLink() {
if (!email.trim()) return
setLoading(true)
setError('')
const result = await authClient.signIn.magicLink({
email: email.trim().toLowerCase(),
callbackURL: '/news',
callbackURL: '/home',
})
setLoading(false)
if (result.error) {
setError(result.error.message ?? 'Ein Fehler ist aufgetreten.')
@@ -37,33 +33,28 @@ export default function LoginScreen() {
}
return (
<SafeAreaView className="flex-1 bg-white">
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1"
style={styles.keyboardView}
>
<View className="flex-1 justify-center px-6">
{/* Logo */}
<View className="items-center mb-10">
<View className="w-20 h-20 bg-brand-500 rounded-2xl items-center justify-center mb-4">
<Text className="text-white font-bold text-3xl">I</Text>
<View style={styles.content}>
<View style={styles.logoSection}>
<View style={styles.logoBox}>
<Text style={styles.logoLetter}>I</Text>
</View>
<Text className="text-2xl font-bold text-gray-900">InnungsApp</Text>
<Text className="text-gray-500 mt-1 text-center">
Die digitale Plattform Ihrer Innung
</Text>
<Text style={styles.appName}>InnungsApp</Text>
<Text style={styles.tagline}>Ihre Kreishandwerkerschaft digital</Text>
</View>
{/* Form */}
<View className="space-y-4">
<View>
<Text className="text-sm font-medium text-gray-700 mb-2">
E-Mail-Adresse
</Text>
<View style={styles.form}>
<Text style={styles.inputLabel}>Email-Adresse</Text>
<View style={styles.inputWrap}>
<Ionicons name="mail-outline" size={18} color="#94A3B8" />
<TextInput
className="border border-gray-300 rounded-xl px-4 py-3.5 text-base text-gray-900 bg-gray-50"
placeholder="ihre@email.de"
placeholderTextColor="#9ca3af"
style={styles.input}
placeholder="beispiel@handwerk.de"
placeholderTextColor="#94A3B8"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
@@ -75,37 +66,141 @@ export default function LoginScreen() {
</View>
{error ? (
<View className="bg-red-50 rounded-xl px-4 py-3">
<Text className="text-red-600 text-sm">{error}</Text>
<View style={styles.errorBox}>
<Text style={styles.errorText}>{error}</Text>
</View>
) : null}
<TouchableOpacity
onPress={handleSendLink}
disabled={loading || !email.trim()}
className={`rounded-xl py-4 items-center ${
loading || !email.trim() ? 'bg-gray-200' : 'bg-brand-500'
}`}
disabled={!canSubmit}
style={[styles.submitBtn, !canSubmit && styles.submitBtnDisabled]}
activeOpacity={0.85}
>
{loading ? (
<ActivityIndicator color="white" />
<ActivityIndicator color="#FFFFFF" />
) : (
<Text
className={`font-semibold text-base ${
loading || !email.trim() ? 'text-gray-400' : 'text-white'
}`}
>
Magic Link senden
</Text>
<View style={styles.submitContent}>
<Text style={styles.submitLabel}>Login-Link senden</Text>
<Ionicons name="arrow-forward" size={16} color="#FFFFFF" />
</View>
)}
</TouchableOpacity>
<Text className="text-center text-sm text-gray-400">
Kein Passwort nötig Zugang nur per Admin-Einladung
</Text>
</View>
<Text style={styles.hint}>
Noch kein Zugang? Kontaktieren Sie Ihre Innungsgeschaeftsstelle.
</Text>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#FFFFFF',
},
keyboardView: {
flex: 1,
},
content: {
flex: 1,
justifyContent: 'center',
paddingHorizontal: 24,
},
logoSection: {
alignItems: 'center',
marginBottom: 40,
},
logoBox: {
width: 64,
height: 64,
backgroundColor: '#003B7E',
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
logoLetter: {
color: '#FFFFFF',
fontSize: 30,
fontWeight: '900',
},
appName: {
fontSize: 30,
fontWeight: '800',
color: '#0F172A',
letterSpacing: -0.6,
marginBottom: 4,
},
tagline: {
fontSize: 14,
color: '#64748B',
textAlign: 'center',
},
form: {
gap: 12,
},
inputLabel: {
fontSize: 14,
fontWeight: '700',
color: '#334155',
},
inputWrap: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#F8FAFC',
borderRadius: 14,
borderWidth: 1,
borderColor: '#E2E8F0',
paddingHorizontal: 12,
gap: 8,
},
input: {
flex: 1,
paddingVertical: 13,
color: '#0F172A',
fontSize: 15,
},
errorBox: {
backgroundColor: '#FEF2F2',
borderWidth: 1,
borderColor: '#FECACA',
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 10,
},
errorText: {
color: '#B91C1C',
fontSize: 13,
},
submitBtn: {
backgroundColor: '#003B7E',
borderRadius: 14,
paddingVertical: 14,
alignItems: 'center',
marginTop: 4,
},
submitBtnDisabled: {
backgroundColor: '#CBD5E1',
},
submitContent: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
submitLabel: {
color: '#FFFFFF',
fontWeight: '700',
fontSize: 15,
},
hint: {
marginTop: 24,
textAlign: 'center',
color: '#64748B',
fontSize: 13,
lineHeight: 18,
},
})