init
This commit is contained in:
41
mb-app/.gitignore
vendored
Normal file
41
mb-app/.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
.kotlin/
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# generated native folders
|
||||
/ios
|
||||
/android
|
||||
1
mb-app/.vscode/extensions.json
vendored
Normal file
1
mb-app/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{ "recommendations": ["expo.vscode-expo-tools"] }
|
||||
7
mb-app/.vscode/settings.json
vendored
Normal file
7
mb-app/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit",
|
||||
"source.sortMembers": "explicit"
|
||||
}
|
||||
}
|
||||
45
mb-app/app.json
Normal file
45
mb-app/app.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "mb-app",
|
||||
"slug": "mb-app",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "mbapp",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"predictiveBackGestureEnabled": false
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-notifications",
|
||||
{
|
||||
"color": "#1D4ED8"
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
}
|
||||
}
|
||||
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%',
|
||||
},
|
||||
});
|
||||
BIN
mb-app/assets/fonts/SpaceMono-Regular.ttf
Normal file
BIN
mb-app/assets/fonts/SpaceMono-Regular.ttf
Normal file
Binary file not shown.
BIN
mb-app/assets/images/adaptive-icon.png
Normal file
BIN
mb-app/assets/images/adaptive-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
mb-app/assets/images/favicon.png
Normal file
BIN
mb-app/assets/images/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
mb-app/assets/images/icon.png
Normal file
BIN
mb-app/assets/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
mb-app/assets/images/splash-icon.png
Normal file
BIN
mb-app/assets/images/splash-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
9
mb-app/babel.config.js
Normal file
9
mb-app/babel.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [
|
||||
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
|
||||
'nativewind/babel',
|
||||
],
|
||||
};
|
||||
};
|
||||
77
mb-app/components/EditScreenInfo.tsx
Normal file
77
mb-app/components/EditScreenInfo.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ExternalLink } from './ExternalLink';
|
||||
import { MonoText } from './StyledText';
|
||||
import { Text, View } from './Themed';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
|
||||
export default function EditScreenInfo({ path }: { path: string }) {
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.getStartedContainer}>
|
||||
<Text
|
||||
style={styles.getStartedText}
|
||||
lightColor="rgba(0,0,0,0.8)"
|
||||
darkColor="rgba(255,255,255,0.8)">
|
||||
Open up the code for this screen:
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[styles.codeHighlightContainer, styles.homeScreenFilename]}
|
||||
darkColor="rgba(255,255,255,0.05)"
|
||||
lightColor="rgba(0,0,0,0.05)">
|
||||
<MonoText>{path}</MonoText>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={styles.getStartedText}
|
||||
lightColor="rgba(0,0,0,0.8)"
|
||||
darkColor="rgba(255,255,255,0.8)">
|
||||
Change any of the text, save the file, and your app will automatically update.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.helpContainer}>
|
||||
<ExternalLink
|
||||
style={styles.helpLink}
|
||||
href="https://docs.expo.io/get-started/create-a-new-app/#opening-the-app-on-your-phonetablet">
|
||||
<Text style={styles.helpLinkText} lightColor={Colors.light.tint}>
|
||||
Tap here if your app doesn't automatically update after making changes
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
getStartedContainer: {
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 50,
|
||||
},
|
||||
homeScreenFilename: {
|
||||
marginVertical: 7,
|
||||
},
|
||||
codeHighlightContainer: {
|
||||
borderRadius: 3,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
getStartedText: {
|
||||
fontSize: 17,
|
||||
lineHeight: 24,
|
||||
textAlign: 'center',
|
||||
},
|
||||
helpContainer: {
|
||||
marginTop: 15,
|
||||
marginHorizontal: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
helpLink: {
|
||||
paddingVertical: 15,
|
||||
},
|
||||
helpLinkText: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
24
mb-app/components/ExternalLink.tsx
Normal file
24
mb-app/components/ExternalLink.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { Link } from 'expo-router';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import React from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export function ExternalLink(
|
||||
props: Omit<React.ComponentProps<typeof Link>, 'href'> & { href: string }
|
||||
) {
|
||||
return (
|
||||
<Link
|
||||
target="_blank"
|
||||
{...props}
|
||||
href={props.href}
|
||||
onPress={(e) => {
|
||||
if (Platform.OS !== 'web') {
|
||||
// Prevent the default behavior of linking to the default browser on native.
|
||||
e.preventDefault();
|
||||
// Open the link in an in-app browser.
|
||||
WebBrowser.openBrowserAsync(props.href as string);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
mb-app/components/StyledText.tsx
Normal file
5
mb-app/components/StyledText.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { Text, TextProps } from './Themed';
|
||||
|
||||
export function MonoText(props: TextProps) {
|
||||
return <Text {...props} style={[props.style, { fontFamily: 'SpaceMono' }]} />;
|
||||
}
|
||||
45
mb-app/components/Themed.tsx
Normal file
45
mb-app/components/Themed.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Learn more about Light and Dark modes:
|
||||
* https://docs.expo.io/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Text as DefaultText, View as DefaultView } from 'react-native';
|
||||
|
||||
import Colors from '@/constants/Colors';
|
||||
import { useColorScheme } from './useColorScheme';
|
||||
|
||||
type ThemeProps = {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
};
|
||||
|
||||
export type TextProps = ThemeProps & DefaultText['props'];
|
||||
export type ViewProps = ThemeProps & DefaultView['props'];
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
return colorFromProps;
|
||||
} else {
|
||||
return Colors[theme][colorName];
|
||||
}
|
||||
}
|
||||
|
||||
export function Text(props: TextProps) {
|
||||
const { style, lightColor, darkColor, ...otherProps } = props;
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
|
||||
return <DefaultText style={[{ color }, style]} {...otherProps} />;
|
||||
}
|
||||
|
||||
export function View(props: ViewProps) {
|
||||
const { style, lightColor, darkColor, ...otherProps } = props;
|
||||
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
||||
|
||||
return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />;
|
||||
}
|
||||
10
mb-app/components/__tests__/StyledText-test.js
Normal file
10
mb-app/components/__tests__/StyledText-test.js
Normal file
@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { MonoText } from '../StyledText';
|
||||
|
||||
it(`renders correctly`, () => {
|
||||
const tree = renderer.create(<MonoText>Snapshot test!</MonoText>).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
4
mb-app/components/useClientOnlyValue.ts
Normal file
4
mb-app/components/useClientOnlyValue.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// This function is web-only as native doesn't currently support server (or build-time) rendering.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
|
||||
return client;
|
||||
}
|
||||
12
mb-app/components/useClientOnlyValue.web.ts
Normal file
12
mb-app/components/useClientOnlyValue.web.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
// `useEffect` is not invoked during server rendering, meaning
|
||||
// we can use this to determine if we're on the server or not.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C {
|
||||
const [value, setValue] = React.useState<S | C>(server);
|
||||
React.useEffect(() => {
|
||||
setValue(client);
|
||||
}, [client]);
|
||||
|
||||
return value;
|
||||
}
|
||||
1
mb-app/components/useColorScheme.ts
Normal file
1
mb-app/components/useColorScheme.ts
Normal file
@ -0,0 +1 @@
|
||||
export { useColorScheme } from 'react-native';
|
||||
8
mb-app/components/useColorScheme.web.ts
Normal file
8
mb-app/components/useColorScheme.web.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// NOTE: The default React Native styling doesn't support server rendering.
|
||||
// Server rendered styles should not change between the first render of the HTML
|
||||
// and the first render on the client. Typically, web developers will use CSS media queries
|
||||
// to render different styles on the client and server, these aren't directly supported in React Native
|
||||
// but can be achieved using a styling library like Nativewind.
|
||||
export function useColorScheme() {
|
||||
return 'light';
|
||||
}
|
||||
19
mb-app/constants/Colors.ts
Normal file
19
mb-app/constants/Colors.ts
Normal file
@ -0,0 +1,19 @@
|
||||
const tintColorLight = '#2f95dc';
|
||||
const tintColorDark = '#fff';
|
||||
|
||||
export default {
|
||||
light: {
|
||||
text: '#000',
|
||||
background: '#fff',
|
||||
tint: tintColorLight,
|
||||
tabIconDefault: '#ccc',
|
||||
tabIconSelected: tintColorLight,
|
||||
},
|
||||
dark: {
|
||||
text: '#fff',
|
||||
background: '#000',
|
||||
tint: tintColorDark,
|
||||
tabIconDefault: '#ccc',
|
||||
tabIconSelected: tintColorDark,
|
||||
},
|
||||
};
|
||||
4
mb-app/constants/metier.ts
Normal file
4
mb-app/constants/metier.ts
Normal file
@ -0,0 +1,4 @@
|
||||
/** Constantes métier marchand de biens (France) — à enrichir avec le métier. */
|
||||
|
||||
export const FRAIS_NOTAIRE_ANCIEN_RATIO = 0.075;
|
||||
export const FRAIS_NOTAIRE_NEUF_RATIO = 0.02;
|
||||
121
mb-app/context/AuthContext.tsx
Normal file
121
mb-app/context/AuthContext.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import type { Session, User } from '@supabase/supabase-js';
|
||||
import { useRouter, useSegments } from 'expo-router';
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { supabase } from '@/services/supabase';
|
||||
|
||||
type AuthContextValue = {
|
||||
initialized: boolean;
|
||||
session: Session | null;
|
||||
user: User | null;
|
||||
signIn: (email: string, password: string) => Promise<{ error?: string }>;
|
||||
signUp: (params: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}) => Promise<{ error?: string }>;
|
||||
signOut: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void supabase.auth.getSession().then(({ data }) => {
|
||||
if (!cancelled) {
|
||||
setSession(data.session ?? null);
|
||||
setInitialized(true);
|
||||
}
|
||||
});
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, nextSession) => {
|
||||
setSession(nextSession);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) return;
|
||||
const root = segments[0];
|
||||
if (!session?.user && root === '(tabs)') {
|
||||
router.replace('/auth/login');
|
||||
return;
|
||||
}
|
||||
if (session?.user && root === 'auth') {
|
||||
router.replace('/(tabs)');
|
||||
}
|
||||
}, [initialized, session?.user, segments, router]);
|
||||
|
||||
const signIn = useCallback(async (email: string, password: string) => {
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
if (error) return { error: error.message };
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const signUp = useCallback(
|
||||
async (params: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}) => {
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email: params.email,
|
||||
password: params.password,
|
||||
options: {
|
||||
data: {
|
||||
first_name: params.firstName,
|
||||
last_name: params.lastName,
|
||||
full_name: `${params.firstName} ${params.lastName}`.trim(),
|
||||
},
|
||||
},
|
||||
});
|
||||
if (error) return { error: error.message };
|
||||
return {};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
await supabase.auth.signOut();
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthContextValue>(
|
||||
() => ({
|
||||
initialized,
|
||||
session,
|
||||
user: session?.user ?? null,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
}),
|
||||
[initialized, session, signIn, signUp, signOut],
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAuth doit être utilisé dans un AuthProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
6
mb-app/env.d.ts
vendored
Normal file
6
mb-app/env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
EXPO_PUBLIC_SUPABASE_URL?: string;
|
||||
EXPO_PUBLIC_SUPABASE_ANON_KEY?: string;
|
||||
}
|
||||
}
|
||||
3
mb-app/global.css
Normal file
3
mb-app/global.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
10
mb-app/lib/query-client.ts
Normal file
10
mb-app/lib/query-client.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
staleTime: 30_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
6
mb-app/metro.config.js
Normal file
6
mb-app/metro.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
const { getDefaultConfig } = require('expo/metro-config');
|
||||
const { withNativeWind } = require('nativewind/metro');
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
module.exports = withNativeWind(config, { input: './global.css' });
|
||||
1
mb-app/nativewind-env.d.ts
vendored
Normal file
1
mb-app/nativewind-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="nativewind/types" />
|
||||
11091
mb-app/package-lock.json
generated
Normal file
11091
mb-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
mb-app/package.json
Normal file
52
mb-app/package.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "mb-app",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"@supabase/supabase-js": "^2.105.1",
|
||||
"@tanstack/react-query": "^5.100.9",
|
||||
"expo": "~54.0.33",
|
||||
"expo-constants": "~18.0.13",
|
||||
"expo-document-picker": "~14.0.8",
|
||||
"expo-file-system": "~19.0.22",
|
||||
"expo-font": "~14.0.11",
|
||||
"expo-image-picker": "~17.0.11",
|
||||
"expo-linking": "~8.0.11",
|
||||
"expo-notifications": "~0.32.17",
|
||||
"expo-router": "~6.0.23",
|
||||
"expo-splash-screen": "~31.0.13",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"expo-web-browser": "~15.0.10",
|
||||
"nativewind": "^4.2.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-maps": "1.20.1",
|
||||
"react-native-paper": "^5.15.1",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.8.0",
|
||||
"react-test-renderer": "19.1.0",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
22
mb-app/services/supabase.ts
Normal file
22
mb-app/services/supabase.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import 'react-native-url-polyfill/auto';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
||||
import Constants from 'expo-constants';
|
||||
|
||||
const supabaseUrl =
|
||||
process.env.EXPO_PUBLIC_SUPABASE_URL ??
|
||||
Constants.expoConfig?.extra?.supabaseUrl ??
|
||||
'';
|
||||
const supabaseAnonKey =
|
||||
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ??
|
||||
Constants.expoConfig?.extra?.supabaseAnonKey ??
|
||||
'';
|
||||
|
||||
export const supabase: SupabaseClient = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
storage: AsyncStorage,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
11
mb-app/stores/ui-store.ts
Normal file
11
mb-app/stores/ui-store.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type UiState = {
|
||||
lastAuthEmail: string;
|
||||
setLastAuthEmail: (email: string) => void;
|
||||
};
|
||||
|
||||
export const useUiStore = create<UiState>((set) => ({
|
||||
lastAuthEmail: '',
|
||||
setLastAuthEmail: (email) => set({ lastAuthEmail: email }),
|
||||
}));
|
||||
20
mb-app/tailwind.config.js
Normal file
20
mb-app/tailwind.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./app/**/*.{js,jsx,ts,tsx}',
|
||||
'./components/**/*.{js,jsx,ts,tsx}',
|
||||
'./context/**/*.{js,jsx,ts,tsx}',
|
||||
'./services/**/*.{js,jsx,ts,tsx}',
|
||||
'./stores/**/*.{js,jsx,ts,tsx}',
|
||||
'./lib/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
presets: [require('nativewind/preset')],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#1D4ED8',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
19
mb-app/tsconfig.json
Normal file
19
mb-app/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts",
|
||||
"nativewind-env.d.ts",
|
||||
"env.d.ts"
|
||||
]
|
||||
}
|
||||
11
mb-app/types/database.ts
Normal file
11
mb-app/types/database.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Types générés Supabase — remplacer par `supabase gen types typescript`
|
||||
* une fois le schéma défini (cf. .cursorrules).
|
||||
*/
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[];
|
||||
Reference in New Issue
Block a user