Big improvements on the app

This commit is contained in:
Arthur Belleville 2025-07-17 23:10:55 +02:00
parent af75776dae
commit 5edc860019
No known key found for this signature in database
19 changed files with 1884 additions and 140 deletions

View file

@ -17,7 +17,7 @@ export default function Auth() {
return (
<View style={styles.container}>
<Image source={require("@/assets/images/logo.jpg")} style={styles.logo} />
<Image source={require("@/assets/images/logo.png")} style={styles.logo} />
<Text style={styles.title}>Connexion XTablo</Text>
<Text style={styles.subtitle}>Connectez-vous à votre compte</Text>
<View style={[styles.verticallySpaced, styles.mt10]}>

View file

@ -17,7 +17,7 @@ export default function SignUp() {
return (
<View style={styles.container}>
<Image source={require("@/assets/images/logo.jpg")} style={styles.logo} />
<Image source={require("@/assets/images/logo.png")} style={styles.logo} />
<Text style={styles.title}>Créer un compte XTablo</Text>
<Text style={styles.subtitle}>Rejoignez-nous !</Text>
<View style={[styles.verticallySpaced, styles.mt10]}>

View file

@ -5,7 +5,7 @@ import { HapticTab } from "@/components/HapticTab";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { Colors } from "@/constants/Colors";
import { useColorScheme } from "@/hooks/useColorScheme";
import { Home, User } from "lucide-react-native";
import { MessageCircle, Calendar, User } from "lucide-react-native";
export default function TabLayout() {
const colorScheme = useColorScheme();
@ -13,7 +13,7 @@ export default function TabLayout() {
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
headerShown: true,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
}}
@ -21,8 +21,19 @@ export default function TabLayout() {
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ size, color }) => <Home size={size} color={color} />,
title: "Discussions",
tabBarIcon: ({ size, color }) => (
<MessageCircle size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="planning"
options={{
title: "Planning",
tabBarIcon: ({ size, color }) => (
<Calendar size={size} color={color} />
),
}}
/>
<Tabs.Screen

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,25 @@
import { View, StyleSheet } from "react-native";
import {
View,
StyleSheet,
ScrollView,
Text,
TouchableOpacity,
} from "react-native";
import { useAuth } from "@/stores/auth";
import { Avatar, Button, Input } from "@rn-vui/themed";
import { Card, ListItem } from "@rn-vui/themed";
import { Avatar, Input } from "@rn-vui/themed";
import { Card } from "@rn-vui/themed";
import { useState } from "react";
import { useUser } from "@/providers/UserProvider";
import { LinearGradient } from "expo-linear-gradient";
import {
User,
Mail,
Edit3,
Check,
LogOut,
Settings,
Shield,
} from "lucide-react-native";
export default function ProfileScreen() {
const signOut = useAuth((state) => state.signOut);
@ -12,145 +28,334 @@ export default function ProfileScreen() {
const [displayName, setDisplayName] = useState(user.name || "");
const [isEditing, setIsEditing] = useState(false);
const handleSaveDisplayName = () => {
// TODO: Implémenter la fonctionnalité de sauvegarde
setIsEditing(false);
};
const menuItems = [
{
icon: Settings,
title: "Paramètres du compte",
subtitle: "Gérez vos préférences de compte",
onPress: () => console.log("Paramètres"),
},
{
icon: Shield,
title: "Confidentialité et sécurité",
subtitle: "Contrôlez vos paramètres de confidentialité",
onPress: () => console.log("Confidentialité"),
},
];
return (
<View style={styles.container}>
<Card containerStyle={styles.card}>
<Avatar
size="xlarge"
rounded
icon={{ name: "user", type: "font-awesome" }}
containerStyle={styles.avatar}
/>
<Card.Title style={styles.cardTitle}>{user.name}</Card.Title>
<Card.Divider />
<ListItem key="email" bottomDivider containerStyle={styles.listItem}>
<ListItem.Content>
<ListItem.Title style={styles.listItemTitle}>Email</ListItem.Title>
<ListItem.Subtitle style={styles.listItemSubtitle}>
{user.email}
</ListItem.Subtitle>
</ListItem.Content>
</ListItem>
<ListItem
key="display-name"
bottomDivider
containerStyle={styles.listItem}
>
<ListItem.Content>
<ListItem.Title style={styles.listItemTitle}>
Display Name
</ListItem.Title>
{isEditing ? (
<Input
placeholder="Enter display name"
value={displayName}
onChangeText={setDisplayName}
containerStyle={styles.inputContainer}
inputStyle={styles.inputText}
autoFocus
/>
) : (
<ListItem.Subtitle style={styles.listItemSubtitle}>
{user.name || "Not Set"}
</ListItem.Subtitle>
)}
</ListItem.Content>
<Button
icon={{
name: isEditing ? "check" : "edit",
type: "font-awesome",
size: 18,
}}
type="clear"
onPress={() => {
if (isEditing) {
// handleSaveDisplayName();
} else {
setDisplayName(user.name ?? "");
setIsEditing(true);
}
}}
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
<LinearGradient
colors={["#1e3a8a", "#3b82f6", "#60a5fa"]}
style={styles.headerGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<View style={styles.headerContent}>
<Avatar
size={120}
rounded
source={user.avatar_url ? { uri: user.avatar_url } : undefined}
icon={
!user.avatar_url
? { name: "user", type: "font-awesome" }
: undefined
}
containerStyle={styles.avatar}
/>
</ListItem>
<Text style={styles.userName}>{user.name || "Utilisateur"}</Text>
<Text style={styles.userEmail}>{user.email}</Text>
</View>
</LinearGradient>
<Button
title="Sign Out"
onPress={signOut}
buttonStyle={styles.signOutButton}
titleStyle={styles.signOutText}
icon={{
name: "sign-out",
type: "font-awesome",
color: "white",
size: 18,
}}
iconRight
/>
</Card>
</View>
{/* Contenu principal */}
<View style={styles.content}>
{/* Carte d'informations personnelles */}
<Card containerStyle={styles.mainCard}>
<View style={styles.cardHeader}>
<User size={20} color="#3b82f6" />
<Text style={styles.cardHeaderTitle}>
Informations personnelles
</Text>
</View>
<View style={styles.infoItem}>
<View style={styles.infoIconContainer}>
<Mail size={18} color="#6b7280" />
</View>
<View style={styles.infoContent}>
<Text style={styles.infoLabel}>Adresse e-mail</Text>
<Text style={styles.infoValue}>{user.email}</Text>
</View>
</View>
<View style={styles.divider} />
<View style={styles.infoItem}>
<View style={styles.infoIconContainer}>
<User size={18} color="#6b7280" />
</View>
<View style={styles.infoContent}>
<Text style={styles.infoLabel}>Nom d'affichage</Text>
{isEditing ? (
<Input
placeholder="Entrez le nom d'affichage"
value={displayName}
onChangeText={setDisplayName}
containerStyle={styles.inputContainer}
inputStyle={styles.inputText}
autoFocus
/>
) : (
<Text style={styles.infoValue}>
{user.name || "Non défini"}
</Text>
)}
</View>
<TouchableOpacity
style={styles.editButton}
onPress={() => {
if (isEditing) {
handleSaveDisplayName();
} else {
setDisplayName(user.name ?? "");
setIsEditing(true);
}
}}
>
{isEditing ? (
<Check size={18} color="#10b981" />
) : (
<Edit3 size={18} color="#6b7280" />
)}
</TouchableOpacity>
</View>
</Card>
{/* Éléments de menu */}
<Card containerStyle={styles.menuCard}>
<View style={styles.cardHeader}>
<Settings size={20} color="#3b82f6" />
<Text style={styles.cardHeaderTitle}>Préférences</Text>
</View>
{menuItems.map((item, index) => (
<View key={index}>
<TouchableOpacity style={styles.menuItem} onPress={item.onPress}>
<View style={styles.menuIconContainer}>
<item.icon size={20} color="#6b7280" />
</View>
<View style={styles.menuContent}>
<Text style={styles.menuTitle}>{item.title}</Text>
<Text style={styles.menuSubtitle}>{item.subtitle}</Text>
</View>
<Text style={styles.menuArrow}></Text>
</TouchableOpacity>
{index < menuItems.length - 1 && <View style={styles.divider} />}
</View>
))}
</Card>
{/* Bouton de déconnexion */}
<TouchableOpacity style={styles.signOutContainer} onPress={signOut}>
<LinearGradient
colors={["#ef4444", "#dc2626"]}
style={styles.signOutGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<LogOut size={20} color="white" />
<Text style={styles.signOutText}>Se déconnecter</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 15,
backgroundColor: "#f8f9fa",
backgroundColor: "#f8fafc",
},
card: {
borderRadius: 10,
headerGradient: {
paddingTop: 60,
paddingBottom: 40,
paddingHorizontal: 20,
},
headerContent: {
alignItems: "center",
},
avatar: {
marginBottom: 16,
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
borderWidth: 4,
borderColor: "white",
},
userName: {
fontSize: 24,
fontWeight: "bold",
color: "white",
marginBottom: 4,
textAlign: "center",
},
userEmail: {
fontSize: 16,
color: "rgba(255, 255, 255, 0.9)",
textAlign: "center",
},
content: {
padding: 20,
margin: 0,
marginTop: -20,
},
mainCard: {
borderRadius: 16,
padding: 0,
marginBottom: 20,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
shadowRadius: 8,
elevation: 4,
borderWidth: 0,
},
avatar: {
menuCard: {
borderRadius: 16,
padding: 0,
marginBottom: 20,
backgroundColor: "#e0e0e0",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
borderWidth: 0,
},
cardTitle: {
fontSize: 20,
fontWeight: "bold",
marginBottom: 15,
cardHeader: {
flexDirection: "row",
alignItems: "center",
padding: 20,
paddingBottom: 16,
},
listItem: {
paddingVertical: 15,
width: "100%",
},
listItemTitle: {
cardHeaderTitle: {
fontSize: 18,
fontWeight: "600",
marginBottom: 5,
color: "#555",
color: "#1f2937",
marginLeft: 12,
},
listItemSubtitle: {
color: "#333",
infoItem: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 20,
paddingVertical: 16,
},
infoIconContainer: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#f3f4f6",
justifyContent: "center",
alignItems: "center",
marginRight: 12,
},
infoContent: {
flex: 1,
},
infoLabel: {
fontSize: 14,
color: "#6b7280",
marginBottom: 4,
fontWeight: "500",
},
infoValue: {
fontSize: 16,
color: "#1f2937",
fontWeight: "500",
},
editButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: "#f3f4f6",
justifyContent: "center",
alignItems: "center",
},
inputContainer: {
paddingHorizontal: 0,
height: 40,
marginTop: 4,
},
inputText: {
fontSize: 16,
color: "#1f2937",
},
signOutButton: {
marginTop: 30,
backgroundColor: "#dc3545",
paddingVertical: 12,
divider: {
height: 1,
backgroundColor: "#e5e7eb",
marginLeft: 72,
},
menuItem: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 20,
borderRadius: 8,
width: "80%",
paddingVertical: 16,
},
menuIconContainer: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#f3f4f6",
justifyContent: "center",
alignItems: "center",
marginRight: 12,
},
menuContent: {
flex: 1,
},
menuTitle: {
fontSize: 16,
fontWeight: "500",
color: "#1f2937",
marginBottom: 2,
},
menuSubtitle: {
fontSize: 14,
color: "#6b7280",
},
menuArrow: {
fontSize: 20,
color: "#9ca3af",
fontWeight: "300",
},
signOutContainer: {
marginTop: 10,
marginBottom: 40,
},
signOutGradient: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingVertical: 16,
paddingHorizontal: 24,
borderRadius: 12,
shadowColor: "#ef4444",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 6,
},
signOutText: {
color: "#fff",
color: "white",
fontSize: 16,
fontWeight: "600",
marginLeft: 10,
marginLeft: 8,
},
});

View file

@ -4,7 +4,7 @@ import {
ThemeProvider,
} from "@react-navigation/native";
import { useFonts } from "expo-font";
import { router, Stack } from "expo-router";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
@ -23,9 +23,10 @@ SplashScreen.preventAutoHideAsync();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
refetchOnWindowFocus: false,
// 3 total attempts (1 initial + 2 retries)
retry: 2,
// 0s -> 1s, 1s → 5s. Little resiliency 😁
retryDelay: (attemptIndex) => Math.min(1000 * 5 ** attemptIndex, 10000),
},
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,59 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/lib/supabase";
import { EventAndTablo, EventInsert } from "@/types/events.types";
import { useUser } from "@/providers/UserProvider";
export const useEventsByTablo = (tabloId: string | null) => {
return useQuery({
queryKey: ["events", tabloId],
queryFn: async () => {
if (!tabloId) {
const { data, error } = await supabase
.from("events_and_tablos")
.select("*")
.order("start_date", { ascending: true })
.order("start_time", { ascending: true });
if (error) throw error;
return data as EventAndTablo[];
}
const { data, error } = await supabase
.from("events_and_tablos")
.select("*")
.eq("tablo_id", tabloId)
.order("start_date", { ascending: true })
.order("start_time", { ascending: true });
if (error) throw error;
return data as EventAndTablo[];
},
});
};
export const useCreateEvent = () => {
const user = useUser();
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: async (event: Omit<EventInsert, "created_by">) => {
const { data, error } = await supabase
.from("events")
.insert({
...event,
created_by: user.id,
})
.select()
.single();
if (error) throw error;
return data as Event;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["events"] });
},
onError: (err) => {
console.error(err);
},
});
return mutate;
};

View file

@ -0,0 +1,50 @@
import { supabase } from "@/lib/supabase";
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";
// type TabloInsert = Tablo["Insert"];
// type TabloUpdate = Tablo["Update"];
// Fetch all tablos
export const useTablosList = () => {
const user = useUser();
return useQuery({
queryKey: ["tablos"],
queryFn: async () => {
const { data, error } = await supabase
.from("user_tablos")
.select("*")
.eq("user_id", user.id);
if (error) throw error;
const tablos = data as UserTablo[];
return tablos;
},
});
};
export const useCreateTablo = () => {
const session = useAuth((state) => state.session);
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (
tablo: Pick<TabloInsert, "name" | "color" | "image" | "status">
) => {
const { data } = await api.post("/api/v1/tablos/create", tablo, {
headers: {
Authorization: `Bearer ${session?.access_token}`,
},
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["tablos"] });
},
onError: (error) => {
console.error(error);
},
});
};

View file

@ -1,31 +1,29 @@
import { api } from "@/lib/api";
import { Tables } from "@/lib/database.types";
import { Tables } from "@/types/database.types";
import { useAuth } from "@/stores/auth";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
type User = Tables<"profiles"> & {
streamToken: string | null;
};
export const useGetUser = (): { user: User | null; isLoading: boolean } => {
const { session } = useAuth();
const queryClient = useQueryClient();
console.log("session in useGetUser", session);
const { data, isLoading } = useQuery<User | null>(
{
queryKey: ["user"],
queryFn: async () => {
console.log("queryFn");
const session = useAuth((state) => state.session);
const { data, isLoading } = useQuery<User | null>({
queryKey: ["user"],
queryFn: async () => {
try {
const { data: user } = await api.get<User>("/api/v1/users/me", {
headers: {
Authorization: `Bearer ${session?.access_token}`,
},
});
return user;
},
} catch (error) {
return null;
}
},
queryClient
);
});
return { user: data ?? null, isLoading };
};

View file

@ -27,6 +27,7 @@
"expo-haptics": "~14.1.4",
"expo-image-manipulator": "~13.1.7",
"expo-image-picker": "~16.1.4",
"expo-linear-gradient": "~14.1.5",
"expo-linking": "~7.1.4",
"expo-router": "~5.1.3",
"expo-secure-store": "~14.2.3",
@ -5597,6 +5598,16 @@
"react": "*"
}
},
"node_modules/expo-linear-gradient": {
"version": "14.1.5",
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz",
"integrity": "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-linking": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.1.7.tgz",

View file

@ -57,7 +57,8 @@
"stream-chat-expo": "^6.7.3",
"zustand": "^5.0.4",
"expo-crypto": "~14.1.5",
"expo-auth-session": "~6.2.1"
"expo-auth-session": "~6.2.1",
"expo-linear-gradient": "~14.1.5"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View file

@ -1,6 +1,6 @@
import { createStore, StoreApi, useStore } from "zustand";
import React from "react";
import { Tables } from "@/lib/database.types";
import { Tables } from "@/types/database.types";
import { ActivityIndicator } from "react-native";
import { Redirect } from "expo-router";
import { useGetUser } from "@/hooks/user";
@ -22,8 +22,6 @@ export const UserStoreProvider = ({
return <ActivityIndicator />;
}
console.log("user in UserStoreProvider", user);
if (!user) {
return <Redirect href="/(auth)/login" />;
}

View file

@ -38,15 +38,12 @@ export const useAuth = create<AuthState>((set, get) => ({
data: { session },
} = await supabase.auth.getSession();
console.log("session", session);
set({
session,
});
supabase.auth.onAuthStateChange(async (event, session) => {
console.log("event", event);
queryClient.invalidateQueries();
queryClient.invalidateQueries({ queryKey: ["user"] });
set({
session,
});

View file

@ -0,0 +1,22 @@
import { Tables, TablesInsert, TablesUpdate } from "@/types/database.types";
import { RemoveNullFromObject } from "@/types/removeNull";
export type Event = RemoveNullFromObject<
Tables<"events">,
"created_at" | "end_time"
>;
export type EventInsert = TablesInsert<"events">;
export type EventUpdate = TablesUpdate<"events">;
export type EventAndTablo = RemoveNullFromObject<
Tables<"events_and_tablos">,
| "event_id"
| "tablo_id"
| "tablo_name"
| "tablo_color"
| "tablo_status"
| "start_time"
| "end_time"
| "title"
| "start_date"
>;

View file

@ -0,0 +1,11 @@
/**
* Utility type to remove null from a type
*/
export type RemoveNull<T> = T extends null ? never : T;
/**
* Utility type to remove null from all properties of an object type
*/
export type RemoveNullFromObject<T, K extends keyof T = keyof T> = {
[L in keyof T]: L extends K ? RemoveNull<T[L]> : T[L];
};

View file

@ -0,0 +1,20 @@
import { Database } from "@/types/database.types";
import { RemoveNullFromObject } from "@/types/removeNull";
export type UserTablo = RemoveNullFromObject<
Database["public"]["Views"]["user_tablos"]["Row"],
| "id"
| "access_level"
| "is_admin"
| "created_at"
| "deleted_at"
| "position"
| "user_id"
| "name"
| "status"
>;
export type Tablo = Database["public"]["Tables"]["tablos"];
export type TabloInsert = Tablo["Insert"];
export type TabloUpdate = Tablo["Update"];