fontsavatarr

This commit is contained in:
Leon Morival 2026-04-19 16:09:27 +02:00
parent 156611f587
commit ef7d04fe24
6 changed files with 409 additions and 4 deletions

View File

@ -27,10 +27,6 @@ EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
EXPO_PUBLIC_FIREBASE_APP_ID=
```
4. Dans Firebase Console, active `Authentication > Sign-in method > Email/Password`
5. Crée Firestore dans le même projet Firebase
## Lancer le projet
```bash

View File

@ -18,6 +18,13 @@ export default function TabLayout() {
tabBarIcon: ({ color }) => <MaterialCommunityIcons size={28} name="home-outline" color={color} />,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ color }) => <MaterialCommunityIcons size={28} name="cog-outline" color={color} />,
}}
/>
</Tabs>
);
}

165
app/(tabs)/settings.tsx Normal file
View File

@ -0,0 +1,165 @@
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { Redirect, router } from 'expo-router';
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';
import { Avatar } from '@/components/common/Avatar';
import { Catamaran, FunnelSans } from '@/constants/fonts';
import { Palette } from '@/constants/palette';
import { Radius, Spacing } from '@/constants/styles';
import { useAuth } from '@/providers/AuthProvider';
function SettingsRow({
label,
description,
onPress,
}: {
label: string;
description: string;
onPress: () => void;
}) {
return (
<Pressable onPress={onPress} style={styles.row}>
<View style={styles.rowContent}>
<Text style={styles.rowLabel}>{label}</Text>
<Text style={styles.rowDescription}>{description}</Text>
</View>
<MaterialCommunityIcons
color={Palette.gray}
name="chevron-right"
size={24}
/>
</Pressable>
);
}
export default function SettingsScreen() {
const { initializing, user } = useAuth();
if (initializing) {
return (
<View style={styles.centered}>
<ActivityIndicator color={Palette.black} />
</View>
);
}
if (!user) {
return <Redirect href="/login" />;
}
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Settings</Text>
<Text style={styles.subtitle}>Gere ton compte et ton profil.</Text>
</View>
<View style={styles.previewCard}>
<Avatar
name={user.username ?? user.email}
size="md"
uri={user.profilePicture}
/>
<View style={styles.previewContent}>
<Text style={styles.previewTitle}>{user.username || 'Profil'}</Text>
<Text style={styles.previewSubtitle}>{user.email}</Text>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Account</Text>
<View style={styles.sectionCard}>
<SettingsRow
description="Photo, pseudo, email et dates de creation."
label="Profile"
onPress={() => router.push('/profile')}
/>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
centered: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Palette.cream,
},
container: {
flex: 1,
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xl,
gap: Spacing.lg,
backgroundColor: Palette.cream,
},
header: {
gap: Spacing.xs,
},
title: {
...Catamaran.extraBold34,
color: Palette.black,
},
subtitle: {
...FunnelSans.regular16,
color: Palette.gray,
},
previewCard: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
padding: Spacing.md,
borderWidth: 1,
borderColor: Palette.cream,
borderRadius: Radius.xl,
backgroundColor: Palette.white,
},
previewContent: {
flex: 1,
gap: Spacing.xs,
},
previewTitle: {
...FunnelSans.medium16,
color: Palette.black,
},
previewSubtitle: {
...FunnelSans.medium14,
color: Palette.gray,
},
section: {
gap: Spacing.sm,
},
sectionTitle: {
...FunnelSans.medium12,
color: Palette.gray,
textTransform: 'uppercase',
},
sectionCard: {
borderWidth: 1,
borderColor: Palette.cream,
borderRadius: Radius.xl,
backgroundColor: Palette.white,
overflow: 'hidden',
},
row: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.md,
},
rowContent: {
flex: 1,
gap: Spacing.xs,
},
rowLabel: {
...FunnelSans.medium16,
color: Palette.black,
},
rowDescription: {
...FunnelSans.regular16,
color: Palette.gray,
},
});

View File

@ -4,6 +4,7 @@ import { useEffect } from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { AuthProvider } from '@/providers/AuthProvider';
import { Palette } from '@/constants/palette';
SplashScreen.preventAutoHideAsync();
@ -34,6 +35,14 @@ export default function RootLayout() {
<Stack>
<Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="register" options={{ headerShown: false }} />
<Stack.Screen
name="profile"
options={{
title: 'Profile',
headerStyle: { backgroundColor: Palette.cream },
headerShadowVisible: false,
}}
/>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
</AuthProvider>

124
app/profile.tsx Normal file
View File

@ -0,0 +1,124 @@
import { Redirect } from 'expo-router';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { Avatar } from '@/components/common/Avatar';
import { Catamaran, FunnelSans } from '@/constants/fonts';
import { Palette } from '@/constants/palette';
import { Radius, Spacing } from '@/constants/styles';
import { useAuth } from '@/providers/AuthProvider';
function formatDate(value?: Date | null) {
if (!value) {
return '-';
}
return value.toLocaleDateString('fr-FR', {
day: '2-digit',
month: 'long',
year: 'numeric',
});
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text>
<Text style={styles.infoValue}>{value}</Text>
</View>
);
}
export default function ProfileScreen() {
const { initializing, user } = useAuth();
if (initializing) {
return <View style={styles.screen} />;
}
if (!user) {
return <Redirect href="/login" />;
}
return (
<ScrollView
contentContainerStyle={styles.content}
style={styles.screen}
>
<View style={styles.header}>
<Avatar
name={user.username ?? user.email}
size="lg"
uri={user.profilePicture}
/>
<View style={styles.headerContent}>
<Text style={styles.title}>{user.username || 'Profil'}</Text>
<Text style={styles.subtitle}>{user.email}</Text>
</View>
</View>
<View style={styles.card}>
<InfoRow label="Username" value={user.username || '-'} />
<InfoRow label="Email" value={user.email || '-'} />
<InfoRow label="UID" value={user.uid} />
<InfoRow
label="Photo"
value={user.profilePicture ? 'Renseignee' : 'Aucune'}
/>
<InfoRow label="Cree le" value={formatDate(user.createdAt)} />
<InfoRow label="Mis a jour le" value={formatDate(user.updatedAt)} />
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: Palette.cream,
},
content: {
paddingHorizontal: Spacing.lg,
paddingTop: Spacing.xl,
paddingBottom: Spacing.xxl,
gap: Spacing.lg,
},
header: {
alignItems: 'center',
gap: Spacing.md,
},
headerContent: {
alignItems: 'center',
gap: Spacing.xs,
},
title: {
...Catamaran.extraBold34,
color: Palette.black,
},
subtitle: {
...FunnelSans.regular16,
color: Palette.gray,
},
card: {
gap: Spacing.md,
padding: Spacing.md,
borderWidth: 1,
borderColor: Palette.cream,
borderRadius: Radius.xl,
backgroundColor: Palette.white,
},
infoRow: {
gap: Spacing.xs,
paddingBottom: Spacing.sm,
borderBottomWidth: 1,
borderBottomColor: Palette.cream,
},
infoLabel: {
...FunnelSans.medium12,
color: Palette.gray,
textTransform: 'uppercase',
},
infoValue: {
...FunnelSans.regular16,
color: Palette.black,
},
});

View File

@ -0,0 +1,104 @@
import { Image } from 'expo-image';
import {
StyleSheet,
Text,
View,
type ImageStyle,
type StyleProp,
type ViewStyle,
} from 'react-native';
import { Catamaran, FunnelSans } from '@/constants/fonts';
import { Palette } from '@/constants/palette';
import { Radius } from '@/constants/styles';
type AvatarSize = 'sm' | 'md' | 'lg';
type AvatarProps = {
uri?: string | null;
name?: string | null;
size?: AvatarSize;
style?: StyleProp<ImageStyle>;
};
const avatarSizeMap = {
sm: 40,
md: 64,
lg: 104,
} as const;
function getInitials(name?: string | null) {
if (!name?.trim()) {
return '?';
}
const parts = name
.trim()
.split(/\s+/)
.slice(0, 2);
return parts.map((part) => part[0]?.toUpperCase() ?? '').join('') || '?';
}
export function Avatar({ uri, name, size = 'md', style }: AvatarProps) {
const avatarSize = avatarSizeMap[size];
const initials = getInitials(name);
if (uri) {
return (
<Image
contentFit="cover"
source={{ uri }}
style={[
styles.base,
{
width: avatarSize,
height: avatarSize,
borderRadius: avatarSize / 2,
},
style,
]}
/>
);
}
return (
<View
style={[
styles.base,
styles.fallback,
{
width: avatarSize,
height: avatarSize,
borderRadius: avatarSize / 2,
},
style,
]}
>
<Text style={[size === 'lg' ? styles.largeText : styles.text]}>{initials}</Text>
</View>
);
}
const styles = StyleSheet.create({
base: {
overflow: 'hidden',
},
fallback: {
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Palette.black,
borderWidth: 1,
borderColor: Palette.gray,
},
text: {
...FunnelSans.semiBold14,
color: Palette.white,
},
largeText: {
...Catamaran.bold18,
color: Palette.white,
},
});
export default Avatar;