push
This commit is contained in:
@@ -1,15 +1,222 @@
|
||||
import { Tabs, Redirect } from 'expo-router'
|
||||
import { Platform } from 'react-native'
|
||||
import { Platform, View, Text, StyleSheet, TextInput, TouchableOpacity, ActivityIndicator, Alert, ScrollView } from 'react-native'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Ionicons } from '@expo/vector-icons'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useAuthStore } from '@/store/auth.store'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { setupPushNotifications } from '@/lib/notifications'
|
||||
import { authClient } from '@/lib/auth-client'
|
||||
|
||||
function UnreadBadge({ count }: { count: number }) {
|
||||
if (count === 0) return null
|
||||
return (
|
||||
<View style={badge.dot}>
|
||||
<Text style={badge.text}>{count > 9 ? '9+' : count}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const badge = StyleSheet.create({
|
||||
dot: {
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: -8,
|
||||
minWidth: 17,
|
||||
height: 17,
|
||||
borderRadius: 9,
|
||||
backgroundColor: '#DC2626',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 4,
|
||||
borderWidth: 2,
|
||||
borderColor: '#FFFFFF',
|
||||
},
|
||||
text: {
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
color: '#FFFFFF',
|
||||
lineHeight: 13,
|
||||
},
|
||||
})
|
||||
|
||||
function ChatTabIcon({ color, focused }: { color: string; focused: boolean }) {
|
||||
const { data: unreadCount } = trpc.messages.getConversations.useQuery(undefined, {
|
||||
refetchInterval: 15_000,
|
||||
staleTime: 10_000,
|
||||
select: (data) => data.filter((c) => c.hasUnread).length,
|
||||
})
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Ionicons name={focused ? 'chatbubbles' : 'chatbubbles-outline'} size={23} color={color} />
|
||||
<UnreadBadge count={unreadCount ?? 0} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ForcePasswordChangeScreen() {
|
||||
const { setSession, signOut } = useAuthStore()
|
||||
const [current, setCurrent] = useState('')
|
||||
const [next, setNext] = useState('')
|
||||
const [confirm, setConfirm] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
async function handleSubmit() {
|
||||
setError('')
|
||||
if (!current) { setError('Bitte temporäres Passwort eingeben.'); return }
|
||||
if (next.length < 8) { setError('Das neue Passwort muss mindestens 8 Zeichen haben.'); return }
|
||||
if (next !== confirm) { setError('Die Passwörter stimmen nicht überein.'); return }
|
||||
|
||||
setLoading(true)
|
||||
const result = await authClient.changePassword({ currentPassword: current, newPassword: next })
|
||||
setLoading(false)
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message ?? 'Passwort konnte nicht geändert werden.')
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh session — mustChangePassword is now false
|
||||
const sessionResult = await authClient.getSession()
|
||||
if (sessionResult?.data?.user) {
|
||||
const u = sessionResult.data.user as any
|
||||
await setSession({
|
||||
user: {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
mustChangePassword: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={fpc.safe}>
|
||||
<ScrollView contentContainerStyle={fpc.content} keyboardShouldPersistTaps="handled">
|
||||
<View style={fpc.card}>
|
||||
<View style={fpc.iconWrap}>
|
||||
<Ionicons name="lock-closed-outline" size={32} color="#003B7E" />
|
||||
</View>
|
||||
<Text style={fpc.title}>Passwort ändern</Text>
|
||||
<Text style={fpc.subtitle}>
|
||||
Ihr Administrator hat ein temporäres Passwort vergeben. Bitte legen Sie jetzt Ihr persönliches Passwort fest.
|
||||
</Text>
|
||||
|
||||
<View style={fpc.field}>
|
||||
<Text style={fpc.label}>Temporäres Passwort</Text>
|
||||
<TextInput
|
||||
style={fpc.input}
|
||||
value={current}
|
||||
onChangeText={setCurrent}
|
||||
secureTextEntry
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor="#CBD5E1"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
<View style={fpc.field}>
|
||||
<Text style={fpc.label}>Neues Passwort</Text>
|
||||
<TextInput
|
||||
style={fpc.input}
|
||||
value={next}
|
||||
onChangeText={setNext}
|
||||
secureTextEntry
|
||||
placeholder="Mindestens 8 Zeichen"
|
||||
placeholderTextColor="#CBD5E1"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
<View style={fpc.field}>
|
||||
<Text style={fpc.label}>Neues Passwort wiederholen</Text>
|
||||
<TextInput
|
||||
style={fpc.input}
|
||||
value={confirm}
|
||||
onChangeText={setConfirm}
|
||||
secureTextEntry
|
||||
placeholder="Neues Passwort wiederholen"
|
||||
placeholderTextColor="#CBD5E1"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{!!error && (
|
||||
<View style={fpc.errorBox}>
|
||||
<Text style={fpc.errorText}>{error}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity style={fpc.btn} onPress={handleSubmit} disabled={loading}>
|
||||
{loading
|
||||
? <ActivityIndicator color="#fff" />
|
||||
: <Text style={fpc.btnText}>Passwort festlegen</Text>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={fpc.logoutBtn} onPress={() => void signOut()}>
|
||||
<Text style={fpc.logoutText}>Abmelden</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
const fpc = StyleSheet.create({
|
||||
safe: { flex: 1, backgroundColor: '#F8FAFC' },
|
||||
content: { flex: 1, justifyContent: 'center', padding: 24 },
|
||||
card: {
|
||||
backgroundColor: '#FFFFFF', borderRadius: 20,
|
||||
borderWidth: 1, borderColor: '#E2E8F0',
|
||||
padding: 24, gap: 12,
|
||||
},
|
||||
iconWrap: {
|
||||
width: 60, height: 60, borderRadius: 16,
|
||||
backgroundColor: '#EFF6FF', alignItems: 'center', justifyContent: 'center',
|
||||
alignSelf: 'center', marginBottom: 4,
|
||||
},
|
||||
title: { fontSize: 22, fontWeight: '800', color: '#0F172A', textAlign: 'center' },
|
||||
subtitle: { fontSize: 13, color: '#64748B', textAlign: 'center', lineHeight: 19 },
|
||||
field: { gap: 4 },
|
||||
label: { fontSize: 12, fontWeight: '700', color: '#475569', textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||
input: {
|
||||
borderWidth: 1, borderColor: '#E2E8F0', borderRadius: 10,
|
||||
paddingHorizontal: 12, paddingVertical: 11,
|
||||
fontSize: 14, color: '#0F172A', backgroundColor: '#F8FAFC',
|
||||
},
|
||||
errorBox: {
|
||||
backgroundColor: '#FEF2F2', borderWidth: 1,
|
||||
borderColor: '#FECACA', borderRadius: 10,
|
||||
paddingHorizontal: 12, paddingVertical: 10,
|
||||
},
|
||||
errorText: { color: '#B91C1C', fontSize: 13 },
|
||||
btn: {
|
||||
backgroundColor: '#003B7E', borderRadius: 12,
|
||||
paddingVertical: 13, alignItems: 'center', marginTop: 4,
|
||||
},
|
||||
btnText: { color: '#FFFFFF', fontWeight: '700', fontSize: 15 },
|
||||
logoutBtn: { alignItems: 'center', paddingVertical: 8 },
|
||||
logoutText: { color: '#94A3B8', fontSize: 13 },
|
||||
})
|
||||
|
||||
export default function AppLayout() {
|
||||
const session = useAuthStore((s) => s.session)
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.user) return
|
||||
setupPushNotifications().catch(() => {})
|
||||
}, [session?.user?.id])
|
||||
|
||||
if (!session) {
|
||||
return <Redirect href="/(auth)/login" />
|
||||
}
|
||||
|
||||
if (session.user.mustChangePassword) {
|
||||
return <ForcePasswordChangeScreen />
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
@@ -67,6 +274,15 @@ export default function AppLayout() {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="chat/index"
|
||||
options={{
|
||||
title: 'Nachrichten',
|
||||
tabBarIcon: ({ color, focused }) => (
|
||||
<ChatTabIcon color={color} focused={focused} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="profil/index"
|
||||
options={{
|
||||
@@ -82,6 +298,7 @@ export default function AppLayout() {
|
||||
<Tabs.Screen name="members/[id]" options={{ href: null }} />
|
||||
<Tabs.Screen name="termine/[id]" options={{ href: null }} />
|
||||
<Tabs.Screen name="stellen/[id]" options={{ href: null }} />
|
||||
<Tabs.Screen name="chat/[id]" options={{ href: null }} />
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user