init
This commit is contained in:
59
mb-app/app/(tabs)/_layout.tsx
Normal file
59
mb-app/app/(tabs)/_layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
mb-app/app/(tabs)/index.tsx
Normal file
31
mb-app/app/(tabs)/index.tsx
Normal 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
31
mb-app/app/(tabs)/two.tsx
Normal 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
38
mb-app/app/+html.tsx
Normal 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
40
mb-app/app/+not-found.tsx
Normal 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
88
mb-app/app/_layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
mb-app/app/auth/_layout.tsx
Normal file
12
mb-app/app/auth/_layout.tsx
Normal 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
96
mb-app/app/auth/login.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
127
mb-app/app/auth/register.tsx
Normal file
127
mb-app/app/auth/register.tsx
Normal 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'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
21
mb-app/app/index.tsx
Normal 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
35
mb-app/app/modal.tsx
Normal 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%',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user