This commit is contained in:
Bastien COIGNOUX
2026-05-03 20:18:33 +02:00
parent ffc2e6b895
commit bd325fe456
113 changed files with 29532 additions and 220 deletions

View File

@ -0,0 +1,59 @@
import React from 'react';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { Link, Tabs } from 'expo-router';
import { Pressable } from 'react-native';
import Colors from '@/constants/Colors';
import { useColorScheme } from '@/components/useColorScheme';
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
function TabBarIcon(props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) {
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />;
}
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
// Disable the static render of the header on web
// to prevent a hydration error in React Navigation v6.
headerShown: useClientOnlyValue(false, true),
}}>
<Tabs.Screen
name="index"
options={{
title: 'Tab One',
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
headerRight: () => (
<Link href="/modal" asChild>
<Pressable>
{({ pressed }) => (
<FontAwesome
name="info-circle"
size={25}
color={Colors[colorScheme ?? 'light'].text}
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
/>
)}
</Pressable>
</Link>
),
}}
/>
<Tabs.Screen
name="two"
options={{
title: 'Tab Two',
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
}}
/>
</Tabs>
);
}

View File

@ -0,0 +1,31 @@
import { StyleSheet } from 'react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
export default function TabOneScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Tab One</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/(tabs)/index.tsx" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});

31
mb-app/app/(tabs)/two.tsx Normal file
View File

@ -0,0 +1,31 @@
import { StyleSheet } from 'react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
export default function TabTwoScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Tab Two</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/(tabs)/two.tsx" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});

38
mb-app/app/+html.tsx Normal file
View File

@ -0,0 +1,38 @@
import { ScrollViewStyleReset } from 'expo-router/html';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

40
mb-app/app/+not-found.tsx Normal file
View File

@ -0,0 +1,40 @@
import { Link, Stack } from 'expo-router';
import { StyleSheet } from 'react-native';
import { Text, View } from '@/components/Themed';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View style={styles.container}>
<Text style={styles.title}>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
link: {
marginTop: 15,
paddingVertical: 15,
},
linkText: {
fontSize: 14,
color: '#2e78b7',
},
});

88
mb-app/app/_layout.tsx Normal file
View File

@ -0,0 +1,88 @@
import 'react-native-gesture-handler';
import '../global.css';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { QueryClientProvider } from '@tanstack/react-query';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { MD3DarkTheme, MD3LightTheme, PaperProvider } from 'react-native-paper';
import 'react-native-reanimated';
import { useColorScheme } from '@/components/useColorScheme';
import { AuthProvider } from '@/context/AuthContext';
import { queryClient } from '@/lib/query-client';
export { ErrorBoundary } from 'expo-router';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
...FontAwesome.font,
});
useEffect(() => {
if (error) throw error;
}, [error]);
useEffect(() => {
if (loaded) {
void SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
return <RootLayoutNav />;
}
function RootLayoutNav() {
const colorScheme = useColorScheme();
const navigationTheme = colorScheme === 'dark' ? DarkTheme : DefaultTheme;
const paperTheme =
colorScheme === 'dark'
? {
...MD3DarkTheme,
colors: {
...MD3DarkTheme.colors,
primary: '#3B82F6',
primaryContainer: '#1E3A8A',
},
}
: {
...MD3LightTheme,
colors: {
...MD3LightTheme.colors,
primary: '#1D4ED8',
primaryContainer: '#DBEAFE',
},
};
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<QueryClientProvider client={queryClient}>
<PaperProvider theme={paperTheme}>
<AuthProvider>
<ThemeProvider value={navigationTheme}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="(tabs)" />
<Stack.Screen name="auth" />
<Stack.Screen
name="modal"
options={{ presentation: 'modal', headerShown: true, title: 'Info' }}
/>
</Stack>
</ThemeProvider>
</AuthProvider>
</PaperProvider>
</QueryClientProvider>
</GestureHandlerRootView>
);
}

View File

@ -0,0 +1,12 @@
import { Stack } from 'expo-router';
export default function AuthLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
animation: 'fade',
}}
/>
);
}

96
mb-app/app/auth/login.tsx Normal file
View File

@ -0,0 +1,96 @@
import { Link } from 'expo-router';
import { useState } from 'react';
import { KeyboardAvoidingView, Platform, Pressable, ScrollView, Text, View } from 'react-native';
import { Button, TextInput } from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAuth } from '@/context/AuthContext';
import { useUiStore } from '@/stores/ui-store';
export default function LoginScreen() {
const { signIn } = useAuth();
const lastEmail = useUiStore((s) => s.lastAuthEmail);
const [email, setEmail] = useState(lastEmail);
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
return (
<SafeAreaView className="flex-1 bg-slate-50">
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
className="flex-1"
>
<ScrollView
keyboardShouldPersistTaps="handled"
contentContainerClassName="flex-grow justify-center px-6 py-10"
>
<View className="mb-10">
<Text className="text-3xl font-bold text-slate-900">Connexion</Text>
<Text className="mt-2 text-base text-slate-600">
Espace marchand de biens accès sécurisé
</Text>
</View>
{error ? (
<Text className="mb-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700">
{error}
</Text>
) : null}
<TextInput
label="E-mail"
mode="outlined"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
className="mb-3 bg-white"
style={{ backgroundColor: '#fff' }}
/>
<TextInput
label="Mot de passe"
mode="outlined"
value={password}
onChangeText={setPassword}
secureTextEntry
autoComplete="password"
className="mb-6 bg-white"
style={{ backgroundColor: '#fff' }}
/>
<Button
mode="contained"
onPress={async () => {
setError(null);
setLoading(true);
const r = await signIn(email.trim(), password);
setLoading(false);
if (r.error) {
setError(r.error);
return;
}
useUiStore.getState().setLastAuthEmail(email.trim());
}}
loading={loading}
disabled={loading}
buttonColor="#1D4ED8"
textColor="#ffffff"
style={{ borderRadius: 10, paddingVertical: 4 }}
>
Se connecter
</Button>
<View className="mt-8 items-center">
<Text className="text-slate-600">Pas encore de compte ?</Text>
<Link href="/auth/register" asChild>
<Pressable className="mt-2 py-2">
<Text className="font-semibold text-primary">Créer un compte</Text>
</Pressable>
</Link>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}

View File

@ -0,0 +1,127 @@
import { Link } from 'expo-router';
import { useState } from 'react';
import { KeyboardAvoidingView, Platform, Pressable, ScrollView, Text, View } from 'react-native';
import { Button, TextInput } from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAuth } from '@/context/AuthContext';
export default function RegisterScreen() {
const { signUp } = useAuth();
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [info, setInfo] = useState<string | null>(null);
return (
<SafeAreaView className="flex-1 bg-slate-50">
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
className="flex-1"
>
<ScrollView
keyboardShouldPersistTaps="handled"
contentContainerClassName="flex-grow px-6 py-8"
>
<View className="mb-8">
<Text className="text-3xl font-bold text-slate-900">Inscription</Text>
<Text className="mt-2 text-base text-slate-600">
Créez votre accès professionnel
</Text>
</View>
{error ? (
<Text className="mb-3 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700">
{error}
</Text>
) : null}
{info ? (
<Text className="mb-3 rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">
{info}
</Text>
) : null}
<TextInput
label="Prénom"
mode="outlined"
value={firstName}
onChangeText={setFirstName}
autoComplete="given-name"
className="mb-3"
style={{ backgroundColor: '#fff' }}
/>
<TextInput
label="Nom"
mode="outlined"
value={lastName}
onChangeText={setLastName}
autoComplete="family-name"
className="mb-3"
style={{ backgroundColor: '#fff' }}
/>
<TextInput
label="E-mail"
mode="outlined"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
className="mb-3"
style={{ backgroundColor: '#fff' }}
/>
<TextInput
label="Mot de passe"
mode="outlined"
value={password}
onChangeText={setPassword}
secureTextEntry
autoComplete="new-password"
className="mb-6"
style={{ backgroundColor: '#fff' }}
/>
<Button
mode="contained"
onPress={async () => {
setError(null);
setInfo(null);
setLoading(true);
const r = await signUp({
email: email.trim(),
password,
firstName: firstName.trim(),
lastName: lastName.trim(),
});
setLoading(false);
if (r.error) {
setError(r.error);
return;
}
setInfo(
'Si la confirmation e-mail est activée sur votre projet Supabase, vérifiez votre boîte avant de vous connecter.',
);
}}
loading={loading}
disabled={loading}
buttonColor="#1D4ED8"
textColor="#ffffff"
style={{ borderRadius: 10, paddingVertical: 4 }}
>
S&apos;inscrire
</Button>
<View className="mt-8 items-center">
<Link href="/auth/login" asChild>
<Pressable className="py-2">
<Text className="font-semibold text-primary">Retour à la connexion</Text>
</Pressable>
</Link>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}

21
mb-app/app/index.tsx Normal file
View File

@ -0,0 +1,21 @@
import { Redirect } from 'expo-router';
import { ActivityIndicator, View } from 'react-native';
import { useAuth } from '@/context/AuthContext';
export default function Index() {
const { initialized, session } = useAuth();
if (!initialized) {
return (
<View className="flex-1 items-center justify-center bg-slate-50">
<ActivityIndicator size="large" color="#1D4ED8" />
</View>
);
}
if (session?.user) {
return <Redirect href="/(tabs)" />;
}
return <Redirect href="/auth/login" />;
}

35
mb-app/app/modal.tsx Normal file
View File

@ -0,0 +1,35 @@
import { StatusBar } from 'expo-status-bar';
import { Platform, StyleSheet } from 'react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
export default function ModalScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Modal</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/modal.tsx" />
{/* Use a light status bar on iOS to account for the black space above the modal */}
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});