From a847b59c12c8bc15697e70c34d7b4e4acb003f3c Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 19 Jul 2025 18:46:38 +0200 Subject: [PATCH 01/16] Deactivate search for now --- xtablo-expo/app/(home)/(tabs)/index.tsx | 304 ++++++++++++++++++++++-- 1 file changed, 283 insertions(+), 21 deletions(-) diff --git a/xtablo-expo/app/(home)/(tabs)/index.tsx b/xtablo-expo/app/(home)/(tabs)/index.tsx index 23d99b9..a4d7589 100644 --- a/xtablo-expo/app/(home)/(tabs)/index.tsx +++ b/xtablo-expo/app/(home)/(tabs)/index.tsx @@ -1,17 +1,17 @@ import { router } from "expo-router"; import { ChannelList } from "stream-chat-expo"; -import { ChannelSort } from "stream-chat"; +import { ChannelSort, Channel } from "stream-chat"; import { useUser } from "@/providers/UserProvider"; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - StatusBar, -} from "react-native"; +import { View, Text, StyleSheet, StatusBar } from "react-native"; import { LinearGradient } from "expo-linear-gradient"; import { Search } from "lucide-react-native"; -import React from "react"; +import React, { useMemo } from "react"; +import { + useSharedValue, + useAnimatedStyle, + interpolate, + Extrapolate, +} from "react-native-reanimated"; import { useTablosList } from "@/hooks/tablos"; import { ColorMap } from "@/constants/colors"; import { UserTablo } from "@/types/tablos.types"; @@ -21,7 +21,7 @@ const CustomChannelAvatar = ({ channel, tablos, }: { - channel: any; + channel: Channel; tablos: UserTablo[]; }) => { const tabloId = channel?.id || ""; @@ -108,25 +108,170 @@ const CustomChannelAvatar = ({ ); }; +// Custom Title Component for bigger channel names +const CustomChannelTitle = ({ channel }: { channel: Channel }) => { + const channelName = channel?.data?.name || channel?.id || "Channel"; + + return ( + + {channelName} + + ); +}; + export default function HomeScreen() { const user = useUser(); const { data: tablos } = useTablosList(); - const filters = { - members: { $in: [user.id] }, - type: "messaging", - }; + // Search animation state + // const [isSearchVisible, setIsSearchVisible] = useState(false); + // const [searchQuery, setSearchQuery] = useState(""); + // const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(""); + const searchAnimation = useSharedValue(0); + // const searchInputRef = useRef(null); + + // // Debounce search query for better performance + // useEffect(() => { + // const timer = setTimeout(() => { + // setDebouncedSearchQuery(searchQuery); + // }, 300); + + // return () => clearTimeout(timer); + // }, [searchQuery]); + + // Create filters based on search query + const filters = useMemo(() => { + const baseFilters = { + members: { $in: [user.id] }, + type: "messaging", + }; + + // Add name filter when searching + // if (debouncedSearchQuery.trim()) { + // return { + // ...baseFilters, + // name: { $autocomplete: debouncedSearchQuery.trim() }, + // }; + // } + + return baseFilters; + }, [user.id]); + const sort: ChannelSort = { last_updated: -1 }; const options = { state: true, watch: true, + limit: 20, }; // Create a wrapper component for the avatar that has access to tablos data - const AvatarWithTablos = ({ channel }: { channel: any }) => ( + const AvatarWithTablos = ({ channel }: { channel: Channel }) => ( ); + // Toggle search animation + // const toggleSearch = () => { + // const toValue = isSearchVisible ? 0 : 1; + + // searchAnimation.value = withTiming(toValue, { + // duration: 300, + // }); + + // if (toValue === 1) { + // // Focus input when animation starts + // setTimeout(() => searchInputRef.current?.focus(), 150); + // } else { + // // Clear search when hiding + // setSearchQuery(""); + // setDebouncedSearchQuery(""); + // } + + // setIsSearchVisible(!isSearchVisible); + // }; + + // Close search when keyboard is dismissed + // useEffect(() => { + // const keyboardDidHideListener = Keyboard.addListener( + // "keyboardDidHide", + // () => { + // if (isSearchVisible && searchQuery === "") { + // toggleSearch(); + // } + // } + // ); + + // return () => { + // keyboardDidHideListener?.remove(); + // }; + // }, [isSearchVisible, searchQuery]); + + // Animated styles using react-native-reanimated + const animatedSearchStyle = useAnimatedStyle(() => { + const height = interpolate( + searchAnimation.value, + [0, 1], + [0, 80], + Extrapolate.CLAMP + ); + + const opacity = interpolate( + searchAnimation.value, + [0, 0.5, 1], + [0, 0, 1], + Extrapolate.CLAMP + ); + + return { + height, + opacity, + }; + }); + + // Simple search header component - no memoization + // const SearchHeader = () => ( + // + // + // + // { + // console.log("Searching for:", searchQuery); + // Keyboard.dismiss(); + // }} + // /> + // {searchQuery.length > 0 && ( + // { + // setSearchQuery(""); + // setDebouncedSearchQuery(""); + // searchInputRef.current?.focus(); + // }} + // style={styles.clearButton} + // > + // + // + // )} + // + + // {/* Search Results Info */} + // {debouncedSearchQuery.trim() && ( + // + // + // Recherche: "{debouncedSearchQuery}" + // + // + // )} + // + // ); + return ( @@ -143,31 +288,77 @@ export default function HomeScreen() { Discussions + {/* {debouncedSearchQuery.trim() + ? `Recherche: "${debouncedSearchQuery}"` + : "Gérez les conversations de vos tablos"} */} Gérez les conversations de vos tablos - - - + {/* + {isSearchVisible ? ( + + ) : ( + + )} + */} - {/* Decorative Elements + {/* Decorative Elements */} - */} + - {/* Channel List */} + {/* Channel List with animated search */} + {/* */} { + // Close search when selecting a channel + // if (isSearchVisible) { + // toggleSearch(); + // } router.push(`/channel/${channel.cid}`); }} sort={sort} options={options} PreviewAvatar={AvatarWithTablos} + PreviewTitle={CustomChannelTitle} + // ListHeaderComponent={SearchHeader} + // Show loading state during search + LoadingIndicator={() => ( + + + {/* {debouncedSearchQuery + ? "Recherche en cours..." + : "Chargement..."} */} + Chargement... + + + )} + // Show empty state when no results + EmptyStateIndicator={() => ( + + + + {/* {debouncedSearchQuery + ? "Aucun résultat" + : "Aucune conversation"} */} + Aucune conversation + + + {/* {debouncedSearchQuery + ? `Aucune conversation trouvée pour "${debouncedSearchQuery}"` + : "Vous n'avez pas encore de conversations"} */} + Vous n'avez pas encore de conversations + + + )} /> @@ -393,4 +584,75 @@ const styles = StyleSheet.create({ fontSize: 11, fontWeight: "bold", }, + // Custom Channel Title Styles + customChannelTitle: { + fontSize: 18, + fontWeight: "bold", + color: "#1f2937", + }, + + // Search Header Styles + searchHeaderContainer: { + backgroundColor: "#f8fafc", + borderBottomWidth: 1, + borderBottomColor: "#e5e7eb", + overflow: "hidden", + paddingHorizontal: 20, + }, + searchInputContainer: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "white", + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 8, + borderWidth: 1, + borderColor: "#e5e7eb", + marginTop: 15, + }, + searchIcon: { + marginRight: 10, + }, + searchInput: { + flex: 1, + fontSize: 16, + color: "#374151", + paddingVertical: 0, + fontWeight: "500", + }, + clearButton: { + padding: 5, + }, + searchInfoContainer: { + paddingTop: 10, + paddingBottom: 15, + }, + searchInfoText: { + fontSize: 14, + color: "#6b7280", + }, + searchLoadingContainer: { + paddingVertical: 20, + alignItems: "center", + }, + searchLoadingText: { + fontSize: 16, + color: "#6b7280", + }, + emptySearchContainer: { + paddingVertical: 40, + alignItems: "center", + }, + emptySearchTitle: { + fontSize: 20, + color: "#4b5563", + marginTop: 10, + }, + emptySearchMessage: { + fontSize: 16, + color: "#6b7280", + marginTop: 5, + textAlign: "center", + paddingHorizontal: 20, + }, }); From 59e896ee695f3cd111461ea82293957f16ad0af4 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 19 Jul 2025 18:55:06 +0200 Subject: [PATCH 02/16] Improve loading view, and fix bug with colors --- xtablo-expo/app/(home)/(tabs)/index.tsx | 282 ++++++++++++++++-------- 1 file changed, 186 insertions(+), 96 deletions(-) diff --git a/xtablo-expo/app/(home)/(tabs)/index.tsx b/xtablo-expo/app/(home)/(tabs)/index.tsx index a4d7589..1db4bc6 100644 --- a/xtablo-expo/app/(home)/(tabs)/index.tsx +++ b/xtablo-expo/app/(home)/(tabs)/index.tsx @@ -17,96 +17,6 @@ import { ColorMap } from "@/constants/colors"; import { UserTablo } from "@/types/tablos.types"; // Custom Avatar Component for Channel List -const CustomChannelAvatar = ({ - channel, - tablos, -}: { - channel: Channel; - tablos: UserTablo[]; -}) => { - const tabloId = channel?.id || ""; - const tablo = tablos?.find((t) => t.id === tabloId); - const tabloColor = tablo?.color || "bg-blue-500"; - const tabloName = tablo?.name || channel?.data?.name || "Tablo"; - - // Get members info - const members = channel?.state?.members || {}; - const memberCount = Object.keys(members).length; - - // Generate initials from tablo name - const getInitials = (name: string) => { - return name - .split(" ") - .map((word) => word.charAt(0)) - .join("") - .toUpperCase() - .slice(0, 2); - }; - - // // Create gradient colors based on tablo color - const getTabloGradientColors = (colorKey: string): [string, string] => { - const baseColor = ColorMap[colorKey] || ColorMap["bg-blue-500"]; - - // Create a lighter version for gradient effect - const lightenColor = (hex: string, percent: number): string => { - const num = parseInt(hex.replace("#", ""), 16); - const amt = Math.round(2.55 * percent); - const R = Math.min(255, Math.max(0, (num >> 16) + amt)); - const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt)); - const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt)); - return "#" + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1); - }; - - // Create a darker version for gradient effect - const darkenColor = (hex: string, percent: number): string => { - const num = parseInt(hex.replace("#", ""), 16); - const amt = Math.round(2.55 * percent); - const R = Math.min(255, Math.max(0, (num >> 16) - amt)); - const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) - amt)); - const B = Math.min(255, Math.max(0, (num & 0x0000ff) - amt)); - return "#" + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1); - }; - - const lightColor = lightenColor(baseColor, 15); - const darkColor = darkenColor(baseColor, 10); - - return [lightColor, darkColor]; - }; - - const initials = getInitials(tabloName); - const gradientColors = getTabloGradientColors(tabloColor); - - return ( - - - {initials} - - {/* Member count indicator for group channels */} - {memberCount > 2 && ( - - {memberCount} - - )} - - - {/* Decorative ring */} - - - {/* Status indicator (online/active) */} - - - ); -}; // Custom Title Component for bigger channel names const CustomChannelTitle = ({ channel }: { channel: Channel }) => { @@ -121,7 +31,7 @@ const CustomChannelTitle = ({ channel }: { channel: Channel }) => { export default function HomeScreen() { const user = useUser(); - const { data: tablos } = useTablosList(); + const { data: tablos, isLoading } = useTablosList(); // Search animation state // const [isSearchVisible, setIsSearchVisible] = useState(false); @@ -164,10 +74,100 @@ export default function HomeScreen() { limit: 20, }; - // Create a wrapper component for the avatar that has access to tablos data - const AvatarWithTablos = ({ channel }: { channel: Channel }) => ( - - ); + const CustomChannelAvatar = ({ + channel, + tablos, + }: { + channel: Channel; + tablos: UserTablo[]; + }) => { + const tabloId = channel?.id || ""; + const tablo = tablos?.find((t) => t.id === tabloId); + const tabloColor = tablo?.color || "bg-blue-500"; + const tabloName = tablo?.name || channel?.data?.name || "Tablo"; + + // Get members info + const members = channel?.state?.members || {}; + const memberCount = Object.keys(members).length; + + // Generate initials from tablo name + const getInitials = (name: string) => { + return name + .split(" ") + .map((word) => word.charAt(0)) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + // // Create gradient colors based on tablo color + const getTabloGradientColors = (colorKey: string): [string, string] => { + const baseColor = ColorMap[colorKey] || ColorMap["bg-blue-500"]; + + // Create a lighter version for gradient effect + const lightenColor = (hex: string, percent: number): string => { + const num = parseInt(hex.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = Math.min(255, Math.max(0, (num >> 16) + amt)); + const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt)); + const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt)); + return ( + "#" + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1) + ); + }; + + // Create a darker version for gradient effect + const darkenColor = (hex: string, percent: number): string => { + const num = parseInt(hex.replace("#", ""), 16); + const amt = Math.round(2.55 * percent); + const R = Math.min(255, Math.max(0, (num >> 16) - amt)); + const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) - amt)); + const B = Math.min(255, Math.max(0, (num & 0x0000ff) - amt)); + return ( + "#" + ((1 << 24) + (R << 16) + (G << 8) + B).toString(16).slice(1) + ); + }; + + const lightColor = lightenColor(baseColor, 15); + const darkColor = darkenColor(baseColor, 10); + + return [lightColor, darkColor]; + }; + + const initials = getInitials(tabloName); + const gradientColors = getTabloGradientColors(tabloColor); + + return ( + + + {initials} + + {/* Member count indicator for group channels */} + {memberCount > 2 && ( + + {memberCount} + + )} + + + {/* Decorative ring */} + + + {/* Status indicator (online/active) */} + + + ); + }; // Toggle search animation // const toggleSearch = () => { @@ -272,6 +272,51 @@ export default function HomeScreen() { // // ); + if (isLoading) { + return ( + + + + {/* Loading Header */} + + + + + Discussions + + Chargement de vos conversations... + + + + + + {/* Decorative Elements */} + + + + + {/* Loading Content */} + + {/* Loading Skeleton Items */} + {[1, 2, 3, 4, 5].map((item) => ( + + + + + + + + ))} + + + ); + } + return ( @@ -327,7 +372,12 @@ export default function HomeScreen() { }} sort={sort} options={options} - PreviewAvatar={AvatarWithTablos} + PreviewAvatar={(props) => ( + + )} PreviewTitle={CustomChannelTitle} // ListHeaderComponent={SearchHeader} // Show loading state during search @@ -655,4 +705,44 @@ const styles = StyleSheet.create({ textAlign: "center", paddingHorizontal: 20, }, + + // Loading Skeleton Styles + loadingContentContainer: { + flex: 1, + backgroundColor: "#f8fafc", + marginTop: -10, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + paddingTop: 20, + paddingHorizontal: 20, + }, + loadingItem: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 12, + marginBottom: 8, + }, + loadingAvatar: { + width: 56, + height: 56, + borderRadius: 16, + backgroundColor: "#e5e7eb", + marginRight: 12, + }, + loadingTextContainer: { + flex: 1, + }, + loadingTitle: { + height: 20, + backgroundColor: "#e5e7eb", + borderRadius: 4, + marginBottom: 8, + width: "70%", + }, + loadingSubtitle: { + height: 16, + backgroundColor: "#f3f4f6", + borderRadius: 4, + width: "50%", + }, }); From 0b3d382285d7e96cf24c24ff3617d9c9a8d15d34 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 19 Jul 2025 18:57:05 +0200 Subject: [PATCH 03/16] Add comment --- .pre-commit-config.yaml | 2 +- xtablo-expo/app/(home)/(tabs)/index.tsx | 36 ++++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5a27d9..dec20d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: entry: just test-frontend language: python pass_filenames: false - files: \.ts* + files: ^ui/.*\.(ts|tsx|js|jsx)$ - id: typecheck name: Typecheck Frontend entry: just typecheck diff --git a/xtablo-expo/app/(home)/(tabs)/index.tsx b/xtablo-expo/app/(home)/(tabs)/index.tsx index 1db4bc6..9438df2 100644 --- a/xtablo-expo/app/(home)/(tabs)/index.tsx +++ b/xtablo-expo/app/(home)/(tabs)/index.tsx @@ -206,26 +206,26 @@ export default function HomeScreen() { // }, [isSearchVisible, searchQuery]); // Animated styles using react-native-reanimated - const animatedSearchStyle = useAnimatedStyle(() => { - const height = interpolate( - searchAnimation.value, - [0, 1], - [0, 80], - Extrapolate.CLAMP - ); + // const animatedSearchStyle = useAnimatedStyle(() => { + // const height = interpolate( + // searchAnimation.value, + // [0, 1], + // [0, 80], + // Extrapolate.CLAMP + // ); - const opacity = interpolate( - searchAnimation.value, - [0, 0.5, 1], - [0, 0, 1], - Extrapolate.CLAMP - ); + // const opacity = interpolate( + // searchAnimation.value, + // [0, 0.5, 1], + // [0, 0, 1], + // Extrapolate.CLAMP + // ); - return { - height, - opacity, - }; - }); + // return { + // height, + // opacity, + // }; + // }); // Simple search header component - no memoization // const SearchHeader = () => ( From 490eef9b431e0265412469656d1bc60d195949fe Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 19 Jul 2025 19:04:39 +0200 Subject: [PATCH 04/16] Add tablo deletion feature --- xtablo-expo/app/(home)/(tabs)/planning.tsx | 1 - xtablo-expo/app/(home)/(tabs)/tablos.tsx | 37 +++++++++++++++++++--- xtablo-expo/hooks/tablos.ts | 27 ++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/xtablo-expo/app/(home)/(tabs)/planning.tsx b/xtablo-expo/app/(home)/(tabs)/planning.tsx index b84d52a..25db5d9 100644 --- a/xtablo-expo/app/(home)/(tabs)/planning.tsx +++ b/xtablo-expo/app/(home)/(tabs)/planning.tsx @@ -166,7 +166,6 @@ export default function PlanningScreen() { Alert.alert("Erreur", "Veuillez sélectionner un tablo"); return; } - console.log({ newEvent }); createEvent({ ...newEvent, start_date: newEvent.start_date, diff --git a/xtablo-expo/app/(home)/(tabs)/tablos.tsx b/xtablo-expo/app/(home)/(tabs)/tablos.tsx index dd58976..8234b4b 100644 --- a/xtablo-expo/app/(home)/(tabs)/tablos.tsx +++ b/xtablo-expo/app/(home)/(tabs)/tablos.tsx @@ -16,7 +16,7 @@ import { Platform, } from "react-native"; import { LinearGradient } from "expo-linear-gradient"; -import { useCreateTablo, useTablosList } from "@/hooks/tablos"; +import { useCreateTablo, useTablosList, useDeleteTablo } from "@/hooks/tablos"; import { UserTablo } from "@/types/tablos.types"; import { Plus, @@ -30,7 +30,6 @@ import { } from "lucide-react-native"; import { router } from "expo-router"; import { AVAILABLE_COLORS, ColorMap } from "@/constants/colors"; -import { useAuth } from "@/stores/auth"; const { width } = Dimensions.get("window"); const numColumns = 2; @@ -46,6 +45,7 @@ export default function TablosScreen() { const [refreshing, setRefreshing] = useState(false); const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); const { mutate: createTablo } = useCreateTablo(); + const { mutate: deleteTablo } = useDeleteTablo(); const [newTablo, setNewTablo] = useState<{ name: string; @@ -156,6 +156,33 @@ export default function TablosScreen() { }); }; + const handleDeleteTablo = (tablo: UserTablo) => { + // Only allow admin users to delete tablos + if (!tablo.is_admin) { + Alert.alert( + "Non autorisé", + "Seuls les administrateurs peuvent supprimer ce tablo." + ); + return; + } + + Alert.alert( + "Supprimer le tablo", + `Êtes-vous sûr de vouloir supprimer "${tablo.name}" ? Cette action est irréversible.`, + [ + { + text: "Annuler", + style: "cancel", + }, + { + text: "Supprimer", + style: "destructive", + onPress: () => deleteTablo(tablo.id), + }, + ] + ); + }; + const renderTabloCard = ({ item: tablo }: { item: UserTablo }) => { const initials = tablo.name?.charAt(0)?.toUpperCase() || "T"; @@ -166,6 +193,7 @@ export default function TablosScreen() { { width: viewMode === "grid" ? itemWidth : "100%" }, ]} onPress={() => navigateToTablo(tablo)} + onLongPress={() => handleDeleteTablo(tablo)} activeOpacity={0.8} > {/* Tablo Image/Color Header */} @@ -249,6 +277,7 @@ export default function TablosScreen() { navigateToTablo(tablo)} + onLongPress={() => handleDeleteTablo(tablo)} activeOpacity={0.8} > { }, }); }; + +// Delete tablo (soft delete) +export const useDeleteTablo = () => { + const session = useAuth((state) => state.session); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (id: string) => { + await api.delete("/api/v1/tablos/delete", { + data: { id }, + headers: { + Authorization: `Bearer ${session?.access_token}`, + }, + }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["tablos"] }); + }, + onError: (error) => { + console.error(error); + Alert.alert( + "Erreur", + "Impossible de supprimer le tablo. Veuillez réessayer." + ); + }, + }); +}; From 73cbe6356f5d502bfb64e6decf455450262082c2 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 19 Jul 2025 19:10:29 +0200 Subject: [PATCH 05/16] Add a good settings page --- xtablo-expo/app/(home)/(tabs)/_layout.tsx | 19 +- xtablo-expo/app/(home)/(tabs)/settings.tsx | 453 +++++++++++++++++++++ 2 files changed, 470 insertions(+), 2 deletions(-) create mode 100644 xtablo-expo/app/(home)/(tabs)/settings.tsx diff --git a/xtablo-expo/app/(home)/(tabs)/_layout.tsx b/xtablo-expo/app/(home)/(tabs)/_layout.tsx index a51d2ee..5fa0f1d 100644 --- a/xtablo-expo/app/(home)/(tabs)/_layout.tsx +++ b/xtablo-expo/app/(home)/(tabs)/_layout.tsx @@ -8,9 +8,8 @@ import { useColorScheme } from "@/hooks/useColorScheme"; import { MessageCircle, Calendar, - List, - Home, Grid3X3, + Settings, } from "lucide-react-native"; export default function TabLayout() { @@ -76,6 +75,7 @@ export default function TabLayout() { /> ), tabBarLabel: "Discussions", + // tabBarBadge: 10, TODO: Add badge for notifications }} /> + ( + + ), + tabBarLabel: "Paramètres", + tabBarBadge: undefined, + }} + /> ); } diff --git a/xtablo-expo/app/(home)/(tabs)/settings.tsx b/xtablo-expo/app/(home)/(tabs)/settings.tsx new file mode 100644 index 0000000..c1b422b --- /dev/null +++ b/xtablo-expo/app/(home)/(tabs)/settings.tsx @@ -0,0 +1,453 @@ +import React, { useState } from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + StatusBar, + ScrollView, + Switch, + Alert, + Linking, +} from "react-native"; +import { LinearGradient } from "expo-linear-gradient"; +import { useAuth } from "@/stores/auth"; +import { useUser } from "@/providers/UserProvider"; +import { + User, + Bell, + Moon, + Shield, + HelpCircle, + Info, + MessageSquare, + LogOut, + ChevronRight, + Smartphone, + Globe, + Lock, + Heart, +} from "lucide-react-native"; +import { router } from "expo-router"; + +export default function SettingsScreen() { + const signOut = useAuth((state) => state.signOut); + const user = useUser(); + + // Settings state + const [pushNotifications, setPushNotifications] = useState(true); + const [emailNotifications, setEmailNotifications] = useState(true); + const [darkMode, setDarkMode] = useState(false); + const [biometricAuth, setBiometricAuth] = useState(false); + + const handleSignOut = () => { + Alert.alert("Déconnexion", "Êtes-vous sûr de vouloir vous déconnecter ?", [ + { + text: "Annuler", + style: "cancel", + }, + { + text: "Se déconnecter", + style: "destructive", + onPress: signOut, + }, + ]); + }; + + const handleContactSupport = () => { + Linking.openURL("mailto:support@xtablo.com?subject=Support XTablo"); + }; + + const handleRateApp = () => { + // Replace with your actual app store URL + Alert.alert( + "Évaluer l'application", + "Vous aimez XTablo ? Laissez-nous un avis sur l'App Store !", + [ + { text: "Plus tard", style: "cancel" }, + { text: "Évaluer", onPress: () => console.log("Rate app") }, + ] + ); + }; + + const renderSettingsSection = (title: string, children: React.ReactNode) => ( + + {title} + {children} + + ); + + const renderSettingsItem = ( + icon: React.ReactNode, + title: string, + subtitle?: string, + onPress?: () => void, + rightComponent?: React.ReactNode, + showArrow: boolean = true + ) => ( + + + {icon} + + {title} + {subtitle && ( + {subtitle} + )} + + + + {rightComponent} + {showArrow && onPress && ( + + )} + + + ); + + const renderSwitchItem = ( + icon: React.ReactNode, + title: string, + subtitle: string, + value: boolean, + onValueChange: (value: boolean) => void + ) => + renderSettingsItem( + icon, + title, + subtitle, + undefined, + , + false + ); + + return ( + + + + {/* Header */} + + + Paramètres + + Gérez vos préférences et votre compte + + + + {/* Decorative Elements */} + + + + + + {/* Account Section */} + {renderSettingsSection( + "Compte", + <> + {renderSettingsItem( + , + "Profil utilisateur", + `${user.name || "Non défini"} • ${user.email}`, + () => router.push("/user/profile"), + undefined, + true + )} + + )} + + {/* Notifications Section */} + {renderSettingsSection( + "Notifications", + <> + {renderSwitchItem( + , + "Notifications push", + "Recevoir des notifications sur votre appareil", + pushNotifications, + setPushNotifications + )} + {renderSwitchItem( + , + "Notifications par email", + "Recevoir des notifications par email", + emailNotifications, + setEmailNotifications + )} + + )} + + {/* Appearance Section */} + {renderSettingsSection( + "Apparence", + <> + {renderSwitchItem( + , + "Mode sombre", + "Utiliser le thème sombre", + darkMode, + setDarkMode + )} + + )} + + {/* Security Section */} + {renderSettingsSection( + "Sécurité et confidentialité", + <> + {renderSwitchItem( + , + "Authentification biométrique", + "Utiliser votre empreinte ou Face ID", + biometricAuth, + setBiometricAuth + )} + {renderSettingsItem( + , + "Politique de confidentialité", + "Consulter notre politique de confidentialité", + () => Linking.openURL("https://xtablo.com/privacy-policy"), + undefined, + true + )} + + )} + + {/* Help & Support Section */} + {renderSettingsSection( + "Aide et support", + <> + {renderSettingsItem( + , + "Centre d'aide", + "FAQ et guides d'utilisation", + () => Linking.openURL("https://xtablo.com/help"), + undefined, + true + )} + {renderSettingsItem( + , + "Contacter le support", + "Envoyez-nous un email", + handleContactSupport, + undefined, + true + )} + {renderSettingsItem( + , + "Évaluer l'application", + "Aidez-nous à améliorer XTablo", + handleRateApp, + undefined, + true + )} + + )} + + {/* About Section */} + {renderSettingsSection( + "À propos", + <> + {renderSettingsItem( + , + "Version de l'application", + "1.0.0 (Build 1)", + undefined, + undefined, + false + )} + {renderSettingsItem( + , + "Site web", + "Visitez notre site web", + () => Linking.openURL("https://xtablo.com"), + undefined, + true + )} + + )} + + {/* Sign Out Section */} + + + + + Se déconnecter + + + + + {/* Bottom Spacing */} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#f8fafc", + }, + headerGradient: { + paddingTop: 50, + paddingBottom: 25, + paddingHorizontal: 20, + position: "relative", + overflow: "hidden", + }, + headerContent: { + zIndex: 10, + }, + headerTitle: { + fontSize: 28, + color: "white", + fontWeight: "bold", + marginBottom: 4, + }, + headerSubtitle: { + fontSize: 16, + color: "rgba(255, 255, 255, 0.8)", + fontWeight: "400", + }, + decorativeCircle1: { + position: "absolute", + top: -50, + right: -30, + width: 120, + height: 120, + borderRadius: 60, + backgroundColor: "rgba(255, 255, 255, 0.1)", + }, + decorativeCircle2: { + position: "absolute", + bottom: -20, + left: -20, + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: "rgba(255, 255, 255, 0.08)", + }, + content: { + flex: 1, + backgroundColor: "#f8fafc", + marginTop: -10, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + paddingTop: 20, + }, + section: { + marginBottom: 24, + paddingHorizontal: 20, + }, + sectionTitle: { + fontSize: 18, + fontWeight: "600", + color: "#1f2937", + marginBottom: 12, + marginLeft: 4, + }, + sectionContent: { + backgroundColor: "white", + borderRadius: 16, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 3, + overflow: "hidden", + }, + settingsItem: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 20, + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: "#f3f4f6", + }, + settingsItemLeft: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + iconContainer: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "#f3f4f6", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + settingsItemContent: { + flex: 1, + }, + settingsItemTitle: { + fontSize: 16, + fontWeight: "500", + color: "#1f2937", + marginBottom: 2, + }, + settingsItemSubtitle: { + fontSize: 14, + color: "#6b7280", + }, + settingsItemRight: { + flexDirection: "row", + alignItems: "center", + }, + signOutSection: { + paddingHorizontal: 20, + marginTop: 20, + marginBottom: 20, + }, + signOutButton: { + borderRadius: 16, + shadowColor: "#ef4444", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 6, + }, + signOutGradient: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + paddingVertical: 16, + paddingHorizontal: 24, + borderRadius: 16, + }, + signOutText: { + color: "white", + fontSize: 16, + fontWeight: "600", + marginLeft: 8, + }, + bottomSpacing: { + height: 100, + }, +}); From 8992f58512d7d8d5ebeda52d593a71043aca515a Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 20 Jul 2025 12:23:18 +0200 Subject: [PATCH 06/16] Rework routing --- .../app/{(home) => (app)}/(tabs)/_layout.tsx | 0 .../app/{(home) => (app)}/(tabs)/index.tsx | 0 .../app/{(home) => (app)}/(tabs)/planning.tsx | 0 .../app/{(home) => (app)}/(tabs)/settings.tsx | 4 +- .../app/{(home) => (app)}/(tabs)/tablos.tsx | 0 xtablo-expo/app/{(home) => (app)}/_layout.tsx | 0 .../app/{(home) => (app)}/channel/[cid].tsx | 0 .../app/{(home) => (app)}/channel/_layout.tsx | 0 .../app/{(home) => (app)}/user/profile.tsx | 4 +- xtablo-expo/app/(auth)/_layout.tsx | 26 ----- xtablo-expo/app/_layout.tsx | 54 ++++++---- xtablo-expo/app/{(auth) => }/login.tsx | 8 +- xtablo-expo/app/{(auth) => }/signup.tsx | 6 +- xtablo-expo/components/AppleLoginButton.tsx | 4 +- xtablo-expo/components/GoogleLoginButton.tsx | 4 +- xtablo-expo/components/LoadingView.tsx | 98 +++++++++++++++++++ xtablo-expo/components/Splash.tsx | 17 ++++ xtablo-expo/hooks/auth.ts | 18 ++++ xtablo-expo/hooks/tablos.ts | 6 +- xtablo-expo/hooks/user.ts | 4 +- xtablo-expo/stores/auth.tsx | 6 +- 21 files changed, 191 insertions(+), 68 deletions(-) rename xtablo-expo/app/{(home) => (app)}/(tabs)/_layout.tsx (100%) rename xtablo-expo/app/{(home) => (app)}/(tabs)/index.tsx (100%) rename xtablo-expo/app/{(home) => (app)}/(tabs)/planning.tsx (100%) rename xtablo-expo/app/{(home) => (app)}/(tabs)/settings.tsx (99%) rename xtablo-expo/app/{(home) => (app)}/(tabs)/tablos.tsx (100%) rename xtablo-expo/app/{(home) => (app)}/_layout.tsx (100%) rename xtablo-expo/app/{(home) => (app)}/channel/[cid].tsx (100%) rename xtablo-expo/app/{(home) => (app)}/channel/_layout.tsx (100%) rename xtablo-expo/app/{(home) => (app)}/user/profile.tsx (98%) delete mode 100644 xtablo-expo/app/(auth)/_layout.tsx rename xtablo-expo/app/{(auth) => }/login.tsx (94%) rename xtablo-expo/app/{(auth) => }/signup.tsx (96%) create mode 100644 xtablo-expo/components/LoadingView.tsx create mode 100644 xtablo-expo/components/Splash.tsx create mode 100644 xtablo-expo/hooks/auth.ts diff --git a/xtablo-expo/app/(home)/(tabs)/_layout.tsx b/xtablo-expo/app/(app)/(tabs)/_layout.tsx similarity index 100% rename from xtablo-expo/app/(home)/(tabs)/_layout.tsx rename to xtablo-expo/app/(app)/(tabs)/_layout.tsx diff --git a/xtablo-expo/app/(home)/(tabs)/index.tsx b/xtablo-expo/app/(app)/(tabs)/index.tsx similarity index 100% rename from xtablo-expo/app/(home)/(tabs)/index.tsx rename to xtablo-expo/app/(app)/(tabs)/index.tsx diff --git a/xtablo-expo/app/(home)/(tabs)/planning.tsx b/xtablo-expo/app/(app)/(tabs)/planning.tsx similarity index 100% rename from xtablo-expo/app/(home)/(tabs)/planning.tsx rename to xtablo-expo/app/(app)/(tabs)/planning.tsx diff --git a/xtablo-expo/app/(home)/(tabs)/settings.tsx b/xtablo-expo/app/(app)/(tabs)/settings.tsx similarity index 99% rename from xtablo-expo/app/(home)/(tabs)/settings.tsx rename to xtablo-expo/app/(app)/(tabs)/settings.tsx index c1b422b..e705b79 100644 --- a/xtablo-expo/app/(home)/(tabs)/settings.tsx +++ b/xtablo-expo/app/(app)/(tabs)/settings.tsx @@ -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 diff --git a/xtablo-expo/app/(home)/(tabs)/tablos.tsx b/xtablo-expo/app/(app)/(tabs)/tablos.tsx similarity index 100% rename from xtablo-expo/app/(home)/(tabs)/tablos.tsx rename to xtablo-expo/app/(app)/(tabs)/tablos.tsx diff --git a/xtablo-expo/app/(home)/_layout.tsx b/xtablo-expo/app/(app)/_layout.tsx similarity index 100% rename from xtablo-expo/app/(home)/_layout.tsx rename to xtablo-expo/app/(app)/_layout.tsx diff --git a/xtablo-expo/app/(home)/channel/[cid].tsx b/xtablo-expo/app/(app)/channel/[cid].tsx similarity index 100% rename from xtablo-expo/app/(home)/channel/[cid].tsx rename to xtablo-expo/app/(app)/channel/[cid].tsx diff --git a/xtablo-expo/app/(home)/channel/_layout.tsx b/xtablo-expo/app/(app)/channel/_layout.tsx similarity index 100% rename from xtablo-expo/app/(home)/channel/_layout.tsx rename to xtablo-expo/app/(app)/channel/_layout.tsx diff --git a/xtablo-expo/app/(home)/user/profile.tsx b/xtablo-expo/app/(app)/user/profile.tsx similarity index 98% rename from xtablo-expo/app/(home)/user/profile.tsx rename to xtablo-expo/app/(app)/user/profile.tsx index dd38091..ee0183c 100644 --- a/xtablo-expo/app/(home)/user/profile.tsx +++ b/xtablo-expo/app/(app)/user/profile.tsx @@ -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 || ""); diff --git a/xtablo-expo/app/(auth)/_layout.tsx b/xtablo-expo/app/(auth)/_layout.tsx deleted file mode 100644 index 586987b..0000000 --- a/xtablo-expo/app/(auth)/_layout.tsx +++ /dev/null @@ -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 ; - } - if (user) { - return ; - } - return ; -} diff --git a/xtablo-expo/app/_layout.tsx b/xtablo-expo/app/_layout.tsx index cb6bcd8..d34e904 100644 --- a/xtablo-expo/app/_layout.tsx +++ b/xtablo-expo/app/_layout.tsx @@ -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 ; - } return ( @@ -53,14 +43,36 @@ export default function RootLayout() { - - - - - + + ); } + +const RootNavigator = () => { + const { isLoading, isLoggedIn } = useInitializeApp(); + + if (isLoading) { + return ; + } + + console.log("isLoggedIn", isLoggedIn); + + return ( + + + + + + + + + + + + + ); +}; diff --git a/xtablo-expo/app/(auth)/login.tsx b/xtablo-expo/app/login.tsx similarity index 94% rename from xtablo-expo/app/(auth)/login.tsx rename to xtablo-expo/app/login.tsx index 776b506..7d9f699 100644 --- a/xtablo-expo/app/(auth)/login.tsx +++ b/xtablo-expo/app/login.tsx @@ -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 ( diff --git a/xtablo-expo/app/(auth)/signup.tsx b/xtablo-expo/app/signup.tsx similarity index 96% rename from xtablo-expo/app/(auth)/signup.tsx rename to xtablo-expo/app/signup.tsx index c15a983..b59ecc2 100644 --- a/xtablo-expo/app/(auth)/signup.tsx +++ b/xtablo-expo/app/signup.tsx @@ -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 ( diff --git a/xtablo-expo/components/AppleLoginButton.tsx b/xtablo-expo/components/AppleLoginButton.tsx index f49eb2a..7c2804b 100644 --- a/xtablo-expo/components/AppleLoginButton.tsx +++ b/xtablo-expo/components/AppleLoginButton.tsx @@ -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 ( void }) => { - const authLoading = useAuth((state) => state.loading); + const authLoading = useAuthStore((state) => state.loading); return ( { + 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 ( + + + + + + + XTablo + + + Initialisation de l'application... + + + + ); +}; + +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, + }, +}); diff --git a/xtablo-expo/components/Splash.tsx b/xtablo-expo/components/Splash.tsx new file mode 100644 index 0000000..09cf463 --- /dev/null +++ b/xtablo-expo/components/Splash.tsx @@ -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; +} diff --git a/xtablo-expo/hooks/auth.ts b/xtablo-expo/hooks/auth.ts new file mode 100644 index 0000000..f93b24c --- /dev/null +++ b/xtablo-expo/hooks/auth.ts @@ -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 }; +}; diff --git a/xtablo-expo/hooks/tablos.ts b/xtablo-expo/hooks/tablos.ts index d3ee201..d83b9dd 100644 --- a/xtablo-expo/hooks/tablos.ts +++ b/xtablo-expo/hooks/tablos.ts @@ -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({ diff --git a/xtablo-expo/hooks/user.ts b/xtablo-expo/hooks/user.ts index 683b313..0abfe1e 100644 --- a/xtablo-expo/hooks/user.ts +++ b/xtablo-expo/hooks/user.ts @@ -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({ queryKey: ["user"], queryFn: async () => { diff --git a/xtablo-expo/stores/auth.tsx b/xtablo-expo/stores/auth.tsx index f7bf3e3..31f2855 100644 --- a/xtablo-expo/stores/auth.tsx +++ b/xtablo-expo/stores/auth.tsx @@ -10,6 +10,7 @@ import { QueryClient } from "@tanstack/react-query"; interface AuthState { session: Session | null; loading: boolean; + initialized: boolean; initialize: (queryClient: QueryClient) => Promise; setSession: (session: Session | null) => void; login: (email: string, password: string) => Promise; @@ -28,8 +29,9 @@ interface AuthState { WebBrowser.maybeCompleteAuthSession(); const redirectTo = makeRedirectUri({ path: "/(home)/(tabs)" }); -export const useAuth = create((set, get) => ({ +export const useAuthStore = create((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((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) => { From eb124f6210ae9b09d3904458864f966b5a111aae Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 20 Jul 2025 18:37:58 +0200 Subject: [PATCH 07/16] Restore animation on channel list --- xtablo-expo/app/(app)/(tabs)/index.tsx | 40 +++- xtablo-expo/app/(app)/(tabs)/planning.tsx | 5 +- xtablo-expo/app/_layout.tsx | 24 +-- .../components/SwipeableChannelPreview.tsx | 195 ++++++++++++++++++ xtablo-expo/package.json | 8 +- 5 files changed, 247 insertions(+), 25 deletions(-) create mode 100644 xtablo-expo/components/SwipeableChannelPreview.tsx diff --git a/xtablo-expo/app/(app)/(tabs)/index.tsx b/xtablo-expo/app/(app)/(tabs)/index.tsx index 9438df2..127c17b 100644 --- a/xtablo-expo/app/(app)/(tabs)/index.tsx +++ b/xtablo-expo/app/(app)/(tabs)/index.tsx @@ -1,20 +1,21 @@ import { router } from "expo-router"; -import { ChannelList } from "stream-chat-expo"; +import { + ChannelList, + ChannelPreviewMessenger, + ChannelPreviewMessengerProps, + DefaultStreamChatGenerics, +} from "stream-chat-expo"; import { ChannelSort, Channel } from "stream-chat"; import { useUser } from "@/providers/UserProvider"; import { View, Text, StyleSheet, StatusBar } from "react-native"; import { LinearGradient } from "expo-linear-gradient"; import { Search } from "lucide-react-native"; import React, { useMemo } from "react"; -import { - useSharedValue, - useAnimatedStyle, - interpolate, - Extrapolate, -} from "react-native-reanimated"; +import { useSharedValue } from "react-native-reanimated"; import { useTablosList } from "@/hooks/tablos"; import { ColorMap } from "@/constants/colors"; import { UserTablo } from "@/types/tablos.types"; +import { SwipeableChannelPreview } from "@/components/SwipeableChannelPreview"; // Custom Avatar Component for Channel List @@ -29,10 +30,24 @@ const CustomChannelTitle = ({ channel }: { channel: Channel }) => { ); }; +// Custom Preview Component with Swipe to Archive +// Swipe left on any channel to reveal the archive button +const CustomChannelPreview = ( + props: ChannelPreviewMessengerProps +) => { + return ( + + + + ); +}; + export default function HomeScreen() { const user = useUser(); const { data: tablos, isLoading } = useTablosList(); + // const animations = useSharedValue>({}); + // Search animation state // const [isSearchVisible, setIsSearchVisible] = useState(false); // const [searchQuery, setSearchQuery] = useState(""); @@ -372,6 +387,17 @@ export default function HomeScreen() { }} sort={sort} options={options} + Preview={(props) => ( + { + // animations.value = { + // [id]: 0, + // }; + // }} + /> + )} PreviewAvatar={(props) => ( diff --git a/xtablo-expo/app/_layout.tsx b/xtablo-expo/app/_layout.tsx index d34e904..de44150 100644 --- a/xtablo-expo/app/_layout.tsx +++ b/xtablo-expo/app/_layout.tsx @@ -11,12 +11,10 @@ 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, 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"; +import { ClickOutsideProvider } from "react-native-click-outside"; window.structuredClone = cloneDeep; @@ -39,15 +37,17 @@ export default function RootLayout() { return ( - - - - - - - + + + + + + + + + ); } diff --git a/xtablo-expo/components/SwipeableChannelPreview.tsx b/xtablo-expo/components/SwipeableChannelPreview.tsx new file mode 100644 index 0000000..102a135 --- /dev/null +++ b/xtablo-expo/components/SwipeableChannelPreview.tsx @@ -0,0 +1,195 @@ +import React from "react"; +import { View, Text, StyleSheet, Alert } from "react-native"; +import { + GestureDetector, + Gesture, + Pressable, +} from "react-native-gesture-handler"; +import Animated, { + useSharedValue, + useAnimatedStyle, + runOnJS, + withSpring, + interpolate, + Extrapolation, + FadeOut, + useDerivedValue, + SharedValue, +} from "react-native-reanimated"; +import { Archive } from "lucide-react-native"; +import { Channel } from "stream-chat"; +import { DefaultStreamChatGenerics } from "stream-chat-expo"; + +interface SwipeableChannelPreviewProps { + channel: Channel; + children: React.ReactNode; +} + +const SWIPE_THRESHOLD = -80; +const ACTION_WIDTH = 80; + +export const SwipeableChannelPreview: React.FC< + SwipeableChannelPreviewProps +> = ({ channel, children }) => { + const id = channel.id ?? ""; + const translateX = useSharedValue(0); + + const handleArchiveChannel = async () => { + try { + // Show confirmation dialog + Alert.alert( + "Archiver la conversation", + "Êtes-vous sûr de vouloir archiver cette conversation ainsi que le tablo associé ?", + [ + { + text: "Annuler", + style: "cancel", + onPress: () => { + // Close the swipe action + translateX.value = withSpring(0); + }, + }, + { + text: "Archiver", + style: "destructive", + onPress: async () => { + try { + // Hide the channel for the current user + await channel.hide(); + + // Close the swipe action + translateX.value = withSpring(0); + + // Show success message + Alert.alert( + "Succès", + "La conversation a été archivée avec succès", + [{ text: "OK" }] + ); + } catch (error) { + console.error("Error archiving channel:", error); + Alert.alert("Erreur", "Impossible d'archiver la conversation"); + } + }, + }, + ], + { cancelable: true } + ); + } catch (error) { + console.error("Error showing archive dialog:", error); + } + }; + + const gestureHandler = Gesture.Pan() + .onStart((context) => { + // cancelOtherAnimations(id); + context.translationX = translateX.value; + }) + .onUpdate((event) => { + // Only allow swiping left (negative values) + translateX.value = Math.min( + 0, + Math.max(SWIPE_THRESHOLD, event.translationX) + ); + }) + .onEnd((event) => { + const shouldOpen = translateX.value < SWIPE_THRESHOLD / 2; + + if (shouldOpen) { + translateX.value = withSpring(SWIPE_THRESHOLD); + } else { + translateX.value = withSpring(0); + } + }); + + const channelAnimatedStyle = useAnimatedStyle(() => { + return { + transform: [{ translateX: translateX.value }], + }; + }); + + const actionAnimatedStyle = useAnimatedStyle(() => { + const scale = interpolate( + translateX.value, + [SWIPE_THRESHOLD, 0], + [1, 0], + Extrapolation.CLAMP + ); + + const opacity = interpolate( + translateX.value, + [SWIPE_THRESHOLD, 0], + [1, 0], + Extrapolation.CLAMP + ); + + return { + transform: [{ scale }], + opacity, + }; + }); + + const onArchivePress = () => { + runOnJS(handleArchiveChannel)(); + }; + + return ( + + {/* Right Actions Background */} + + + + + Archiver + + + + + {/* Channel Content */} + + + {children} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + position: "relative", + }, + channelContainer: { + backgroundColor: "white", + flex: 1, + }, + rightActionsContainer: { + position: "absolute", + right: 0, + top: 0, + bottom: 0, + width: ACTION_WIDTH, + justifyContent: "center", + alignItems: "center", + }, + archiveButton: { + backgroundColor: "#166534", // Dark green color for archive + justifyContent: "center", + alignItems: "center", + width: ACTION_WIDTH, + height: "100%", + paddingHorizontal: 10, + }, + actionContent: { + justifyContent: "center", + alignItems: "center", + gap: 4, + }, + actionText: { + color: "white", + fontSize: 12, + fontWeight: "600", + textAlign: "center", + }, +}); diff --git a/xtablo-expo/package.json b/xtablo-expo/package.json index e4739c7..78ac640 100644 --- a/xtablo-expo/package.json +++ b/xtablo-expo/package.json @@ -25,13 +25,16 @@ "@tanstack/react-query": "^5.75.2", "aes-js": "^3.1.2", "expo": "^53.0.19", + "expo-auth-session": "~6.2.1", "expo-av": "~15.1.7", "expo-blur": "~14.1.5", "expo-constants": "~17.1.5", + "expo-crypto": "~14.1.5", "expo-font": "~13.3.2", "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", @@ -55,10 +58,7 @@ "react-native-web": "^0.20.0", "react-native-webview": "13.13.5", "stream-chat-expo": "^6.7.3", - "zustand": "^5.0.4", - "expo-crypto": "~14.1.5", - "expo-auth-session": "~6.2.1", - "expo-linear-gradient": "~14.1.5" + "zustand": "^5.0.4" }, "devDependencies": { "@babel/core": "^7.25.2", From 527f4a8fb176ff9f32f948de56ad5e42ba9fe5ec Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 20 Jul 2025 18:39:47 +0200 Subject: [PATCH 08/16] not sure what to do when a channel is archived --- .../components/SwipeableChannelPreview.tsx | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/xtablo-expo/components/SwipeableChannelPreview.tsx b/xtablo-expo/components/SwipeableChannelPreview.tsx index 102a135..3b78baf 100644 --- a/xtablo-expo/components/SwipeableChannelPreview.tsx +++ b/xtablo-expo/components/SwipeableChannelPreview.tsx @@ -31,7 +31,6 @@ const ACTION_WIDTH = 80; export const SwipeableChannelPreview: React.FC< SwipeableChannelPreviewProps > = ({ channel, children }) => { - const id = channel.id ?? ""; const translateX = useSharedValue(0); const handleArchiveChannel = async () => { @@ -52,25 +51,25 @@ export const SwipeableChannelPreview: React.FC< { text: "Archiver", style: "destructive", - onPress: async () => { - try { - // Hide the channel for the current user - await channel.hide(); + // onPress: async () => { + // try { + // // Hide the channel for the current user + // await channel.hide(); - // Close the swipe action - translateX.value = withSpring(0); + // // Close the swipe action + // translateX.value = withSpring(0); - // Show success message - Alert.alert( - "Succès", - "La conversation a été archivée avec succès", - [{ text: "OK" }] - ); - } catch (error) { - console.error("Error archiving channel:", error); - Alert.alert("Erreur", "Impossible d'archiver la conversation"); - } - }, + // // Show success message + // Alert.alert( + // "Succès", + // "La conversation a été archivée avec succès", + // [{ text: "OK" }] + // ); + // } catch (error) { + // console.error("Error archiving channel:", error); + // Alert.alert("Erreur", "Impossible d'archiver la conversation"); + // } + // }, }, ], { cancelable: true } From be236b64df3913dcc37c560a62964e6b301b6bf0 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 20 Jul 2025 19:28:56 +0200 Subject: [PATCH 09/16] Dark mode --- xtablo-expo/app/(app)/(tabs)/index.tsx | 159 ++++++-- xtablo-expo/app/(app)/(tabs)/planning.tsx | 373 ++++++++++++------ xtablo-expo/app/(app)/(tabs)/settings.tsx | 102 +++-- xtablo-expo/app/(app)/(tabs)/tablos.tsx | 118 ++++-- xtablo-expo/app/(app)/channel/[cid].tsx | 62 ++- xtablo-expo/app/(app)/user/profile.tsx | 244 ++++++------ xtablo-expo/app/login.tsx | 52 ++- xtablo-expo/app/signup.tsx | 40 +- xtablo-expo/components/LoadingView.tsx | 27 +- .../components/SwipeableChannelPreview.tsx | 40 +- xtablo-expo/providers/ChatProvider.tsx | 103 ++++- 11 files changed, 940 insertions(+), 380 deletions(-) diff --git a/xtablo-expo/app/(app)/(tabs)/index.tsx b/xtablo-expo/app/(app)/(tabs)/index.tsx index 127c17b..1bb802f 100644 --- a/xtablo-expo/app/(app)/(tabs)/index.tsx +++ b/xtablo-expo/app/(app)/(tabs)/index.tsx @@ -16,15 +16,24 @@ import { useTablosList } from "@/hooks/tablos"; import { ColorMap } from "@/constants/colors"; import { UserTablo } from "@/types/tablos.types"; import { SwipeableChannelPreview } from "@/components/SwipeableChannelPreview"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; // Custom Avatar Component for Channel List // Custom Title Component for bigger channel names const CustomChannelTitle = ({ channel }: { channel: Channel }) => { const channelName = channel?.data?.name || channel?.id || "Channel"; + const textColor = useThemeColor( + { light: "#1f2937", dark: "#f9fafb" }, + "text" + ); return ( - + {channelName} ); @@ -45,6 +54,54 @@ const CustomChannelPreview = ( export default function HomeScreen() { const user = useUser(); const { data: tablos, isLoading } = useTablosList(); + const colorScheme = useColorScheme(); + + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f8fafc", dark: "#111827" }, + "background" + ); + const textColor = useThemeColor( + { light: "#1f2937", dark: "#f9fafb" }, + "text" + ); + const subtitleColor = useThemeColor( + { light: "#6b7280", dark: "#9ca3af" }, + "text" + ); + const cardBackgroundColor = useThemeColor( + { light: "#ffffff", dark: "#1f2937" }, + "background" + ); + const borderColor = colorScheme === "dark" ? "#374151" : "#e5e7eb"; + const placeholderColor = useThemeColor( + { light: "#9ca3af", dark: "#6b7280" }, + "text" + ); + const iconColor = useThemeColor( + { light: "#6b7280", dark: "#9ca3af" }, + "text" + ); + const emptyTextColor = useThemeColor( + { light: "#4b5563", dark: "#d1d5db" }, + "text" + ); + const emptyIconColor = useThemeColor( + { light: "#d1d5db", dark: "#6b7280" }, + "text" + ); + + // Theme-aware gradient colors + const gradientColors: [string, string, string] = + colorScheme === "dark" + ? ["#1f2937", "#374151", "#4b5563"] + : ["#1e3a8a", "#3b82f6", "#60a5fa"]; + + const loadingColors = { + container: colorScheme === "dark" ? "#1f2937" : "#f8fafc", + item: colorScheme === "dark" ? "#374151" : "#e5e7eb", + itemSecondary: colorScheme === "dark" ? "#4b5563" : "#f3f4f6", + }; // const animations = useSharedValue>({}); @@ -179,7 +236,9 @@ export default function HomeScreen() { /> {/* Status indicator (online/active) */} - + ); }; @@ -289,12 +348,15 @@ export default function HomeScreen() { if (isLoading) { return ( - - + + {/* Loading Header */} {/* Loading Content */} - + {/* Loading Skeleton Items */} {[1, 2, 3, 4, 5].map((item) => ( - + - - + + ))} @@ -333,12 +415,15 @@ export default function HomeScreen() { } return ( - - + + {/* Beautiful Header */} {/* Channel List with animated search */} - + {/* */} ( - + {/* {debouncedSearchQuery ? "Recherche en cours..." : "Chargement..."} */} @@ -420,14 +507,18 @@ export default function HomeScreen() { // Show empty state when no results EmptyStateIndicator={() => ( - - + + {/* {debouncedSearchQuery ? "Aucun résultat" : "Aucune conversation"} */} Aucune conversation - + {/* {debouncedSearchQuery ? `Aucune conversation trouvée pour "${debouncedSearchQuery}"` : "Vous n'avez pas encore de conversations"} */} @@ -444,7 +535,7 @@ export default function HomeScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#f8fafc", + // backgroundColor is set dynamically }, headerGradient: { paddingTop: 50, @@ -575,7 +666,7 @@ const styles = StyleSheet.create({ }, channelListContainer: { flex: 1, - backgroundColor: "#f8fafc", + // backgroundColor is set dynamically marginTop: -10, borderTopLeftRadius: 10, borderTopRightRadius: 10, @@ -630,7 +721,7 @@ const styles = StyleSheet.create({ borderRadius: 8, backgroundColor: "#10b981", borderWidth: 3, - borderColor: "white", + // borderColor is set dynamically to match background shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, @@ -664,26 +755,26 @@ const styles = StyleSheet.create({ customChannelTitle: { fontSize: 18, fontWeight: "bold", - color: "#1f2937", + // color is set dynamically }, // Search Header Styles searchHeaderContainer: { - backgroundColor: "#f8fafc", + // backgroundColor is set dynamically borderBottomWidth: 1, - borderBottomColor: "#e5e7eb", + // borderBottomColor is set dynamically overflow: "hidden", paddingHorizontal: 20, }, searchInputContainer: { flexDirection: "row", alignItems: "center", - backgroundColor: "white", + // backgroundColor is set dynamically borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, borderWidth: 1, - borderColor: "#e5e7eb", + // borderColor is set dynamically marginTop: 15, }, searchIcon: { @@ -692,7 +783,7 @@ const styles = StyleSheet.create({ searchInput: { flex: 1, fontSize: 16, - color: "#374151", + // color is set dynamically paddingVertical: 0, fontWeight: "500", }, @@ -705,7 +796,7 @@ const styles = StyleSheet.create({ }, searchInfoText: { fontSize: 14, - color: "#6b7280", + // color is set dynamically }, searchLoadingContainer: { paddingVertical: 20, @@ -713,7 +804,7 @@ const styles = StyleSheet.create({ }, searchLoadingText: { fontSize: 16, - color: "#6b7280", + // color is set dynamically }, emptySearchContainer: { paddingVertical: 40, @@ -721,12 +812,12 @@ const styles = StyleSheet.create({ }, emptySearchTitle: { fontSize: 20, - color: "#4b5563", + // color is set dynamically marginTop: 10, }, emptySearchMessage: { fontSize: 16, - color: "#6b7280", + // color is set dynamically marginTop: 5, textAlign: "center", paddingHorizontal: 20, @@ -735,7 +826,7 @@ const styles = StyleSheet.create({ // Loading Skeleton Styles loadingContentContainer: { flex: 1, - backgroundColor: "#f8fafc", + // backgroundColor is set dynamically marginTop: -10, borderTopLeftRadius: 10, borderTopRightRadius: 10, @@ -752,7 +843,7 @@ const styles = StyleSheet.create({ width: 56, height: 56, borderRadius: 16, - backgroundColor: "#e5e7eb", + // backgroundColor is set dynamically marginRight: 12, }, loadingTextContainer: { @@ -760,14 +851,14 @@ const styles = StyleSheet.create({ }, loadingTitle: { height: 20, - backgroundColor: "#e5e7eb", + // backgroundColor is set dynamically borderRadius: 4, marginBottom: 8, width: "70%", }, loadingSubtitle: { height: 16, - backgroundColor: "#f3f4f6", + // backgroundColor is set dynamically borderRadius: 4, width: "50%", }, diff --git a/xtablo-expo/app/(app)/(tabs)/planning.tsx b/xtablo-expo/app/(app)/(tabs)/planning.tsx index dea8769..3c0abff 100644 --- a/xtablo-expo/app/(app)/(tabs)/planning.tsx +++ b/xtablo-expo/app/(app)/(tabs)/planning.tsx @@ -31,6 +31,8 @@ import { EventAndTablo, EventInsert } from "@/types/events.types"; import { useTablosList } from "@/hooks/tablos"; import { UserTablo } from "@/types/tablos.types"; import { ColorMap } from "@/constants/colors"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; type ViewMode = "month" | "week"; @@ -74,6 +76,33 @@ export default function PlanningScreen() { const { data: events } = useEventsByTablo(null); const { data: tablos } = useTablosList(); const createEvent = useCreateEvent(); + const colorScheme = useColorScheme(); + + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f8fafc", dark: "#111827" }, + "background" + ); + const cardBackgroundColor = useThemeColor( + { light: "#ffffff", dark: "#1f2937" }, + "background" + ); + const textColor = useThemeColor( + { light: "#1f2937", dark: "#f9fafb" }, + "text" + ); + const subtitleColor = useThemeColor( + { light: "#6b7280", dark: "#9ca3af" }, + "text" + ); + const borderColor = colorScheme === "dark" ? "#374151" : "#e5e7eb"; + const modalOverlayColor = + colorScheme === "dark" ? "rgba(0, 0, 0, 0.8)" : "rgba(0, 0, 0, 0.5)"; + const inactiveDayColor = colorScheme === "dark" ? "#4b5563" : "#d1d5db"; + const emptyIconColor = colorScheme === "dark" ? "#6b7280" : "#d1d5db"; + const viewModeToggleColor = colorScheme === "dark" ? "#374151" : "#f3f4f6"; + const weekHeaderBorderColor = colorScheme === "dark" ? "#374151" : "#e5e7eb"; + const selectedOptionBgColor = colorScheme === "dark" ? "#1e3a8a" : "#eff6ff"; const filteredEvents: EventAndTablo[] = (selectedTablo === null @@ -176,7 +205,7 @@ export default function PlanningScreen() { const renderTabloOption = ({ item }: { item: UserTablo }) => ( selectTablo(item)} > @@ -189,7 +218,9 @@ export default function PlanningScreen() { ]} /> - {item.name} + + {item.name} + {selectedTablo?.id === item.id && } @@ -197,22 +228,31 @@ export default function PlanningScreen() { ); const renderEvent = ({ item }: { item: EventAndTablo }) => ( - + - {item.title} + + {item.title} + - - + + {item.start_time?.substring(0, 5)} - - {item.tablo_name} + + + {item.tablo_name} + @@ -233,7 +273,8 @@ export default function PlanningScreen() { ))} {dayEvents.length > 6 && ( - - + + +{dayEvents.length - 6} @@ -318,14 +369,16 @@ export default function PlanningScreen() { return ( - + {selectedDate.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long", })} - + {selectedDayEvents.length} événement {selectedDayEvents.length > 1 ? "s" : ""} @@ -342,9 +395,11 @@ export default function PlanningScreen() { /> ) : ( - - Aucun événement - + + + Aucun événement + + Vous n'avez aucun événement prévu pour cette date. @@ -358,12 +413,17 @@ export default function PlanningScreen() { const todayEvents = getEventsForDate(selectedDate); return ( - + {/* Header */} - Planning - + + Planning + + {viewMode === "month" ? months[currentMonth.getMonth()] + " " + @@ -386,7 +446,12 @@ export default function PlanningScreen() { {/* View Mode Toggle */} - + @@ -416,11 +482,12 @@ export default function PlanningScreen() { > @@ -433,13 +500,20 @@ export default function PlanningScreen() { {/* Tablo Selector */} setShowTabloSelector(true)} > - +
- Tablo actuel + + Tablo actuel + - + {selectedTablo?.name ?? "Tous les tablos"} - + {/* Calendar/Week View */} - + @@ -473,7 +552,7 @@ export default function PlanningScreen() { > - + {viewMode === "month" ? `${ months[currentMonth.getMonth()] @@ -495,9 +574,17 @@ export default function PlanningScreen() { {viewMode === "month" ? ( <> - + {daysOfWeek.map((day, i) => ( - + {day} ))} @@ -524,7 +611,7 @@ export default function PlanningScreen() { - + Événements du jour ({todayEvents.length}) @@ -539,9 +626,11 @@ export default function PlanningScreen() { /> ) : ( - - Aucun événement - + + + Aucun événement + + Vous n'avez aucun événement prévu pour cette date. @@ -557,13 +646,22 @@ export default function PlanningScreen() { onRequestClose={() => setShowTabloSelector(false)} > setShowTabloSelector(false)} > - - - Choisir un tablo + + + + Choisir un tablo + selectTablo(null)} > @@ -580,12 +681,14 @@ export default function PlanningScreen() { style={[ styles.tabloColorDot, { - backgroundColor: "#6b7280", + backgroundColor: subtitleColor, }, ]} /> - + Tous les tablos @@ -606,15 +709,34 @@ export default function PlanningScreen() { animationType="slide" onRequestClose={() => setShowCreateEventModal(false)} > - - - - Nouvel événement + + + + + Nouvel événement + setShowCreateEventModal(false)} style={styles.createEventCloseButton} > - + @@ -624,57 +746,92 @@ export default function PlanningScreen() { > {/* Title Field */} - Titre * + + Titre * + setNewEvent({ ...newEvent, title: text }) } placeholder="Titre de l'événement" - placeholderTextColor="#9ca3af" + placeholderTextColor={subtitleColor} /> {/* Date Field */} - Date + + Date + setNewEvent({ ...newEvent, start_date: text }) } placeholder="YYYY-MM-DD" - placeholderTextColor="#9ca3af" + placeholderTextColor={subtitleColor} /> {/* Time Field */} - Heure + + Heure + setNewEvent({ ...newEvent, start_time: text }) } placeholder="HH:MM" - placeholderTextColor="#9ca3af" + placeholderTextColor={subtitleColor} /> {/* Tablo Selector */} - Tablo * + + Tablo * + ( setNewEvent({ ...newEvent, tablo_id: item.id }) @@ -693,6 +850,12 @@ export default function PlanningScreen() { item.id} showsVerticalScrollIndicator={false} scrollEnabled={false} - style={styles.tabloListInForm} /> - + setShowCreateEventModal(false)} > - Annuler + + Annuler + state.signOut); const user = useUser(); + const colorScheme = useColorScheme(); + + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f8fafc", dark: "#111827" }, + "background" + ); + const cardBackgroundColor = useThemeColor( + { light: "#ffffff", dark: "#1f2937" }, + "background" + ); + const textColor = useThemeColor( + { light: "#1f2937", dark: "#f9fafb" }, + "text" + ); + const subtitleColor = useThemeColor( + { light: "#6b7280", dark: "#9ca3af" }, + "text" + ); + const borderColor = colorScheme === "dark" ? "#374151" : "#e5e7eb"; + + // Theme-aware gradient colors + const gradientColors: [string, string, string] = + colorScheme === "dark" + ? ["#1f2937", "#374151", "#4b5563"] + : ["#1e3a8a", "#3b82f6", "#60a5fa"]; // Settings state const [pushNotifications, setPushNotifications] = useState(true); const [emailNotifications, setEmailNotifications] = useState(true); - const [darkMode, setDarkMode] = useState(false); const [biometricAuth, setBiometricAuth] = useState(false); const handleSignOut = () => { @@ -72,8 +99,17 @@ export default function SettingsScreen() { const renderSettingsSection = (title: string, children: React.ReactNode) => ( - {title} - {children} + + {title} + + + {children} + ); @@ -86,7 +122,7 @@ export default function SettingsScreen() { showArrow: boolean = true ) => ( {icon} - {title} + + {title} + {subtitle && ( - {subtitle} + + {subtitle} + )} {rightComponent} {showArrow && onPress && ( - + )} @@ -124,20 +170,26 @@ export default function SettingsScreen() { , false ); return ( - - + + {/* Header */} - + {/* Account Section */} {renderSettingsSection( "Compte", @@ -198,9 +253,14 @@ export default function SettingsScreen() { {renderSwitchItem( , "Mode sombre", - "Utiliser le thème sombre", - darkMode, - setDarkMode + "Thème système automatique", + colorScheme === "dark", + () => { + Alert.alert( + "Thème automatique", + "Le thème suit automatiquement les préférences de votre appareil. Modifiez le thème dans les paramètres de votre appareil." + ); + } )} )} @@ -310,7 +370,6 @@ export default function SettingsScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#f8fafc", }, headerGradient: { paddingTop: 50, @@ -353,7 +412,6 @@ const styles = StyleSheet.create({ }, content: { flex: 1, - backgroundColor: "#f8fafc", marginTop: -10, borderTopLeftRadius: 10, borderTopRightRadius: 10, @@ -366,12 +424,10 @@ const styles = StyleSheet.create({ sectionTitle: { fontSize: 18, fontWeight: "600", - color: "#1f2937", marginBottom: 12, marginLeft: 4, }, sectionContent: { - backgroundColor: "white", borderRadius: 16, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, @@ -379,6 +435,7 @@ const styles = StyleSheet.create({ shadowRadius: 8, elevation: 3, overflow: "hidden", + borderWidth: 1, }, settingsItem: { flexDirection: "row", @@ -387,7 +444,6 @@ const styles = StyleSheet.create({ paddingHorizontal: 20, paddingVertical: 16, borderBottomWidth: 1, - borderBottomColor: "#f3f4f6", }, settingsItemLeft: { flexDirection: "row", @@ -398,7 +454,7 @@ const styles = StyleSheet.create({ width: 40, height: 40, borderRadius: 20, - backgroundColor: "#f3f4f6", + backgroundColor: "rgba(0, 0, 0, 0.05)", justifyContent: "center", alignItems: "center", marginRight: 12, @@ -409,12 +465,10 @@ const styles = StyleSheet.create({ settingsItemTitle: { fontSize: 16, fontWeight: "500", - color: "#1f2937", marginBottom: 2, }, settingsItemSubtitle: { fontSize: 14, - color: "#6b7280", }, settingsItemRight: { flexDirection: "row", diff --git a/xtablo-expo/app/(app)/(tabs)/tablos.tsx b/xtablo-expo/app/(app)/(tabs)/tablos.tsx index 8234b4b..e4135c2 100644 --- a/xtablo-expo/app/(app)/(tabs)/tablos.tsx +++ b/xtablo-expo/app/(app)/(tabs)/tablos.tsx @@ -30,6 +30,8 @@ import { } from "lucide-react-native"; import { router } from "expo-router"; import { AVAILABLE_COLORS, ColorMap } from "@/constants/colors"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; const { width } = Dimensions.get("window"); const numColumns = 2; @@ -46,6 +48,32 @@ export default function TablosScreen() { const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); const { mutate: createTablo } = useCreateTablo(); const { mutate: deleteTablo } = useDeleteTablo(); + const colorScheme = useColorScheme(); + + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f8fafc", dark: "#111827" }, + "background" + ); + const cardBackgroundColor = useThemeColor( + { light: "#ffffff", dark: "#1f2937" }, + "background" + ); + const textColor = useThemeColor( + { light: "#1f2937", dark: "#f9fafb" }, + "text" + ); + const subtitleColor = useThemeColor( + { light: "#6b7280", dark: "#9ca3af" }, + "text" + ); + const borderColor = colorScheme === "dark" ? "#374151" : "#e5e7eb"; + + // Theme-aware gradient colors + const gradientColors: [string, string, string] = + colorScheme === "dark" + ? ["#1f2937", "#374151", "#4b5563"] + : ["#1e3a8a", "#3b82f6", "#60a5fa"]; const [newTablo, setNewTablo] = useState<{ name: string; @@ -190,7 +218,11 @@ export default function TablosScreen() { navigateToTablo(tablo)} onLongPress={() => handleDeleteTablo(tablo)} @@ -216,7 +248,10 @@ export default function TablosScreen() { {/* Tablo Info */} - + {tablo.name} - + router.push("/planning")} > - + @@ -275,7 +310,10 @@ export default function TablosScreen() { const renderListItem = ({ item: tablo }: { item: UserTablo }) => { return ( navigateToTablo(tablo)} onLongPress={() => handleDeleteTablo(tablo)} activeOpacity={0.8} @@ -294,7 +332,9 @@ export default function TablosScreen() { - {tablo.name} + + {tablo.name} + - + router.push("/planning")} > - + @@ -369,12 +409,15 @@ export default function TablosScreen() { } return ( - - + + {/* Beautiful Header */} {/* Content */} - + {isLoading && !refreshing ? ( - Chargement de vos tablos... + + Chargement de vos tablos... + ) : filteredTablos && filteredTablos.length > 0 ? ( ) : ( - Aucun tablo trouvé - + + Aucun tablo trouvé + + {filterStatus === "all" ? "Vous n'avez encore aucun tablo. Créez votre premier tablo pour commencer !" : `Aucun tablo avec le statut "${getStatusLabel( @@ -485,20 +532,37 @@ export default function TablosScreen() { behavior={Platform.OS === "ios" ? "padding" : "height"} style={styles.modalOverlay} > - + setIsCreateModalVisible(false)} > - + - Nouveau Tablo + + Nouveau Tablo + - Nom du Tablo + + Nom du Tablo + setNewTablo({ ...newTablo, name: text }) @@ -506,7 +570,9 @@ export default function TablosScreen() { /> - Couleur + + Couleur + {AVAILABLE_COLORS.map((color) => ( - Statut + + Statut + {(["todo", "in_progress", "done"] as const).map((status) => ( (); @@ -25,9 +27,32 @@ export default function ChannelScreen() { const channel = client.channel(type, id); const [hasMessages, setHasMessages] = useState(false); const [isLoading, setIsLoading] = useState(true); + const colorScheme = useColorScheme(); const headerHeight = useHeaderHeight(); + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f8fafc", dark: "#111827" }, + "background" + ); + const textColor = useThemeColor( + { light: "#1f2937", dark: "#f9fafb" }, + "text" + ); + const subtitleColor = useThemeColor( + { light: "#6b7280", dark: "#9ca3af" }, + "text" + ); + const iconColor = useThemeColor( + { light: "#d1d5db", dark: "#6b7280" }, + "icon" + ); + const iconSecondaryColor = useThemeColor( + { light: "#e5e7eb", dark: "#4b5563" }, + "icon" + ); + useEffect(() => { if (channel) { const checkMessages = async () => { @@ -64,31 +89,35 @@ export default function ChannelScreen() { const EmptyState = () => ( - + - + - + - Commencez la conversation - + + Commencez la conversation + + Soyez le premier à envoyer un message dans ce canal ! ); return ( - + {isLoading ? ( - Chargement des messages... + + Chargement des messages... + ) : hasMessages ? ( @@ -112,12 +141,10 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "center", alignItems: "center", - backgroundColor: "#f8fafc", gap: 16, }, loadingText: { fontSize: 16, - color: "#6b7280", fontWeight: "500", }, emptyContainer: { @@ -125,7 +152,6 @@ const styles = StyleSheet.create({ justifyContent: "center", alignItems: "center", paddingHorizontal: 40, - backgroundColor: "#f8fafc", }, emptyIconContainer: { position: "relative", @@ -144,7 +170,7 @@ const styles = StyleSheet.create({ position: "absolute", top: 10, right: 5, - backgroundColor: "white", + backgroundColor: "rgba(255, 255, 255, 0.9)", borderRadius: 15, padding: 6, shadowColor: "#000", @@ -157,7 +183,7 @@ const styles = StyleSheet.create({ position: "absolute", bottom: 15, left: 8, - backgroundColor: "white", + backgroundColor: "rgba(255, 255, 255, 0.9)", borderRadius: 12, padding: 5, shadowColor: "#000", @@ -169,24 +195,22 @@ const styles = StyleSheet.create({ emptyTitle: { fontSize: 24, fontWeight: "bold", - color: "#1f2937", marginBottom: 12, textAlign: "center", }, emptyMessage: { fontSize: 16, - color: "#6b7280", textAlign: "center", lineHeight: 24, marginBottom: 32, }, emptyHint: { - backgroundColor: "white", + backgroundColor: "rgba(255, 255, 255, 0.9)", paddingHorizontal: 20, paddingVertical: 12, borderRadius: 20, borderWidth: 1, - borderColor: "#e5e7eb", + borderColor: "rgba(0, 0, 0, 0.1)", shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, @@ -195,14 +219,10 @@ const styles = StyleSheet.create({ }, emptyHintText: { fontSize: 14, - color: "#9ca3af", fontWeight: "500", textAlign: "center", }, keyboardContainer: { - backgroundColor: "#f8fafc", - }, - messageInputContainer: { - backgroundColor: "#f8fafc", + flexShrink: 0, }, }); diff --git a/xtablo-expo/app/(app)/user/profile.tsx b/xtablo-expo/app/(app)/user/profile.tsx index ee0183c..cb64a84 100644 --- a/xtablo-expo/app/(app)/user/profile.tsx +++ b/xtablo-expo/app/(app)/user/profile.tsx @@ -20,6 +20,8 @@ import { Settings, Shield, } from "lucide-react-native"; +import { Stack } from "expo-router"; +import { SafeAreaView } from "react-native-safe-area-context"; export default function ProfileScreen() { const signOut = useAuthStore((state) => state.signOut); @@ -49,132 +51,140 @@ export default function ProfileScreen() { ]; return ( - - - - - {user.name || "Utilisateur"} - {user.email} - - - - {/* Contenu principal */} - - {/* Carte d'informations personnelles */} - - - - - Informations personnelles - + + + + + + + {user.name || "Utilisateur"} + {user.email} + - - - + {/* Contenu principal */} + + {/* Carte d'informations personnelles */} + + + + + Informations personnelles + - - Adresse e-mail - {user.email} + + + + + + + Adresse e-mail + {user.email} + - - + - - - - - - Nom d'affichage - {isEditing ? ( - - ) : ( - - {user.name || "Non défini"} - - )} - - { - if (isEditing) { - handleSaveDisplayName(); - } else { - setDisplayName(user.name ?? ""); - setIsEditing(true); - } - }} - > - {isEditing ? ( - - ) : ( - - )} - - - - - {/* Éléments de menu */} - - - - Préférences - - - {menuItems.map((item, index) => ( - - - - - - - {item.title} - {item.subtitle} - - + + + + + + Nom d'affichage + {isEditing ? ( + + ) : ( + + {user.name || "Non défini"} + + )} + + { + if (isEditing) { + handleSaveDisplayName(); + } else { + setDisplayName(user.name ?? ""); + setIsEditing(true); + } + }} + > + {isEditing ? ( + + ) : ( + + )} - {index < menuItems.length - 1 && } - ))} - + - {/* Bouton de déconnexion */} - - - - Se déconnecter - - - - + {/* Éléments de menu */} + + + + Préférences + + + {menuItems.map((item, index) => ( + + + + + + + {item.title} + {item.subtitle} + + + + {index < menuItems.length - 1 && ( + + )} + + ))} + + + {/* Bouton de déconnexion */} + + + + Se déconnecter + + + + + ); } diff --git a/xtablo-expo/app/login.tsx b/xtablo-expo/app/login.tsx index 7d9f699..653a22e 100644 --- a/xtablo-expo/app/login.tsx +++ b/xtablo-expo/app/login.tsx @@ -6,6 +6,8 @@ import { Link } from "expo-router"; import { Mail, Lock } from "lucide-react-native"; import { GoogleLoginButton } from "@/components/GoogleLoginButton"; import { AppleLoginButton } from "@/components/AppleLoginButton"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; export default function Auth() { const [email, setEmail] = useState(""); @@ -14,12 +16,34 @@ export default function Auth() { const login = useAuthStore((state) => state.login); const authLoading = useAuthStore((state) => state.loading); const performOAuth = useAuthStore((state) => state.performOAuth); + const colorScheme = useColorScheme(); + + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f5f5f5", dark: "#111827" }, + "background" + ); + const textColor = useThemeColor({ light: "#333", dark: "#f9fafb" }, "text"); + const subtitleColor = useThemeColor( + { light: "#666", dark: "#9ca3af" }, + "text" + ); + const separatorColor = useThemeColor( + { light: "#ddd", dark: "#374151" }, + "text" + ); + const linkColor = useThemeColor( + { light: "#3b82f6", dark: "#60a5fa" }, + "text" + ); return ( - + - Connexion XTablo - Connectez-vous à votre compte + Connexion XTablo + + Connectez-vous à votre compte + - - ou - + + ou + performOAuth("google")} /> @@ -62,8 +86,10 @@ export default function Auth() { performOAuth("apple")} /> - Pas encore de compte ? - + + Pas encore de compte ?{" "} + + S'inscrire @@ -76,7 +102,6 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "center", padding: 16, - backgroundColor: "#f5f5f5", // Light grey background }, logo: { width: 80, @@ -90,13 +115,11 @@ const styles = StyleSheet.create({ fontWeight: "bold", textAlign: "center", marginBottom: 3, - color: "#333", }, subtitle: { fontSize: 14, textAlign: "center", marginBottom: 16, - color: "#666", }, verticallySpaced: { paddingTop: 2, @@ -132,11 +155,9 @@ const styles = StyleSheet.create({ separator: { flex: 1, height: 1, - backgroundColor: "#ddd", }, separatorText: { marginHorizontal: 15, - color: "#666", fontSize: 14, }, linkContainer: { @@ -144,11 +165,8 @@ const styles = StyleSheet.create({ justifyContent: "center", marginTop: 12, }, - linkText: { - color: "#666", - }, + linkText: {}, link: { - color: "#007bff", fontWeight: "bold", }, }); diff --git a/xtablo-expo/app/signup.tsx b/xtablo-expo/app/signup.tsx index b59ecc2..55b54bf 100644 --- a/xtablo-expo/app/signup.tsx +++ b/xtablo-expo/app/signup.tsx @@ -4,6 +4,7 @@ import { Button, Input } from "@rn-vui/themed"; import { useAuthStore } from "@/stores/auth"; import { Link } from "expo-router"; import { Mail, Lock, User, Building2 } from "lucide-react-native"; +import { useThemeColor } from "@/hooks/useThemeColor"; export default function SignUp() { const [firstName, setFirstName] = useState(""); @@ -15,11 +16,30 @@ export default function SignUp() { const signUp = useAuthStore((state) => state.signUp); const authLoading = useAuthStore((state) => state.loading); + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f5f5f5", dark: "#111827" }, + "background" + ); + const textColor = useThemeColor({ light: "#333", dark: "#f9fafb" }, "text"); + const subtitleColor = useThemeColor( + { light: "#666", dark: "#9ca3af" }, + "text" + ); + const linkColor = useThemeColor( + { light: "#3b82f6", dark: "#60a5fa" }, + "text" + ); + return ( - + - Créer un compte XTablo - Rejoignez-nous ! + + Créer un compte XTablo + + + Rejoignez-nous ! + - Vous avez déjà un compte ? - + + Vous avez déjà un compte ?{" "} + + Se connecter @@ -99,7 +121,6 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "center", padding: 16, - backgroundColor: "#f5f5f5", // Light grey background }, logo: { width: 80, @@ -113,13 +134,11 @@ const styles = StyleSheet.create({ fontWeight: "bold", textAlign: "center", marginBottom: 3, - color: "#333", }, subtitle: { fontSize: 14, textAlign: "center", marginBottom: 16, - color: "#666", }, verticallySpaced: { paddingTop: 2, @@ -167,11 +186,8 @@ const styles = StyleSheet.create({ justifyContent: "center", marginTop: 12, }, - linkText: { - color: "#666", - }, + linkText: {}, link: { - color: "#007bff", fontWeight: "bold", }, }); diff --git a/xtablo-expo/components/LoadingView.tsx b/xtablo-expo/components/LoadingView.tsx index 07a76ab..2d47d22 100644 --- a/xtablo-expo/components/LoadingView.tsx +++ b/xtablo-expo/components/LoadingView.tsx @@ -9,11 +9,26 @@ import Animated, { } from "react-native-reanimated"; import { ThemedView } from "@/components/ThemedView"; import { ThemedText } from "@/components/ThemedText"; +import { useThemeColor } from "@/hooks/useThemeColor"; export const LoadingView = () => { const rotation = useSharedValue(0); const width = useSharedValue(80); + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f8fafc", dark: "#111827" }, + "background" + ); + const textColor = useThemeColor( + { light: "#1f2937", dark: "#f9fafb" }, + "text" + ); + const subtitleColor = useThemeColor( + { light: "#6b7280", dark: "#9ca3af" }, + "text" + ); + rotation.value = withRepeat( withTiming(360, { easing: Easing.linear, duration: 2000 }), -1, @@ -37,7 +52,7 @@ export const LoadingView = () => { }); return ( - + { style={[styles.logo, animatedStyle]} /> - + XTablo - + Initialisation de l'application... @@ -61,7 +79,6 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "center", alignItems: "center", - backgroundColor: "#f8fafc", }, loadingContent: { alignItems: "center", @@ -86,13 +103,11 @@ const styles = StyleSheet.create({ fontWeight: "bold", textAlign: "center", marginBottom: 8, - color: "#1f2937", }, subtitle: { fontSize: 16, textAlign: "center", marginBottom: 32, - color: "#6b7280", opacity: 0.8, }, }); diff --git a/xtablo-expo/components/SwipeableChannelPreview.tsx b/xtablo-expo/components/SwipeableChannelPreview.tsx index 3b78baf..90af0b4 100644 --- a/xtablo-expo/components/SwipeableChannelPreview.tsx +++ b/xtablo-expo/components/SwipeableChannelPreview.tsx @@ -19,6 +19,8 @@ import Animated, { import { Archive } from "lucide-react-native"; import { Channel } from "stream-chat"; import { DefaultStreamChatGenerics } from "stream-chat-expo"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; interface SwipeableChannelPreviewProps { channel: Channel; @@ -32,6 +34,19 @@ export const SwipeableChannelPreview: React.FC< SwipeableChannelPreviewProps > = ({ channel, children }) => { const translateX = useSharedValue(0); + const colorScheme = useColorScheme(); + + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#ffffff", dark: "#1f1f1f" }, + "background" + ); + const textColor = useThemeColor( + { light: "#ffffff", dark: "#ffffff" }, + "text" + ); + const archiveButtonColor = colorScheme === "dark" ? "#0f4a3c" : "#166534"; + const iconColor = "#ffffff"; const handleArchiveChannel = async () => { try { @@ -136,17 +151,31 @@ export const SwipeableChannelPreview: React.FC< {/* Right Actions Background */} - + - - Archiver + + + Archiver + {/* Channel Content */} - + {children} @@ -160,7 +189,6 @@ const styles = StyleSheet.create({ position: "relative", }, channelContainer: { - backgroundColor: "white", flex: 1, }, rightActionsContainer: { @@ -173,7 +201,6 @@ const styles = StyleSheet.create({ alignItems: "center", }, archiveButton: { - backgroundColor: "#166534", // Dark green color for archive justifyContent: "center", alignItems: "center", width: ACTION_WIDTH, @@ -186,7 +213,6 @@ const styles = StyleSheet.create({ gap: 4, }, actionText: { - color: "white", fontSize: 12, fontWeight: "600", textAlign: "center", diff --git a/xtablo-expo/providers/ChatProvider.tsx b/xtablo-expo/providers/ChatProvider.tsx index f9f1ee7..f4eb9bf 100644 --- a/xtablo-expo/providers/ChatProvider.tsx +++ b/xtablo-expo/providers/ChatProvider.tsx @@ -2,6 +2,10 @@ import { ActivityIndicator, View } from "react-native"; import { Chat, OverlayProvider, useCreateChatClient } from "stream-chat-expo"; import { useUser } from "@/providers/UserProvider"; import { Text } from "react-native"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; +import { useMemo } from "react"; +import type { DeepPartial, Theme } from "stream-chat-expo"; export default function ChatProvider({ children, @@ -9,6 +13,92 @@ export default function ChatProvider({ children: React.ReactNode; }) { const user = useUser(); + const colorScheme = useColorScheme(); + + // Theme-aware colors + const backgroundColor = useThemeColor( + { light: "#f8fafc", dark: "#111827" }, + "background" + ); + const textColor = useThemeColor( + { light: "#1f2937", dark: "#f9fafb" }, + "text" + ); + + // Aggressive Stream Chat theme to force correct backgrounds + const streamChatTheme: DeepPartial = useMemo( + () => ({ + colors: { + // Force ALL background-related colors to match page background + white: backgroundColor, + white_snow: backgroundColor, + white_smoke: backgroundColor, + grey_whisper: backgroundColor, + // Keep text colors appropriate + black: colorScheme === "dark" ? "#f9fafb" : "#1f2937", + grey: colorScheme === "dark" ? "#9ca3af" : "#6b7280", + grey_gainsboro: colorScheme === "dark" ? "#4b5563" : "#9ca3af", + + // Accent colors + accent_blue: "#3b82f6", + accent_green: "#10b981", + accent_red: "#ef4444", + + // Overlays + overlay: + colorScheme === "dark" ? "rgba(0, 0, 0, 0.8)" : "rgba(0, 0, 0, 0.3)", + // Border colors + border: colorScheme === "dark" ? "#374151" : "#e5e7eb", + }, + + // Force channel list backgrounds + channelListMessenger: { + flatList: { + backgroundColor: backgroundColor, + }, + flatListContent: { + backgroundColor: backgroundColor, + }, + container: { + backgroundColor: backgroundColor, + }, + }, + + // Force channel preview backgrounds + channelPreview: { + container: { + backgroundColor: backgroundColor, + }, + title: { + color: colorScheme === "dark" ? "#f9fafb" : "#1f2937", + }, + message: { + color: colorScheme === "dark" ? "#9ca3af" : "#6b7280", + }, + date: { + color: colorScheme === "dark" ? "#6b7280" : "#9ca3af", + }, + }, + + // Force message backgrounds + messageSimple: { + content: { + container: { + backgroundColor: backgroundColor, + }, + }, + }, + + // Force input backgrounds + messageInput: { + container: { + backgroundColor: backgroundColor, + borderTopColor: colorScheme === "dark" ? "#374151" : "#e5e7eb", + }, + }, + }), + [colorScheme, backgroundColor] + ); const client = useCreateChatClient({ apiKey: process.env.EXPO_PUBLIC_STREAM_CHAT_API_KEY as string, @@ -22,8 +112,15 @@ export default function ChatProvider({ if (!user.streamToken) { return ( - - Chat Indisponible + + Chat Indisponible ); } @@ -33,7 +130,7 @@ export default function ChatProvider({ } return ( - + {children} ); From c310a351e4eb915cb8d807165c973978e9fa3e6a Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Tue, 22 Jul 2025 07:52:06 +0200 Subject: [PATCH 10/16] Fix npm and add stuff to app --- api/src/tablo.ts | 22 + justfile | 4 +- xtablo-expo/app.json | 2 +- xtablo-expo/app/(app)/(tabs)/_layout.tsx | 2 +- xtablo-expo/app/(app)/(tabs)/settings.tsx | 27 +- .../app/(app)/user/{profile.tsx => index.tsx} | 75 +- xtablo-expo/app/_layout.tsx | 2 - xtablo-expo/app/login.tsx | 17 +- xtablo-expo/app/signup.tsx | 9 +- xtablo-expo/assets/images/logo_white.png | Bin 0 -> 45273 bytes xtablo-expo/components/LoadingView.tsx | 12 +- .../components/SwipeableChannelPreview.tsx | 175 +++-- xtablo-expo/hooks/user.ts | 6 +- xtablo-expo/package-lock.json | 713 +++++++++++++++--- xtablo-expo/package.json | 2 - xtablo-expo/providers/UserProvider.tsx | 9 +- xtablo-expo/types/user.types.ts | 9 + 17 files changed, 768 insertions(+), 318 deletions(-) rename xtablo-expo/app/(app)/user/{profile.tsx => index.tsx} (78%) create mode 100644 xtablo-expo/assets/images/logo_white.png create mode 100644 xtablo-expo/types/user.types.ts diff --git a/api/src/tablo.ts b/api/src/tablo.ts index 23da5a7..ecc6e82 100644 --- a/api/src/tablo.ts +++ b/api/src/tablo.ts @@ -275,3 +275,25 @@ tabloRouter.get("/members/:tablo_id", async (c) => { })), }); }); + +tabloRouter.post("/leave", async (c) => { + const user = c.get("user"); + const supabase = c.get("supabase"); + const streamServerClient = c.get("streamServerClient"); + const { tablo_id } = await c.req.json(); + + const channel = streamServerClient.channel("messaging", tablo_id); + await channel.removeMembers([user.id]); + + const { error } = await supabase + .from("tablo_access") + .update({ is_active: false }) + .eq("tablo_id", tablo_id) + .eq("user_id", user.id); + + if (error) { + return c.json({ error: error.message }, 500); + } + + return c.json({ message: "Tablo left successfully" }); +}); diff --git a/justfile b/justfile index c2f1ad1..d557c7b 100644 --- a/justfile +++ b/justfile @@ -23,10 +23,10 @@ update-types: npx supabase gen types typescript --project-id "mhcafqvzbrrwvahpvvzd" --schema public > ui/src/types/database.types.ts && cp ui/src/types/database.types.ts api/src/database.types.ts && cp ui/src/types/database.types.ts xtablo-expo/lib/database.types.ts expo-install-all: - cd xtablo-expo && npx expo install -- --legacy-peer-deps + cd xtablo-expo && npx expo install expo-install +package: - cd xtablo-expo && npx expo install {{package}} -- --legacy-peer-deps + cd xtablo-expo && npx expo install {{package}} expo-start *args: cd xtablo-expo && npx expo start {{args}} diff --git a/xtablo-expo/app.json b/xtablo-expo/app.json index daca2a0..a16d24b 100644 --- a/xtablo-expo/app.json +++ b/xtablo-expo/app.json @@ -1,7 +1,7 @@ { "expo": { "name": "xtablo", - "slug": "xtablo", + "slug": "xtablo-expo", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", diff --git a/xtablo-expo/app/(app)/(tabs)/_layout.tsx b/xtablo-expo/app/(app)/(tabs)/_layout.tsx index 5fa0f1d..c5b472f 100644 --- a/xtablo-expo/app/(app)/(tabs)/_layout.tsx +++ b/xtablo-expo/app/(app)/(tabs)/_layout.tsx @@ -75,7 +75,7 @@ export default function TabLayout() { /> ), tabBarLabel: "Discussions", - // tabBarBadge: 10, TODO: Add badge for notifications + // tabBarBadge: 10, // TODO: Add badge for notifications }} /> - {/* Account Section */} {renderSettingsSection( "Compte", <> @@ -218,15 +217,14 @@ export default function SettingsScreen() { , "Profil utilisateur", `${user.name || "Non défini"} • ${user.email}`, - () => router.push("/user/profile"), + () => router.push("/user"), undefined, true )} )} - {/* Notifications Section */} - {renderSettingsSection( + {/* {renderSettingsSection( "Notifications", <> {renderSwitchItem( @@ -244,7 +242,7 @@ export default function SettingsScreen() { setEmailNotifications )} - )} + )} */} {/* Appearance Section */} {renderSettingsSection( @@ -265,8 +263,7 @@ export default function SettingsScreen() { )} - {/* Security Section */} - {renderSettingsSection( + {/* {renderSettingsSection( "Sécurité et confidentialité", <> {renderSwitchItem( @@ -285,20 +282,20 @@ export default function SettingsScreen() { true )} - )} + )} */} {/* Help & Support Section */} {renderSettingsSection( "Aide et support", <> - {renderSettingsItem( + {/* {renderSettingsItem( , "Centre d'aide", "FAQ et guides d'utilisation", () => Linking.openURL("https://xtablo.com/help"), undefined, true - )} + )} */} {renderSettingsItem( , "Contacter le support", @@ -307,14 +304,14 @@ export default function SettingsScreen() { undefined, true )} - {renderSettingsItem( + {/* {renderSettingsItem( , "Évaluer l'application", "Aidez-nous à améliorer XTablo", handleRateApp, undefined, true - )} + )} */} )} @@ -330,14 +327,14 @@ export default function SettingsScreen() { undefined, false )} - {renderSettingsItem( + {/* {renderSettingsItem( , "Site web", "Visitez notre site web", - () => Linking.openURL("https://xtablo.com"), + () => Linking.openURL("https://app.xtablo.com"), undefined, true - )} + )} */} )} diff --git a/xtablo-expo/app/(app)/user/profile.tsx b/xtablo-expo/app/(app)/user/index.tsx similarity index 78% rename from xtablo-expo/app/(app)/user/profile.tsx rename to xtablo-expo/app/(app)/user/index.tsx index cb64a84..bb72e5b 100644 --- a/xtablo-expo/app/(app)/user/profile.tsx +++ b/xtablo-expo/app/(app)/user/index.tsx @@ -21,42 +21,28 @@ import { Shield, } from "lucide-react-native"; import { Stack } from "expo-router"; -import { SafeAreaView } from "react-native-safe-area-context"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function ProfileScreen() { const signOut = useAuthStore((state) => state.signOut); const user = useUser(); + const insets = useSafeAreaInsets(); - const [displayName, setDisplayName] = useState(user.name || ""); - const [isEditing, setIsEditing] = useState(false); + // 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é"), - }, - ]; + // const handleSaveDisplayName = () => { + // // TODO: Implémenter la fonctionnalité de sauvegarde + // setIsEditing(false); + // }; return ( - + @@ -106,7 +92,8 @@ export default function ProfileScreen() { Nom d'affichage - {isEditing ? ( + {user.name} + {/* {isEditing ? ( {user.name || "Non défini"} - )} + )} */} - { if (isEditing) { @@ -137,39 +124,10 @@ export default function ProfileScreen() { ) : ( )} - + */} - {/* Éléments de menu */} - - - - Préférences - - - {menuItems.map((item, index) => ( - - - - - - - {item.title} - {item.subtitle} - - - - {index < menuItems.length - 1 && ( - - )} - - ))} - - {/* Bouton de déconnexion */} - + ); } @@ -194,7 +152,6 @@ const styles = StyleSheet.create({ backgroundColor: "#f8fafc", }, headerGradient: { - paddingTop: 60, paddingBottom: 40, paddingHorizontal: 20, }, diff --git a/xtablo-expo/app/_layout.tsx b/xtablo-expo/app/_layout.tsx index de44150..c22cea8 100644 --- a/xtablo-expo/app/_layout.tsx +++ b/xtablo-expo/app/_layout.tsx @@ -59,8 +59,6 @@ const RootNavigator = () => { return ; } - console.log("isLoggedIn", isLoggedIn); - return ( diff --git a/xtablo-expo/app/login.tsx b/xtablo-expo/app/login.tsx index 653a22e..22a9e3d 100644 --- a/xtablo-expo/app/login.tsx +++ b/xtablo-expo/app/login.tsx @@ -1,5 +1,11 @@ import React, { useState } from "react"; -import { StyleSheet, View, Text, Image } from "react-native"; +import { + StyleSheet, + View, + Text, + Image, + ImageSourcePropType, +} from "react-native"; import { Button, Input } from "@rn-vui/themed"; import { useAuthStore } from "@/stores/auth"; import { Link } from "expo-router"; @@ -16,7 +22,6 @@ export default function Auth() { const login = useAuthStore((state) => state.login); const authLoading = useAuthStore((state) => state.loading); const performOAuth = useAuthStore((state) => state.performOAuth); - const colorScheme = useColorScheme(); // Theme-aware colors const backgroundColor = useThemeColor( @@ -37,9 +42,15 @@ export default function Auth() { "text" ); + const dark = useColorScheme() === "dark"; + + const logo = dark + ? require("@/assets/images/logo_white.png") + : require("@/assets/images/logo.png"); + return ( - + Connexion XTablo Connectez-vous à votre compte diff --git a/xtablo-expo/app/signup.tsx b/xtablo-expo/app/signup.tsx index 55b54bf..a99dc55 100644 --- a/xtablo-expo/app/signup.tsx +++ b/xtablo-expo/app/signup.tsx @@ -5,6 +5,7 @@ import { useAuthStore } from "@/stores/auth"; import { Link } from "expo-router"; import { Mail, Lock, User, Building2 } from "lucide-react-native"; import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; export default function SignUp() { const [firstName, setFirstName] = useState(""); @@ -31,9 +32,15 @@ export default function SignUp() { "text" ); + const dark = useColorScheme() === "dark"; + + const logo = dark + ? require("@/assets/images/logo_white.png") + : require("@/assets/images/logo.png"); + return ( - + Créer un compte XTablo diff --git a/xtablo-expo/assets/images/logo_white.png b/xtablo-expo/assets/images/logo_white.png new file mode 100644 index 0000000000000000000000000000000000000000..4a474a3e8199fc852d7e51aef594b5236efa4736 GIT binary patch literal 45273 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%0AdYh8oeoY>QIVdV-8F^V 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 { const rotation = useSharedValue(0); - const width = useSharedValue(80); // Theme-aware colors const backgroundColor = useThemeColor( @@ -34,12 +33,6 @@ export const LoadingView = () => { -1, false ); - width.value = withRepeat( - withTiming(100, { easing: Easing.linear, duration: 2000 }), - -1, - true - ); - const rotationDeg = useDerivedValue(() => { return `${rotation.value}deg`; }); @@ -47,7 +40,6 @@ export const LoadingView = () => { const animatedStyle = useAnimatedStyle(() => { return { transform: [{ rotate: rotationDeg.value }], - width: width.value, }; }); @@ -90,8 +82,8 @@ const styles = StyleSheet.create({ height: 130, }, logo: { - width: 80, - height: 80, + width: 100, + height: 100, shadowColor: "#000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, diff --git a/xtablo-expo/components/SwipeableChannelPreview.tsx b/xtablo-expo/components/SwipeableChannelPreview.tsx index 90af0b4..b5b19ad 100644 --- a/xtablo-expo/components/SwipeableChannelPreview.tsx +++ b/xtablo-expo/components/SwipeableChannelPreview.tsx @@ -16,7 +16,7 @@ import Animated, { useDerivedValue, SharedValue, } from "react-native-reanimated"; -import { Archive } from "lucide-react-native"; +import { Archive, Trash } from "lucide-react-native"; import { Channel } from "stream-chat"; import { DefaultStreamChatGenerics } from "stream-chat-expo"; import { useThemeColor } from "@/hooks/useThemeColor"; @@ -45,76 +45,76 @@ export const SwipeableChannelPreview: React.FC< { light: "#ffffff", dark: "#ffffff" }, "text" ); - const archiveButtonColor = colorScheme === "dark" ? "#0f4a3c" : "#166534"; + const deleteButtonColor = colorScheme === "dark" ? "#c2410c" : "#ea580c"; const iconColor = "#ffffff"; - const handleArchiveChannel = async () => { - try { - // Show confirmation dialog - Alert.alert( - "Archiver la conversation", - "Êtes-vous sûr de vouloir archiver cette conversation ainsi que le tablo associé ?", - [ - { - text: "Annuler", - style: "cancel", - onPress: () => { - // Close the swipe action - translateX.value = withSpring(0); - }, - }, - { - text: "Archiver", - style: "destructive", - // onPress: async () => { - // try { - // // Hide the channel for the current user - // await channel.hide(); + // const handleDeleteChannel = async () => { + // try { + // // Show confirmation dialog + // Alert.alert( + // "Quitter la conversation", + // "Êtes-vous sûr de vouloir quitter cette conversation ? Vous n'aurez plus accès au tablo associé.", + // [ + // { + // text: "Annuler", + // style: "cancel", + // onPress: () => { + // // Close the swipe action + // translateX.value = withSpring(0); + // }, + // }, + // { + // text: "Quitter", + // style: "destructive", + // onPress: async () => { + // try { + // // Hide the channel for the current user + // await channel.delete({ hard_delete: false }); - // // Close the swipe action - // translateX.value = withSpring(0); + // // Close the swipe action + // translateX.value = withSpring(0); - // // Show success message - // Alert.alert( - // "Succès", - // "La conversation a été archivée avec succès", - // [{ text: "OK" }] - // ); - // } catch (error) { - // console.error("Error archiving channel:", error); - // Alert.alert("Erreur", "Impossible d'archiver la conversation"); - // } - // }, - }, - ], - { cancelable: true } - ); - } catch (error) { - console.error("Error showing archive dialog:", error); - } - }; + // // // Show success message + // // Alert.alert( + // // "Succès", + // // "La conversation a été supprimée avec succès", + // // [{ text: "OK" }] + // // ); + // } catch (error) { + // console.error("Error deleting channel:", error); + // Alert.alert("Erreur", "Impossible de quitter la conversation"); + // } + // }, + // }, + // ], + // { cancelable: true } + // ); + // } catch (error) { + // console.error("Error showing archive dialog:", error); + // } + // }; - const gestureHandler = Gesture.Pan() - .onStart((context) => { - // cancelOtherAnimations(id); - context.translationX = translateX.value; - }) - .onUpdate((event) => { - // Only allow swiping left (negative values) - translateX.value = Math.min( - 0, - Math.max(SWIPE_THRESHOLD, event.translationX) - ); - }) - .onEnd((event) => { - const shouldOpen = translateX.value < SWIPE_THRESHOLD / 2; + // const gestureHandler = Gesture.Pan() + // .onStart((context) => { + // // cancelOtherAnimations(id); + // context.translationX = translateX.value; + // }) + // .onUpdate((event) => { + // // Only allow swiping left (negative values) + // translateX.value = Math.min( + // 0, + // Math.max(SWIPE_THRESHOLD, event.translationX) + // ); + // }) + // .onEnd((event) => { + // const shouldOpen = translateX.value < SWIPE_THRESHOLD / 2; - if (shouldOpen) { - translateX.value = withSpring(SWIPE_THRESHOLD); - } else { - translateX.value = withSpring(0); - } - }); + // if (shouldOpen) { + // translateX.value = withSpring(SWIPE_THRESHOLD); + // } else { + // translateX.value = withSpring(0); + // } + // }); const channelAnimatedStyle = useAnimatedStyle(() => { return { @@ -143,42 +143,39 @@ export const SwipeableChannelPreview: React.FC< }; }); - const onArchivePress = () => { - runOnJS(handleArchiveChannel)(); - }; + // const onDeletePress = () => { + // runOnJS(handleDeleteChannel)(); + // }; return ( {/* Right Actions Background */} - + {/* - + - Archiver + Quitter - + */} {/* Channel Content */} - - - {children} - - + {/* */} + + {children} + + {/* */} ); }; @@ -200,7 +197,7 @@ const styles = StyleSheet.create({ justifyContent: "center", alignItems: "center", }, - archiveButton: { + deleteButton: { justifyContent: "center", alignItems: "center", width: ACTION_WIDTH, diff --git a/xtablo-expo/hooks/user.ts b/xtablo-expo/hooks/user.ts index 0abfe1e..4c7cab0 100644 --- a/xtablo-expo/hooks/user.ts +++ b/xtablo-expo/hooks/user.ts @@ -1,11 +1,7 @@ import { api } from "@/lib/api"; -import { Tables } from "@/types/database.types"; import { useAuthStore } from "@/stores/auth"; import { useQuery } from "@tanstack/react-query"; - -type User = Tables<"profiles"> & { - streamToken: string | null; -}; +import { User } from "@/types/user.types"; export const useGetUser = (): { user: User | null; isLoading: boolean } => { const session = useAuthStore((state) => state.session); diff --git a/xtablo-expo/package-lock.json b/xtablo-expo/package-lock.json index e05e87b..4f1a4ca 100644 --- a/xtablo-expo/package-lock.json +++ b/xtablo-expo/package-lock.json @@ -59,10 +59,8 @@ "@types/jest": "^29.5.12", "@types/lodash": "^4.17.13", "@types/react": "~19.0.10", - "@types/react-test-renderer": "^19.0.0", "jest": "^29.2.1", "jest-expo": "~53.0.9", - "react-test-renderer": "18.3.1", "typescript": "^5.3.3" } }, @@ -3365,6 +3363,35 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "peer": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3497,16 +3524,6 @@ "@types/react-native": "^0.70" } }, - "node_modules/@types/react-test-renderer": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", - "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3565,6 +3582,167 @@ "@urql/core": "^5.0.0" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -3574,6 +3752,20 @@ "node": ">=10.0.0" } }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "peer": true + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -3607,10 +3799,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "license": "MIT", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "bin": { "acorn": "bin/acorn" }, @@ -3629,6 +3820,19 @@ "acorn-walk": "^8.0.2" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-loose": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.0.tgz", @@ -3837,21 +4041,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4145,10 +4334,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4382,6 +4570,16 @@ "node": ">=12.13.0" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/chromium-edge-launcher": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", @@ -4590,15 +4788,15 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -5177,6 +5375,20 @@ "node": ">= 0.8" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", @@ -5234,6 +5446,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "peer": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5309,6 +5528,30 @@ "source-map": "~0.6.1" } }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -5322,6 +5565,19 @@ "node": ">=4" } }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -5359,6 +5615,16 @@ "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exec-async": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", @@ -5954,6 +6220,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/freeport-async": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", @@ -6091,11 +6372,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "peer": true + }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dependencies": { "balanced-match": "^1.0.0" } @@ -7734,22 +8021,6 @@ } } }, - "node_modules/jsdom/node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8124,6 +8395,16 @@ "integrity": "sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==", "license": "MIT" }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9079,9 +9360,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "engines": { "node": ">= 0.8" } @@ -9720,6 +10001,16 @@ "inherits": "~2.0.3" } }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -10040,6 +10331,92 @@ "react-native": "*" } }, + "node_modules/react-native-vector-icons": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz", + "integrity": "sha512-n5HGcxUuVaTf9QJPs/W22xQpC2Z9u0nb0KgLPnVltP8vdUvOp6+R26gF55kilP/fV4eL4vsAHUqUjewppJMBOQ==", + "peer": true, + "dependencies": { + "prop-types": "^15.7.2", + "yargs": "^16.1.1" + }, + "bin": { + "fa-upgrade.sh": "bin/fa-upgrade.sh", + "fa5-upgrade": "bin/fa5-upgrade.sh", + "fa6-upgrade": "bin/fa6-upgrade.sh", + "generate-icon": "bin/generate-icon.js" + } + }, + "node_modules/react-native-vector-icons/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/react-native-vector-icons/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "peer": true + }, + "node_modules/react-native-vector-icons/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-native-vector-icons/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "peer": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/react-native-web": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz", @@ -10187,35 +10564,6 @@ "webpack": "^5.59.0" } }, - "node_modules/react-test-renderer": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz", - "integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-is": "^18.3.1", - "react-shallow-renderer": "^16.15.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-test-renderer/node_modules/react-shallow-renderer": { - "version": "16.15.0", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", - "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -10488,16 +10836,6 @@ "node": ">=v12.22.7" } }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", @@ -10597,6 +10935,16 @@ "node": ">=0.10.0" } }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -11097,21 +11445,6 @@ "react-native": "*" } }, - "node_modules/stream-chat/node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/stream-chat/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", @@ -11377,6 +11710,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tar": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", @@ -11456,6 +11799,72 @@ "node": ">=10" } }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -11874,6 +12283,20 @@ "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", "license": "MIT" }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -11892,12 +12315,60 @@ "node": ">=12" } }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "node_modules/webpack": { + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.2.tgz", + "integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.2", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.13.0" } diff --git a/xtablo-expo/package.json b/xtablo-expo/package.json index 78ac640..80ecf7f 100644 --- a/xtablo-expo/package.json +++ b/xtablo-expo/package.json @@ -66,10 +66,8 @@ "@types/jest": "^29.5.12", "@types/lodash": "^4.17.13", "@types/react": "~19.0.10", - "@types/react-test-renderer": "^19.0.0", "jest": "^29.2.1", "jest-expo": "~53.0.9", - "react-test-renderer": "18.3.1", "typescript": "^5.3.3" }, "private": true diff --git a/xtablo-expo/providers/UserProvider.tsx b/xtablo-expo/providers/UserProvider.tsx index 8607be6..be21378 100644 --- a/xtablo-expo/providers/UserProvider.tsx +++ b/xtablo-expo/providers/UserProvider.tsx @@ -1,13 +1,8 @@ import { createStore, StoreApi, useStore } from "zustand"; import React from "react"; -import { Tables } from "@/types/database.types"; import { ActivityIndicator } from "react-native"; -import { Redirect } from "expo-router"; import { useGetUser } from "@/hooks/user"; - -type User = Tables<"profiles"> & { - streamToken: string | null; -}; +import { User } from "@/types/user.types"; const UserStoreContext = React.createContext | null>(null); @@ -23,7 +18,7 @@ export const UserStoreProvider = ({ } if (!user) { - return ; + return null; } const store = createStore()(() => user); diff --git a/xtablo-expo/types/user.types.ts b/xtablo-expo/types/user.types.ts new file mode 100644 index 0000000..c0cc9f3 --- /dev/null +++ b/xtablo-expo/types/user.types.ts @@ -0,0 +1,9 @@ +import { Tables } from "./database.types"; +import { RemoveNullFromObject } from "./removeNull"; + +export type User = RemoveNullFromObject< + Tables<"profiles"> & { + streamToken: string | null; + }, + "email" | "name" +>; From feb99e7bd01c05df5f5a9827a5b0abb8abf039a9 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Tue, 22 Jul 2025 21:55:30 +0200 Subject: [PATCH 11/16] Make build work --- justfile | 3 +++ xtablo-expo/app/_layout.tsx | 21 +++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/justfile b/justfile index d557c7b..e79b8b9 100644 --- a/justfile +++ b/justfile @@ -30,3 +30,6 @@ expo-install +package: expo-start *args: cd xtablo-expo && npx expo start {{args}} + +build-expo: + cd xtablo-expo && eas build --platform all diff --git a/xtablo-expo/app/_layout.tsx b/xtablo-expo/app/_layout.tsx index c22cea8..20eb496 100644 --- a/xtablo-expo/app/_layout.tsx +++ b/xtablo-expo/app/_layout.tsx @@ -14,7 +14,6 @@ import { cloneDeep } from "lodash"; import { SplashScreenController } from "@/components/Splash"; import { useInitializeApp } from "@/hooks/auth"; import { LoadingView } from "@/components/LoadingView"; -import { ClickOutsideProvider } from "react-native-click-outside"; window.structuredClone = cloneDeep; @@ -37,17 +36,15 @@ export default function RootLayout() { return ( - - - - - - - - - + + + + + + + ); } From a08375ad42ccb419ddd10e371038ec75cda7def8 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 27 Jul 2025 14:28:07 +0200 Subject: [PATCH 12/16] Add expo dev client --- xtablo-expo/package-lock.json | 97 +++++++++++++++++++++++++++++++++++ xtablo-expo/package.json | 3 +- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/xtablo-expo/package-lock.json b/xtablo-expo/package-lock.json index 4f1a4ca..f46803a 100644 --- a/xtablo-expo/package-lock.json +++ b/xtablo-expo/package-lock.json @@ -23,6 +23,7 @@ "expo-blur": "~14.1.5", "expo-constants": "~17.1.5", "expo-crypto": "~14.1.5", + "expo-dev-client": "~5.2.4", "expo-font": "~13.3.2", "expo-haptics": "~14.1.4", "expo-image-manipulator": "~13.1.7", @@ -5792,6 +5793,69 @@ "expo": "*" } }, + "node_modules/expo-dev-client": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.2.4.tgz", + "integrity": "sha512-s/N/nK5LPo0QZJpV4aPijxyrzV4O49S3dN8D2fljqrX2WwFZzWwFO6dX1elPbTmddxumdcpczsdUPY+Ms8g43g==", + "dependencies": { + "expo-dev-launcher": "5.1.16", + "expo-dev-menu": "6.1.14", + "expo-dev-menu-interface": "1.10.0", + "expo-manifests": "~0.16.6", + "expo-updates-interface": "~1.1.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-5.1.16.tgz", + "integrity": "sha512-tbCske9pvbozaEblyxoyo/97D6od9Ma4yAuyUnXtRET1CKAPKYS+c4fiZ+I3B4qtpZwN3JNFUjG3oateN0y6Hg==", + "dependencies": { + "ajv": "8.11.0", + "expo-dev-menu": "6.1.14", + "expo-manifests": "~0.16.6", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/expo-dev-menu": { + "version": "6.1.14", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-6.1.14.tgz", + "integrity": "sha512-yonNMg2GHJZtuisVowdl1iQjZfYP85r1D1IO+ar9D9zlrBPBJhq2XEju52jd1rDmDkmDuEhBSbPNhzIcsBNiPg==", + "dependencies": { + "expo-dev-menu-interface": "1.10.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz", + "integrity": "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-file-system": { "version": "18.1.11", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.1.11.tgz", @@ -5854,6 +5918,11 @@ "expo": "*" } }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==" + }, "node_modules/expo-keep-awake": { "version": "14.1.4", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz", @@ -5887,6 +5956,18 @@ "react-native": "*" } }, + "node_modules/expo-manifests": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.16.6.tgz", + "integrity": "sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==", + "dependencies": { + "@expo/config": "~11.0.12", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "2.1.14", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz", @@ -6030,6 +6111,14 @@ } } }, + "node_modules/expo-updates-interface": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz", + "integrity": "sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-web-browser": { "version": "14.2.0", "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.2.0.tgz", @@ -12167,6 +12256,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", diff --git a/xtablo-expo/package.json b/xtablo-expo/package.json index 80ecf7f..77cd438 100644 --- a/xtablo-expo/package.json +++ b/xtablo-expo/package.json @@ -58,7 +58,8 @@ "react-native-web": "^0.20.0", "react-native-webview": "13.13.5", "stream-chat-expo": "^6.7.3", - "zustand": "^5.0.4" + "zustand": "^5.0.4", + "expo-dev-client": "~5.2.4" }, "devDependencies": { "@babel/core": "^7.25.2", From 394fc3fd226121a1672a2251b9dd798ae6790993 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 27 Jul 2025 14:28:21 +0200 Subject: [PATCH 13/16] Add ICS export functionality to Planning --- ui/src/pages/planning.tsx | 43 ++++++++++++++++++++ ui/src/utils/helpers.ts | 85 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/ui/src/pages/planning.tsx b/ui/src/pages/planning.tsx index f9e0b2d..b3f2f75 100644 --- a/ui/src/pages/planning.tsx +++ b/ui/src/pages/planning.tsx @@ -9,6 +9,7 @@ import { SelectListItem, } from "@ui/ui-library/select"; import { Outlet, useNavigate, useParams } from "react-router-dom"; +import { generateICSFromEvents, downloadICSFile } from "@ui/utils/helpers"; type ViewType = "month" | "week" | "day"; @@ -31,6 +32,27 @@ export const PlanningPage = () => { const deleteEvent = useDeleteEvent(); + const handleExportICS = () => { + if (!tabloEvents || tabloEvents.length === 0) { + return; + } + + const calendarName = + selectedTabloId === "all" + ? "Planning - Tous les tablos" + : tablos?.find((t) => t.id === selectedTabloId)?.name || "Planning"; + + const icsContent = generateICSFromEvents(tabloEvents, calendarName); + const filename = + selectedTabloId === "all" + ? "planning-tous-tablos.ics" + : `planning-${ + tablos?.find((t) => t.id === selectedTabloId)?.name || "tablo" + }.ics`; + + downloadICSFile(icsContent, filename); + }; + const navigateToCreateEvent = (date: Date, tablo_id: string) => { if (tablo_id === "all") { navigate({ @@ -732,6 +754,27 @@ export const PlanningPage = () => {
+
{(["month", "week", "day"] as ViewType[]).map((view) => ( ))}
From 45c0e95e85d6a5e84d827f6abfd5ee168921ae13 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 27 Jul 2025 14:41:32 +0200 Subject: [PATCH 16/16] Add pre-commit hook to prevent direct commits to the main branch --- .pre-commit-config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dec20d4..bf4e4c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,12 @@ repos: - repo: local hooks: + - id: no-commit-to-main + name: Don't commit to main branch + entry: sh -c 'if [ "$(git rev-parse --abbrev-ref HEAD)" = "main" ]; then echo "Direct commits to main branch are not allowed!"; exit 1; fi' + language: system + pass_filenames: false + always_run: true - id: test-ui name: Test Frontend entry: just test-frontend