fontsavatarr
This commit is contained in:
parent
156611f587
commit
ef7d04fe24
|
|
@ -27,10 +27,6 @@ EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
|
||||||
EXPO_PUBLIC_FIREBASE_APP_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
|
## Lancer le projet
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ export default function TabLayout() {
|
||||||
tabBarIcon: ({ color }) => <MaterialCommunityIcons size={28} name="home-outline" color={color} />,
|
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>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,7 @@ import { useEffect } from 'react';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import { AuthProvider } from '@/providers/AuthProvider';
|
import { AuthProvider } from '@/providers/AuthProvider';
|
||||||
|
import { Palette } from '@/constants/palette';
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
|
|
@ -34,6 +35,14 @@ export default function RootLayout() {
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="register" 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.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue