Rework routing
This commit is contained in:
parent
73cbe6356f
commit
8992f58512
21 changed files with 191 additions and 68 deletions
|
|
@ -11,7 +11,7 @@ import {
|
|||
Linking,
|
||||
} from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import {
|
||||
User,
|
||||
|
|
@ -31,7 +31,7 @@ import {
|
|||
import { router } from "expo-router";
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const signOut = useAuth((state) => state.signOut);
|
||||
const signOut = useAuthStore((state) => state.signOut);
|
||||
const user = useUser();
|
||||
|
||||
// Settings state
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
Text,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { Avatar, Input } from "@rn-vui/themed";
|
||||
import { Card } from "@rn-vui/themed";
|
||||
import { useState } from "react";
|
||||
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from "lucide-react-native";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const signOut = useAuth((state) => state.signOut);
|
||||
const signOut = useAuthStore((state) => state.signOut);
|
||||
const user = useUser();
|
||||
|
||||
const [displayName, setDisplayName] = useState(user.name || "");
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { Redirect, Slot } from "expo-router";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { useGetUser } from "@/hooks/user";
|
||||
import { useEffect } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export default function AuthLayout() {
|
||||
const { loading, initialize } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { user, isLoading: isUserLoading } = useGetUser();
|
||||
|
||||
const isLoading = loading || isUserLoading;
|
||||
|
||||
useEffect(() => {
|
||||
initialize(queryClient);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
if (user) {
|
||||
return <Redirect href="/(home)/(tabs)" />;
|
||||
}
|
||||
return <Slot />;
|
||||
}
|
||||
|
|
@ -3,17 +3,20 @@ import {
|
|||
DefaultTheme,
|
||||
ThemeProvider,
|
||||
} from "@react-navigation/native";
|
||||
import { useFonts } from "expo-font";
|
||||
import { Stack } from "expo-router";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { useEffect } from "react";
|
||||
import "react-native-reanimated";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { ActivityIndicator, View, StyleSheet, Image } from "react-native";
|
||||
import { SplashScreenController } from "@/components/Splash";
|
||||
import { useInitializeApp } from "@/hooks/auth";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { LoadingView } from "@/components/LoadingView";
|
||||
|
||||
window.structuredClone = cloneDeep;
|
||||
|
||||
|
|
@ -33,19 +36,6 @@ const queryClient = new QueryClient({
|
|||
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const [loaded] = useFonts({
|
||||
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [loaded]);
|
||||
|
||||
if (!loaded) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
|
|
@ -53,14 +43,36 @@ export default function RootLayout() {
|
|||
<ThemeProvider
|
||||
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
|
||||
>
|
||||
<Stack>
|
||||
<Stack.Screen name="(home)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<SplashScreenController />
|
||||
<RootNavigator />
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
const RootNavigator = () => {
|
||||
const { isLoading, isLoggedIn } = useInitializeApp();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
console.log("isLoggedIn", isLoggedIn);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Protected guard={isLoggedIn}>
|
||||
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
||||
</Stack.Protected>
|
||||
|
||||
<Stack.Protected guard={!isLoggedIn}>
|
||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="signup" options={{ headerShown: false }} />
|
||||
</Stack.Protected>
|
||||
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import { Button, Input } from "@rn-vui/themed";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { Link } from "expo-router";
|
||||
import { Mail, Lock } from "lucide-react-native";
|
||||
import { GoogleLoginButton } from "@/components/GoogleLoginButton";
|
||||
|
|
@ -11,9 +11,9 @@ export default function Auth() {
|
|||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const login = useAuth((state) => state.login);
|
||||
const authLoading = useAuth((state) => state.loading);
|
||||
const performOAuth = useAuth((state) => state.performOAuth);
|
||||
const login = useAuthStore((state) => state.login);
|
||||
const authLoading = useAuthStore((state) => state.loading);
|
||||
const performOAuth = useAuthStore((state) => state.performOAuth);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import { Button, Input } from "@rn-vui/themed";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { Link } from "expo-router";
|
||||
import { Mail, Lock, User, Building2 } from "lucide-react-native";
|
||||
|
||||
|
|
@ -12,8 +12,8 @@ export default function SignUp() {
|
|||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const signUp = useAuth((state) => state.signUp);
|
||||
const authLoading = useAuth((state) => state.loading);
|
||||
const signUp = useAuthStore((state) => state.signUp);
|
||||
const authLoading = useAuthStore((state) => state.loading);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { Svg, Path } from "react-native-svg";
|
||||
|
||||
const AppleIcon = ({ color = "#fff", size = 20 }) => (
|
||||
|
|
@ -17,7 +17,7 @@ const AppleIcon = ({ color = "#fff", size = 20 }) => (
|
|||
);
|
||||
|
||||
export const AppleLoginButton = ({ onPress }: { onPress: () => void }) => {
|
||||
const authLoading = useAuth((state) => state.loading);
|
||||
const authLoading = useAuthStore((state) => state.loading);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import { StyleSheet, View, Text, TouchableOpacity, Image } from "react-native";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
export const GoogleLoginButton = ({ onPress }: { onPress: () => void }) => {
|
||||
const authLoading = useAuth((state) => state.loading);
|
||||
const authLoading = useAuthStore((state) => state.loading);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
98
xtablo-expo/components/LoadingView.tsx
Normal file
98
xtablo-expo/components/LoadingView.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { StyleSheet, View } from "react-native";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withRepeat,
|
||||
withTiming,
|
||||
useDerivedValue,
|
||||
Easing,
|
||||
} from "react-native-reanimated";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
|
||||
export const LoadingView = () => {
|
||||
const rotation = useSharedValue(0);
|
||||
const width = useSharedValue(80);
|
||||
|
||||
rotation.value = withRepeat(
|
||||
withTiming(360, { easing: Easing.linear, duration: 2000 }),
|
||||
-1,
|
||||
false
|
||||
);
|
||||
width.value = withRepeat(
|
||||
withTiming(100, { easing: Easing.linear, duration: 2000 }),
|
||||
-1,
|
||||
true
|
||||
);
|
||||
|
||||
const rotationDeg = useDerivedValue(() => {
|
||||
return `${rotation.value}deg`;
|
||||
});
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [{ rotate: rotationDeg.value }],
|
||||
width: width.value,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.loadingContainer}>
|
||||
<View style={styles.loadingContent}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Animated.Image
|
||||
source={require("@/assets/images/logo.png")}
|
||||
style={[styles.logo, animatedStyle]}
|
||||
/>
|
||||
</View>
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
XTablo
|
||||
</ThemedText>
|
||||
<ThemedText type="subtitle" style={styles.subtitle}>
|
||||
Initialisation de l'application...
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#f8fafc",
|
||||
},
|
||||
loadingContent: {
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 40,
|
||||
},
|
||||
logoContainer: {
|
||||
alignItems: "center",
|
||||
width: 130,
|
||||
height: 130,
|
||||
},
|
||||
logo: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 8,
|
||||
color: "#1f2937",
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
textAlign: "center",
|
||||
marginBottom: 32,
|
||||
color: "#6b7280",
|
||||
opacity: 0.8,
|
||||
},
|
||||
});
|
||||
17
xtablo-expo/components/Splash.tsx
Normal file
17
xtablo-expo/components/Splash.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { SplashScreen } from "expo-router";
|
||||
import { useInitializeApp } from "@/hooks/auth";
|
||||
import { useFonts } from "expo-font";
|
||||
|
||||
export function SplashScreenController() {
|
||||
const { isLoading } = useInitializeApp();
|
||||
|
||||
const [loaded] = useFonts({
|
||||
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
||||
});
|
||||
|
||||
if (!isLoading && loaded) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
18
xtablo-expo/hooks/auth.ts
Normal file
18
xtablo-expo/hooks/auth.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { useEffect } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useGetUser } from "@/hooks/user";
|
||||
|
||||
export const useInitializeApp = () => {
|
||||
const { loading, initialize } = useAuthStore();
|
||||
const queryClient = useQueryClient();
|
||||
const { user, isLoading: isUserLoading } = useGetUser();
|
||||
|
||||
const isLoading = loading || isUserLoading;
|
||||
|
||||
useEffect(() => {
|
||||
initialize(queryClient);
|
||||
}, []);
|
||||
|
||||
return { isLoading, isLoggedIn: !!user };
|
||||
};
|
||||
|
|
@ -3,7 +3,7 @@ import { useUser } from "@/providers/UserProvider";
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { TabloInsert, UserTablo } from "@/types/tablos.types";
|
||||
import { api } from "@/lib/api";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
// type TabloInsert = Tablo["Insert"];
|
||||
|
|
@ -28,7 +28,7 @@ export const useTablosList = () => {
|
|||
};
|
||||
|
||||
export const useCreateTablo = () => {
|
||||
const session = useAuth((state) => state.session);
|
||||
const session = useAuthStore((state) => state.session);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
|
|
@ -53,7 +53,7 @@ export const useCreateTablo = () => {
|
|||
|
||||
// Delete tablo (soft delete)
|
||||
export const useDeleteTablo = () => {
|
||||
const session = useAuth((state) => state.session);
|
||||
const session = useAuthStore((state) => state.session);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { api } from "@/lib/api";
|
||||
import { Tables } from "@/types/database.types";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
type User = Tables<"profiles"> & {
|
||||
|
|
@ -8,7 +8,7 @@ type User = Tables<"profiles"> & {
|
|||
};
|
||||
|
||||
export const useGetUser = (): { user: User | null; isLoading: boolean } => {
|
||||
const session = useAuth((state) => state.session);
|
||||
const session = useAuthStore((state) => state.session);
|
||||
const { data, isLoading } = useQuery<User | null>({
|
||||
queryKey: ["user"],
|
||||
queryFn: async () => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { QueryClient } from "@tanstack/react-query";
|
|||
interface AuthState {
|
||||
session: Session | null;
|
||||
loading: boolean;
|
||||
initialized: boolean;
|
||||
initialize: (queryClient: QueryClient) => Promise<void>;
|
||||
setSession: (session: Session | null) => void;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
|
|
@ -28,8 +29,9 @@ interface AuthState {
|
|||
WebBrowser.maybeCompleteAuthSession();
|
||||
const redirectTo = makeRedirectUri({ path: "/(home)/(tabs)" });
|
||||
|
||||
export const useAuth = create<AuthState>((set, get) => ({
|
||||
export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
loading: true,
|
||||
initialized: false,
|
||||
session: null,
|
||||
setSession: (session: Session | null) => set({ session }),
|
||||
initialize: async (queryClient: QueryClient) => {
|
||||
|
|
@ -61,6 +63,8 @@ export const useAuth = create<AuthState>((set, get) => ({
|
|||
console.error("Auth initialization error:", error);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
set({ initialized: true });
|
||||
queryClient.invalidateQueries({ queryKey: ["user"] });
|
||||
}
|
||||
},
|
||||
login: async (email: string, password: string) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue