diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a5a27d9..bf4e4c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,18 @@ 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 language: python pass_filenames: false - files: \.ts* + files: ^ui/.*\.(ts|tsx|js|jsx)$ - id: typecheck name: Typecheck Frontend entry: just typecheck 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..e79b8b9 100644 --- a/justfile +++ b/justfile @@ -23,10 +23,13 @@ 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}} + +build-expo: + cd xtablo-expo && eas build --platform all diff --git a/ui/src/pages/planning.tsx b/ui/src/pages/planning.tsx index f9e0b2d..8323bc4 100644 --- a/ui/src/pages/planning.tsx +++ b/ui/src/pages/planning.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useTablosList } from "@ui/hooks/tablos"; import { useEventsByTablo, useDeleteEvent } from "@ui/hooks/events"; import { @@ -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,65 @@ export const PlanningPage = () => { const deleteEvent = useDeleteEvent(); + // Keyboard shortcuts for view switching + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + // Only trigger shortcuts when not typing in input fields + if ( + event.target instanceof HTMLInputElement || + event.target instanceof HTMLTextAreaElement || + event.target instanceof HTMLSelectElement + ) { + return; + } + + // Prevent default behavior and switch views + switch (event.key.toLowerCase()) { + case "m": + case "1": + event.preventDefault(); + setCurrentView("month"); + break; + case "w": + case "s": + case "2": + event.preventDefault(); + setCurrentView("week"); + break; + case "d": + case "j": + case "3": + event.preventDefault(); + setCurrentView("day"); + break; + } + }; + + window.addEventListener("keydown", handleKeyPress); + return () => window.removeEventListener("keydown", handleKeyPress); + }, []); + + 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,11 +792,45 @@ export const PlanningPage = () => {
+
{(["month", "week", "day"] as ViewType[]).map((view) => ( ))}
diff --git a/ui/src/utils/helpers.ts b/ui/src/utils/helpers.ts index 30893b1..18e4f52 100644 --- a/ui/src/utils/helpers.ts +++ b/ui/src/utils/helpers.ts @@ -1,4 +1,5 @@ import { Database } from "@ui/types/database.types"; +import { EventAndTablo } from "@ui/types/events.types"; import jsPDF from "jspdf"; export const calculateTax = (amount: number, taxRate: number) => { @@ -79,3 +80,89 @@ export const exportDevisToPdf = (devis: Devis) => { export const isStaging = import.meta.env.MODE === "staging"; export const isProd = import.meta.env.MODE === "production"; + +// ICS Export functionality +export const generateICSFromEvents = ( + events: EventAndTablo[], + calendarName: string = "Planning" +) => { + const formatDate = (date: string, time: string) => { + // Combine date (YYYY-MM-DD) and time (HH:MM:SS) into ISO format then convert to UTC + const dateTime = new Date(`${date}T${time}`); + return dateTime.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"; + }; + + const escapeICSText = (text: string) => { + return text + .replace(/\\/g, "\\\\") + .replace(/;/g, "\\;") + .replace(/,/g, "\\,") + .replace(/\n/g, "\\n") + .replace(/\r/g, ""); + }; + + const generateUID = (eventId: string) => { + return `${eventId}@xtablo.com`; + }; + + let icsContent = [ + "BEGIN:VCALENDAR", + "VERSION:2.0", + "PRODID:-//XTablo//Planning Export//EN", + `X-WR-CALNAME:${escapeICSText(calendarName)}`, + "X-WR-TIMEZONE:Europe/Paris", + "CALSCALE:GREGORIAN", + "METHOD:PUBLISH", + ].join("\r\n"); + + events.forEach((event) => { + if (!event.start_date || !event.start_time || !event.title) return; + + console.log("event", event); + + const startDateTime = formatDate(event.start_date, event.start_time); + const endDateTime = event.end_time + ? formatDate(event.start_date, event.end_time) + : formatDate(event.start_date, event.start_time); // Default to start time if no end time + + const eventLines = [ + "", + "BEGIN:VEVENT", + `UID:${generateUID(event.event_id)}`, + `DTSTART:${startDateTime}`, + `DTEND:${endDateTime}`, + `SUMMARY:${escapeICSText(event.title)}`, + `DESCRIPTION:${escapeICSText( + `Tablo: ${event.tablo_name}\n${event.description || ""}` + )}`, + event.tablo_name ? `CATEGORIES:${escapeICSText(event.tablo_name)}` : "", + `CREATED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`, + `LAST-MODIFIED:${ + new Date().toISOString().replace(/[-:]/g, "").split(".")[0] + }Z`, + "STATUS:CONFIRMED", + "TRANSP:OPAQUE", + "END:VEVENT", + ].filter((line) => line !== ""); // Remove empty lines + + icsContent += "\r\n" + eventLines.join("\r\n"); + }); + + icsContent += "\r\n" + "END:VCALENDAR"; + return icsContent; +}; + +export const downloadICSFile = ( + icsContent: string, + filename: string = "planning.ics" +) => { + console.log("icsContent", icsContent); + const blob = new Blob([icsContent], { type: "text/calendar;charset=utf-8" }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); +}; 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/(home)/(tabs)/_layout.tsx b/xtablo-expo/app/(app)/(tabs)/_layout.tsx similarity index 86% rename from xtablo-expo/app/(home)/(tabs)/_layout.tsx rename to xtablo-expo/app/(app)/(tabs)/_layout.tsx index a51d2ee..c5b472f 100644 --- a/xtablo-expo/app/(home)/(tabs)/_layout.tsx +++ b/xtablo-expo/app/(app)/(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/(app)/(tabs)/index.tsx b/xtablo-expo/app/(app)/(tabs)/index.tsx new file mode 100644 index 0000000..1bb802f --- /dev/null +++ b/xtablo-expo/app/(app)/(tabs)/index.tsx @@ -0,0 +1,865 @@ +import { router } from "expo-router"; +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 } 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"; +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} + + ); +}; + +// 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 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>({}); + + // 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, + }; + + 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 = () => { + // 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}" + // + // + // )} + // + // ); + + 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 ( + + + + {/* Beautiful Header */} + + + + + Discussions + + {/* {debouncedSearchQuery.trim() + ? `Recherche: "${debouncedSearchQuery}"` + : "Gérez les conversations de vos tablos"} */} + Gérez les conversations de vos tablos + + + + {/* + {isSearchVisible ? ( + + ) : ( + + )} + */} + + + + {/* Decorative Elements */} + + + + + {/* Channel List with animated search */} + + {/* */} + { + // Close search when selecting a channel + // if (isSearchVisible) { + // toggleSearch(); + // } + router.push(`/channel/${channel.cid}`); + }} + sort={sort} + options={options} + Preview={(props) => ( + { + // animations.value = { + // [id]: 0, + // }; + // }} + /> + )} + PreviewAvatar={(props) => ( + + )} + 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 + + + )} + /> + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + // backgroundColor is set dynamically + }, + headerGradient: { + paddingTop: 50, + paddingBottom: 25, + paddingHorizontal: 20, + position: "relative", + overflow: "hidden", + }, + headerContent: { + zIndex: 10, + }, + headerTop: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 20, + }, + userInfo: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + avatar: { + marginRight: 12, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 5, + borderWidth: 3, + borderColor: "rgba(255, 255, 255, 0.3)", + }, + greetingContainer: { + flex: 1, + }, + greeting: { + fontSize: 16, + color: "rgba(255, 255, 255, 0.9)", + fontWeight: "500", + }, + userName: { + fontSize: 20, + color: "white", + fontWeight: "bold", + marginTop: 2, + }, + headerActions: { + flexDirection: "row", + alignItems: "center", + gap: 15, + }, + actionButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: "rgba(255, 255, 255, 0.2)", + justifyContent: "center", + alignItems: "center", + position: "relative", + }, + notificationBadge: { + position: "absolute", + top: -2, + right: -2, + backgroundColor: "#ef4444", + borderRadius: 10, + minWidth: 20, + height: 20, + justifyContent: "center", + alignItems: "center", + borderWidth: 2, + borderColor: "white", + }, + badgeText: { + color: "white", + fontSize: 12, + fontWeight: "bold", + }, + headerBottom: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-end", + }, + titleContainer: { + flex: 1, + }, + headerTitle: { + fontSize: 28, + color: "white", + fontWeight: "bold", + marginBottom: 4, + }, + headerSubtitle: { + fontSize: 16, + color: "rgba(255, 255, 255, 0.8)", + fontWeight: "400", + }, + searchButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: "white", + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 4, + }, + 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)", + }, + channelListContainer: { + flex: 1, + // backgroundColor is set dynamically + marginTop: -10, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + paddingTop: 10, + }, + + // Custom Avatar Styles + avatarContainer: { + position: "relative", + width: 56, + height: 56, + marginRight: 12, + }, + avatarGradient: { + width: 56, + height: 56, + borderRadius: 16, + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.15, + shadowRadius: 8, + elevation: 6, + position: "relative", + }, + avatarInitials: { + fontSize: 18, + fontWeight: "bold", + color: "white", + textShadowColor: "rgba(0, 0, 0, 0.3)", + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 2, + }, + avatarRing: { + position: "absolute", + top: -2, + left: -2, + width: 60, + height: 60, + borderRadius: 18, + borderWidth: 2, + borderColor: "rgba(59, 130, 246, 0.2)", + backgroundColor: "transparent", + }, + statusIndicator: { + position: "absolute", + bottom: 2, + right: 2, + width: 16, + height: 16, + borderRadius: 8, + backgroundColor: "#10b981", + borderWidth: 3, + // borderColor is set dynamically to match background + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 3, + }, + memberCountBadge: { + position: "absolute", + top: -4, + right: -4, + backgroundColor: "#3b82f6", + borderRadius: 10, + minWidth: 20, + height: 20, + justifyContent: "center", + alignItems: "center", + borderWidth: 2, + borderColor: "white", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 4, + elevation: 3, + }, + memberCountText: { + color: "white", + fontSize: 11, + fontWeight: "bold", + }, + // Custom Channel Title Styles + customChannelTitle: { + fontSize: 18, + fontWeight: "bold", + // color is set dynamically + }, + + // Search Header Styles + searchHeaderContainer: { + // backgroundColor is set dynamically + borderBottomWidth: 1, + // borderBottomColor is set dynamically + overflow: "hidden", + paddingHorizontal: 20, + }, + searchInputContainer: { + flexDirection: "row", + alignItems: "center", + // backgroundColor is set dynamically + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 8, + borderWidth: 1, + // borderColor is set dynamically + marginTop: 15, + }, + searchIcon: { + marginRight: 10, + }, + searchInput: { + flex: 1, + fontSize: 16, + // color is set dynamically + paddingVertical: 0, + fontWeight: "500", + }, + clearButton: { + padding: 5, + }, + searchInfoContainer: { + paddingTop: 10, + paddingBottom: 15, + }, + searchInfoText: { + fontSize: 14, + // color is set dynamically + }, + searchLoadingContainer: { + paddingVertical: 20, + alignItems: "center", + }, + searchLoadingText: { + fontSize: 16, + // color is set dynamically + }, + emptySearchContainer: { + paddingVertical: 40, + alignItems: "center", + }, + emptySearchTitle: { + fontSize: 20, + // color is set dynamically + marginTop: 10, + }, + emptySearchMessage: { + fontSize: 16, + // color is set dynamically + marginTop: 5, + textAlign: "center", + paddingHorizontal: 20, + }, + + // Loading Skeleton Styles + loadingContentContainer: { + flex: 1, + // backgroundColor is set dynamically + 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 is set dynamically + marginRight: 12, + }, + loadingTextContainer: { + flex: 1, + }, + loadingTitle: { + height: 20, + // backgroundColor is set dynamically + borderRadius: 4, + marginBottom: 8, + width: "70%", + }, + loadingSubtitle: { + height: 16, + // backgroundColor is set dynamically + borderRadius: 4, + width: "50%", + }, +}); diff --git a/xtablo-expo/app/(home)/(tabs)/planning.tsx b/xtablo-expo/app/(app)/(tabs)/planning.tsx similarity index 75% rename from xtablo-expo/app/(home)/(tabs)/planning.tsx rename to xtablo-expo/app/(app)/(tabs)/planning.tsx index b84d52a..3c0abff 100644 --- a/xtablo-expo/app/(home)/(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 @@ -166,7 +195,6 @@ export default function PlanningScreen() { Alert.alert("Erreur", "Veuillez sélectionner un tablo"); return; } - console.log({ newEvent }); createEvent({ ...newEvent, start_date: newEvent.start_date, @@ -177,7 +205,7 @@ export default function PlanningScreen() { const renderTabloOption = ({ item }: { item: UserTablo }) => ( selectTablo(item)} > @@ -190,7 +218,9 @@ export default function PlanningScreen() { ]} /> - {item.name} + + {item.name} + {selectedTablo?.id === item.id && } @@ -198,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} + @@ -234,7 +273,8 @@ export default function PlanningScreen() { ))} {dayEvents.length > 6 && ( - - + + +{dayEvents.length - 6} @@ -319,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" : ""} @@ -343,9 +395,11 @@ export default function PlanningScreen() { /> ) : ( - - Aucun événement - + + + Aucun événement + + Vous n'avez aucun événement prévu pour cette date. @@ -359,12 +413,17 @@ export default function PlanningScreen() { const todayEvents = getEventsForDate(selectedDate); return ( - + {/* Header */} - Planning - + + Planning + + {viewMode === "month" ? months[currentMonth.getMonth()] + " " + @@ -387,7 +446,12 @@ export default function PlanningScreen() { {/* View Mode Toggle */} - + @@ -417,11 +482,12 @@ export default function PlanningScreen() { > @@ -434,35 +500,48 @@ 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 [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 */} + + + + + + {renderSettingsSection( + "Compte", + <> + {renderSettingsItem( + , + "Profil utilisateur", + `${user.name || "Non défini"} • ${user.email}`, + () => router.push("/user"), + undefined, + true + )} + + )} + + {/* {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", + "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." + ); + } + )} + + )} + + {/* {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://app.xtablo.com"), + undefined, + true + )} */} + + )} + + {/* Sign Out Section */} + + + + + Se déconnecter + + + + + {/* Bottom Spacing */} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + 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, + marginTop: -10, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + paddingTop: 20, + }, + section: { + marginBottom: 24, + paddingHorizontal: 20, + }, + sectionTitle: { + fontSize: 18, + fontWeight: "600", + marginBottom: 12, + marginLeft: 4, + }, + sectionContent: { + borderRadius: 16, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 3, + overflow: "hidden", + borderWidth: 1, + }, + settingsItem: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 20, + paddingVertical: 16, + borderBottomWidth: 1, + }, + settingsItemLeft: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + iconContainer: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "rgba(0, 0, 0, 0.05)", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + settingsItemContent: { + flex: 1, + }, + settingsItemTitle: { + fontSize: 16, + fontWeight: "500", + marginBottom: 2, + }, + settingsItemSubtitle: { + fontSize: 14, + }, + 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, + }, +}); diff --git a/xtablo-expo/app/(home)/(tabs)/tablos.tsx b/xtablo-expo/app/(app)/(tabs)/tablos.tsx similarity index 85% rename from xtablo-expo/app/(home)/(tabs)/tablos.tsx rename to xtablo-expo/app/(app)/(tabs)/tablos.tsx index dd58976..e4135c2 100644 --- a/xtablo-expo/app/(home)/(tabs)/tablos.tsx +++ b/xtablo-expo/app/(app)/(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,8 @@ import { } from "lucide-react-native"; import { router } from "expo-router"; import { AVAILABLE_COLORS, ColorMap } from "@/constants/colors"; -import { useAuth } from "@/stores/auth"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; const { width } = Dimensions.get("window"); const numColumns = 2; @@ -46,6 +47,33 @@ export default function TablosScreen() { const [refreshing, setRefreshing] = useState(false); 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; @@ -156,6 +184,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"; @@ -163,9 +218,14 @@ export default function TablosScreen() { navigateToTablo(tablo)} + onLongPress={() => handleDeleteTablo(tablo)} activeOpacity={0.8} > {/* Tablo Image/Color Header */} @@ -188,7 +248,10 @@ export default function TablosScreen() { {/* Tablo Info */} - + {tablo.name} - + router.push("/planning")} > - + @@ -247,8 +310,12 @@ export default function TablosScreen() { const renderListItem = ({ item: tablo }: { item: UserTablo }) => { return ( navigateToTablo(tablo)} + onLongPress={() => handleDeleteTablo(tablo)} activeOpacity={0.8} > - {tablo.name} + + {tablo.name} + - + router.push("/planning")} > - + @@ -340,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( @@ -456,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 }) @@ -477,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/(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/(app)/user/index.tsx b/xtablo-expo/app/(app)/user/index.tsx new file mode 100644 index 0000000..bb72e5b --- /dev/null +++ b/xtablo-expo/app/(app)/user/index.tsx @@ -0,0 +1,328 @@ +import { + View, + StyleSheet, + ScrollView, + Text, + TouchableOpacity, +} from "react-native"; +import { useAuthStore } from "@/stores/auth"; +import { Avatar, Input } from "@rn-vui/themed"; +import { Card } from "@rn-vui/themed"; +import { useState } from "react"; +import { useUser } from "@/providers/UserProvider"; +import { LinearGradient } from "expo-linear-gradient"; +import { + User, + Mail, + Edit3, + Check, + LogOut, + Settings, + Shield, +} from "lucide-react-native"; +import { Stack } from "expo-router"; +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 handleSaveDisplayName = () => { + // // TODO: Implémenter la fonctionnalité de sauvegarde + // setIsEditing(false); + // }; + + return ( + + + + + + + {user.name || "Utilisateur"} + {user.email} + + + + {/* Contenu principal */} + + {/* Carte d'informations personnelles */} + + + + + Informations personnelles + + + + + + + + + Adresse e-mail + {user.email} + + + + + + + + + + + Nom d'affichage + {user.name} + {/* {isEditing ? ( + + ) : ( + + {user.name || "Non défini"} + + )} */} + + {/* { + if (isEditing) { + handleSaveDisplayName(); + } else { + setDisplayName(user.name ?? ""); + setIsEditing(true); + } + }} + > + {isEditing ? ( + + ) : ( + + )} + */} + + + + {/* Bouton de déconnexion */} + + + + Se déconnecter + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#f8fafc", + }, + headerGradient: { + paddingBottom: 40, + paddingHorizontal: 20, + }, + headerContent: { + alignItems: "center", + }, + avatar: { + marginBottom: 16, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + borderWidth: 4, + borderColor: "white", + }, + userName: { + fontSize: 24, + fontWeight: "bold", + color: "white", + marginBottom: 4, + textAlign: "center", + }, + userEmail: { + fontSize: 16, + color: "rgba(255, 255, 255, 0.9)", + textAlign: "center", + }, + content: { + padding: 20, + marginTop: -20, + }, + mainCard: { + borderRadius: 16, + padding: 0, + marginBottom: 20, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + borderWidth: 0, + }, + menuCard: { + borderRadius: 16, + padding: 0, + marginBottom: 20, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + borderWidth: 0, + }, + cardHeader: { + flexDirection: "row", + alignItems: "center", + padding: 20, + paddingBottom: 16, + }, + cardHeaderTitle: { + fontSize: 18, + fontWeight: "600", + color: "#1f2937", + marginLeft: 12, + }, + infoItem: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 20, + paddingVertical: 16, + }, + infoIconContainer: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "#f3f4f6", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + infoContent: { + flex: 1, + }, + infoLabel: { + fontSize: 14, + color: "#6b7280", + marginBottom: 4, + fontWeight: "500", + }, + infoValue: { + fontSize: 16, + color: "#1f2937", + fontWeight: "500", + }, + editButton: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: "#f3f4f6", + justifyContent: "center", + alignItems: "center", + }, + inputContainer: { + paddingHorizontal: 0, + height: 40, + marginTop: 4, + }, + inputText: { + fontSize: 16, + color: "#1f2937", + }, + divider: { + height: 1, + backgroundColor: "#e5e7eb", + marginLeft: 72, + }, + menuItem: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 20, + paddingVertical: 16, + }, + menuIconContainer: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: "#f3f4f6", + justifyContent: "center", + alignItems: "center", + marginRight: 12, + }, + menuContent: { + flex: 1, + }, + menuTitle: { + fontSize: 16, + fontWeight: "500", + color: "#1f2937", + marginBottom: 2, + }, + menuSubtitle: { + fontSize: 14, + color: "#6b7280", + }, + menuArrow: { + fontSize: 20, + color: "#9ca3af", + fontWeight: "300", + }, + signOutContainer: { + marginTop: 10, + marginBottom: 40, + }, + signOutGradient: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + paddingVertical: 16, + paddingHorizontal: 24, + borderRadius: 12, + shadowColor: "#ef4444", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 6, + }, + signOutText: { + color: "white", + fontSize: 16, + fontWeight: "600", + marginLeft: 8, + }, +}); 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/(home)/(tabs)/index.tsx b/xtablo-expo/app/(home)/(tabs)/index.tsx deleted file mode 100644 index 23d99b9..0000000 --- a/xtablo-expo/app/(home)/(tabs)/index.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import { router } from "expo-router"; -import { ChannelList } from "stream-chat-expo"; -import { ChannelSort } from "stream-chat"; -import { useUser } from "@/providers/UserProvider"; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - StatusBar, -} from "react-native"; -import { LinearGradient } from "expo-linear-gradient"; -import { Search } from "lucide-react-native"; -import React from "react"; -import { useTablosList } from "@/hooks/tablos"; -import { ColorMap } from "@/constants/colors"; -import { UserTablo } from "@/types/tablos.types"; - -// Custom Avatar Component for Channel List -const CustomChannelAvatar = ({ - channel, - tablos, -}: { - channel: any; - 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) */} - - - ); -}; - -export default function HomeScreen() { - const user = useUser(); - const { data: tablos } = useTablosList(); - - const filters = { - members: { $in: [user.id] }, - type: "messaging", - }; - const sort: ChannelSort = { last_updated: -1 }; - const options = { - state: true, - watch: true, - }; - - // Create a wrapper component for the avatar that has access to tablos data - const AvatarWithTablos = ({ channel }: { channel: any }) => ( - - ); - - return ( - - - - {/* Beautiful Header */} - - - - - Discussions - - Gérez les conversations de vos tablos - - - - - - - - - - {/* Decorative Elements - - */} - - - {/* Channel List */} - - { - router.push(`/channel/${channel.cid}`); - }} - sort={sort} - options={options} - PreviewAvatar={AvatarWithTablos} - /> - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#f8fafc", - }, - headerGradient: { - paddingTop: 50, - paddingBottom: 25, - paddingHorizontal: 20, - position: "relative", - overflow: "hidden", - }, - headerContent: { - zIndex: 10, - }, - headerTop: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 20, - }, - userInfo: { - flexDirection: "row", - alignItems: "center", - flex: 1, - }, - avatar: { - marginRight: 12, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.3, - shadowRadius: 4, - elevation: 5, - borderWidth: 3, - borderColor: "rgba(255, 255, 255, 0.3)", - }, - greetingContainer: { - flex: 1, - }, - greeting: { - fontSize: 16, - color: "rgba(255, 255, 255, 0.9)", - fontWeight: "500", - }, - userName: { - fontSize: 20, - color: "white", - fontWeight: "bold", - marginTop: 2, - }, - headerActions: { - flexDirection: "row", - alignItems: "center", - gap: 15, - }, - actionButton: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: "rgba(255, 255, 255, 0.2)", - justifyContent: "center", - alignItems: "center", - position: "relative", - }, - notificationBadge: { - position: "absolute", - top: -2, - right: -2, - backgroundColor: "#ef4444", - borderRadius: 10, - minWidth: 20, - height: 20, - justifyContent: "center", - alignItems: "center", - borderWidth: 2, - borderColor: "white", - }, - badgeText: { - color: "white", - fontSize: 12, - fontWeight: "bold", - }, - headerBottom: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "flex-end", - }, - titleContainer: { - flex: 1, - }, - headerTitle: { - fontSize: 28, - color: "white", - fontWeight: "bold", - marginBottom: 4, - }, - headerSubtitle: { - fontSize: 16, - color: "rgba(255, 255, 255, 0.8)", - fontWeight: "400", - }, - searchButton: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: "white", - justifyContent: "center", - alignItems: "center", - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.15, - shadowRadius: 8, - elevation: 4, - }, - 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)", - }, - channelListContainer: { - flex: 1, - backgroundColor: "#f8fafc", - marginTop: -10, - borderTopLeftRadius: 10, - borderTopRightRadius: 10, - paddingTop: 10, - }, - - // Custom Avatar Styles - avatarContainer: { - position: "relative", - width: 56, - height: 56, - marginRight: 12, - }, - avatarGradient: { - width: 56, - height: 56, - borderRadius: 16, - justifyContent: "center", - alignItems: "center", - shadowColor: "#000", - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.15, - shadowRadius: 8, - elevation: 6, - position: "relative", - }, - avatarInitials: { - fontSize: 18, - fontWeight: "bold", - color: "white", - textShadowColor: "rgba(0, 0, 0, 0.3)", - textShadowOffset: { width: 0, height: 1 }, - textShadowRadius: 2, - }, - avatarRing: { - position: "absolute", - top: -2, - left: -2, - width: 60, - height: 60, - borderRadius: 18, - borderWidth: 2, - borderColor: "rgba(59, 130, 246, 0.2)", - backgroundColor: "transparent", - }, - statusIndicator: { - position: "absolute", - bottom: 2, - right: 2, - width: 16, - height: 16, - borderRadius: 8, - backgroundColor: "#10b981", - borderWidth: 3, - borderColor: "white", - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 4, - elevation: 3, - }, - memberCountBadge: { - position: "absolute", - top: -4, - right: -4, - backgroundColor: "#3b82f6", - borderRadius: 10, - minWidth: 20, - height: 20, - justifyContent: "center", - alignItems: "center", - borderWidth: 2, - borderColor: "white", - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 4, - elevation: 3, - }, - memberCountText: { - color: "white", - fontSize: 11, - fontWeight: "bold", - }, -}); diff --git a/xtablo-expo/app/(home)/user/profile.tsx b/xtablo-expo/app/(home)/user/profile.tsx deleted file mode 100644 index dd38091..0000000 --- a/xtablo-expo/app/(home)/user/profile.tsx +++ /dev/null @@ -1,361 +0,0 @@ -import { - View, - StyleSheet, - ScrollView, - Text, - TouchableOpacity, -} from "react-native"; -import { useAuth } from "@/stores/auth"; -import { Avatar, Input } from "@rn-vui/themed"; -import { Card } from "@rn-vui/themed"; -import { useState } from "react"; -import { useUser } from "@/providers/UserProvider"; -import { LinearGradient } from "expo-linear-gradient"; -import { - User, - Mail, - Edit3, - Check, - LogOut, - Settings, - Shield, -} from "lucide-react-native"; - -export default function ProfileScreen() { - const signOut = useAuth((state) => state.signOut); - const user = useUser(); - - const [displayName, setDisplayName] = useState(user.name || ""); - const [isEditing, setIsEditing] = useState(false); - - const handleSaveDisplayName = () => { - // TODO: Implémenter la fonctionnalité de sauvegarde - setIsEditing(false); - }; - - const menuItems = [ - { - icon: Settings, - title: "Paramètres du compte", - subtitle: "Gérez vos préférences de compte", - onPress: () => console.log("Paramètres"), - }, - { - icon: Shield, - title: "Confidentialité et sécurité", - subtitle: "Contrôlez vos paramètres de confidentialité", - onPress: () => console.log("Confidentialité"), - }, - ]; - - return ( - - - - - {user.name || "Utilisateur"} - {user.email} - - - - {/* Contenu principal */} - - {/* Carte d'informations personnelles */} - - - - - Informations personnelles - - - - - - - - - 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} - - - - {index < menuItems.length - 1 && } - - ))} - - - {/* Bouton de déconnexion */} - - - - Se déconnecter - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#f8fafc", - }, - headerGradient: { - paddingTop: 60, - paddingBottom: 40, - paddingHorizontal: 20, - }, - headerContent: { - alignItems: "center", - }, - avatar: { - marginBottom: 16, - shadowColor: "#000", - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 8, - borderWidth: 4, - borderColor: "white", - }, - userName: { - fontSize: 24, - fontWeight: "bold", - color: "white", - marginBottom: 4, - textAlign: "center", - }, - userEmail: { - fontSize: 16, - color: "rgba(255, 255, 255, 0.9)", - textAlign: "center", - }, - content: { - padding: 20, - marginTop: -20, - }, - mainCard: { - borderRadius: 16, - padding: 0, - marginBottom: 20, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 4, - borderWidth: 0, - }, - menuCard: { - borderRadius: 16, - padding: 0, - marginBottom: 20, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 4, - borderWidth: 0, - }, - cardHeader: { - flexDirection: "row", - alignItems: "center", - padding: 20, - paddingBottom: 16, - }, - cardHeaderTitle: { - fontSize: 18, - fontWeight: "600", - color: "#1f2937", - marginLeft: 12, - }, - infoItem: { - flexDirection: "row", - alignItems: "center", - paddingHorizontal: 20, - paddingVertical: 16, - }, - infoIconContainer: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: "#f3f4f6", - justifyContent: "center", - alignItems: "center", - marginRight: 12, - }, - infoContent: { - flex: 1, - }, - infoLabel: { - fontSize: 14, - color: "#6b7280", - marginBottom: 4, - fontWeight: "500", - }, - infoValue: { - fontSize: 16, - color: "#1f2937", - fontWeight: "500", - }, - editButton: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: "#f3f4f6", - justifyContent: "center", - alignItems: "center", - }, - inputContainer: { - paddingHorizontal: 0, - height: 40, - marginTop: 4, - }, - inputText: { - fontSize: 16, - color: "#1f2937", - }, - divider: { - height: 1, - backgroundColor: "#e5e7eb", - marginLeft: 72, - }, - menuItem: { - flexDirection: "row", - alignItems: "center", - paddingHorizontal: 20, - paddingVertical: 16, - }, - menuIconContainer: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: "#f3f4f6", - justifyContent: "center", - alignItems: "center", - marginRight: 12, - }, - menuContent: { - flex: 1, - }, - menuTitle: { - fontSize: 16, - fontWeight: "500", - color: "#1f2937", - marginBottom: 2, - }, - menuSubtitle: { - fontSize: 14, - color: "#6b7280", - }, - menuArrow: { - fontSize: 20, - color: "#9ca3af", - fontWeight: "300", - }, - signOutContainer: { - marginTop: 10, - marginBottom: 40, - }, - signOutGradient: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - paddingVertical: 16, - paddingHorizontal: 24, - borderRadius: 12, - shadowColor: "#ef4444", - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 6, - }, - signOutText: { - color: "white", - fontSize: 16, - fontWeight: "600", - marginLeft: 8, - }, -}); diff --git a/xtablo-expo/app/_layout.tsx b/xtablo-expo/app/_layout.tsx index cb6bcd8..20eb496 100644 --- a/xtablo-expo/app/_layout.tsx +++ b/xtablo-expo/app/_layout.tsx @@ -3,17 +3,17 @@ 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 { SplashScreenController } from "@/components/Splash"; +import { useInitializeApp } from "@/hooks/auth"; +import { LoadingView } from "@/components/LoadingView"; window.structuredClone = cloneDeep; @@ -33,19 +33,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 +40,34 @@ export default function RootLayout() { - - - - - + + ); } + +const RootNavigator = () => { + const { isLoading, isLoggedIn } = useInitializeApp(); + + if (isLoading) { + return ; + } + + return ( + + + + + + + + + + + + + ); +}; diff --git a/xtablo-expo/app/(auth)/login.tsx b/xtablo-expo/app/login.tsx similarity index 63% rename from xtablo-expo/app/(auth)/login.tsx rename to xtablo-expo/app/login.tsx index 776b506..22a9e3d 100644 --- a/xtablo-expo/app/(auth)/login.tsx +++ b/xtablo-expo/app/login.tsx @@ -1,25 +1,60 @@ 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 { 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"; import { AppleLoginButton } from "@/components/AppleLoginButton"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; 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); + + // 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" + ); + + 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 + + + Connexion XTablo + + Connectez-vous à votre compte + - - ou - + + ou + performOAuth("google")} /> @@ -62,8 +97,10 @@ export default function Auth() { performOAuth("apple")} /> - Pas encore de compte ? - + + Pas encore de compte ?{" "} + + S'inscrire @@ -76,7 +113,6 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "center", padding: 16, - backgroundColor: "#f5f5f5", // Light grey background }, logo: { width: 80, @@ -90,13 +126,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 +166,9 @@ const styles = StyleSheet.create({ separator: { flex: 1, height: 1, - backgroundColor: "#ddd", }, separatorText: { marginHorizontal: 15, - color: "#666", fontSize: 14, }, linkContainer: { @@ -144,11 +176,8 @@ const styles = StyleSheet.create({ justifyContent: "center", marginTop: 12, }, - linkText: { - color: "#666", - }, + linkText: {}, link: { - color: "#007bff", fontWeight: "bold", }, }); diff --git a/xtablo-expo/app/(auth)/signup.tsx b/xtablo-expo/app/signup.tsx similarity index 74% rename from xtablo-expo/app/(auth)/signup.tsx rename to xtablo-expo/app/signup.tsx index c15a983..a99dc55 100644 --- a/xtablo-expo/app/(auth)/signup.tsx +++ b/xtablo-expo/app/signup.tsx @@ -1,9 +1,11 @@ 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"; +import { useThemeColor } from "@/hooks/useThemeColor"; +import { useColorScheme } from "@/hooks/useColorScheme"; export default function SignUp() { const [firstName, setFirstName] = useState(""); @@ -12,14 +14,39 @@ 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); + + // 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" + ); + + const dark = useColorScheme() === "dark"; + + const logo = dark + ? require("@/assets/images/logo_white.png") + : require("@/assets/images/logo.png"); 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 +128,6 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: "center", padding: 16, - backgroundColor: "#f5f5f5", // Light grey background }, logo: { width: 80, @@ -113,13 +141,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 +193,8 @@ const styles = StyleSheet.create({ justifyContent: "center", marginTop: 12, }, - linkText: { - color: "#666", - }, + linkText: {}, link: { - color: "#007bff", fontWeight: "bold", }, }); diff --git a/xtablo-expo/assets/images/logo_white.png b/xtablo-expo/assets/images/logo_white.png new file mode 100644 index 0000000..4a474a3 Binary files /dev/null and b/xtablo-expo/assets/images/logo_white.png differ 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); + + // 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, + false + ); + const rotationDeg = useDerivedValue(() => { + return `${rotation.value}deg`; + }); + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{ rotate: rotationDeg.value }], + }; + }); + + return ( + + + + + + + XTablo + + + Initialisation de l'application... + + + + ); +}; + +const styles = StyleSheet.create({ + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + loadingContent: { + alignItems: "center", + paddingHorizontal: 40, + }, + logoContainer: { + alignItems: "center", + width: 130, + height: 130, + }, + logo: { + width: 100, + height: 100, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 4, + }, + title: { + fontSize: 28, + fontWeight: "bold", + textAlign: "center", + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + textAlign: "center", + marginBottom: 32, + 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/components/SwipeableChannelPreview.tsx b/xtablo-expo/components/SwipeableChannelPreview.tsx new file mode 100644 index 0000000..b5b19ad --- /dev/null +++ b/xtablo-expo/components/SwipeableChannelPreview.tsx @@ -0,0 +1,217 @@ +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, Trash } 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; + children: React.ReactNode; +} + +const SWIPE_THRESHOLD = -80; +const ACTION_WIDTH = 80; + +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 deleteButtonColor = colorScheme === "dark" ? "#c2410c" : "#ea580c"; + const iconColor = "#ffffff"; + + // 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); + + // // // 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; + + // 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 onDeletePress = () => { + // runOnJS(handleDeleteChannel)(); + // }; + + return ( + + {/* Right Actions Background */} + {/* + + + + + Quitter + + + + */} + + {/* Channel Content */} + {/* */} + + {children} + + {/* */} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + position: "relative", + }, + channelContainer: { + flex: 1, + }, + rightActionsContainer: { + position: "absolute", + right: 0, + top: 0, + bottom: 0, + width: ACTION_WIDTH, + justifyContent: "center", + alignItems: "center", + }, + deleteButton: { + justifyContent: "center", + alignItems: "center", + width: ACTION_WIDTH, + height: "100%", + paddingHorizontal: 10, + }, + actionContent: { + justifyContent: "center", + alignItems: "center", + gap: 4, + }, + actionText: { + fontSize: 12, + fontWeight: "600", + textAlign: "center", + }, +}); 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 b12e8fc..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({ @@ -50,3 +50,30 @@ export const useCreateTablo = () => { }, }); }; + +// Delete tablo (soft delete) +export const useDeleteTablo = () => { + const session = useAuthStore((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." + ); + }, + }); +}; diff --git a/xtablo-expo/hooks/user.ts b/xtablo-expo/hooks/user.ts index 683b313..4c7cab0 100644 --- a/xtablo-expo/hooks/user.ts +++ b/xtablo-expo/hooks/user.ts @@ -1,14 +1,10 @@ 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"> & { - streamToken: string | null; -}; +import { User } from "@/types/user.types"; 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/package-lock.json b/xtablo-expo/package-lock.json index e05e87b..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", @@ -59,10 +60,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 +3364,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 +3525,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 +3583,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 +3753,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 +3800,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 +3821,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 +4042,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 +4335,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 +4571,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 +4789,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 +5376,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 +5447,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 +5529,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 +5566,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 +5616,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", @@ -5526,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", @@ -5588,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", @@ -5621,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", @@ -5764,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", @@ -5954,6 +6309,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 +6461,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 +8110,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 +8484,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 +9449,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 +10090,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 +10420,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 +10653,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 +10925,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 +11024,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 +11534,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 +11799,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 +11888,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", @@ -11758,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", @@ -11874,6 +12380,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 +12412,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 e4739c7..77cd438 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", @@ -56,9 +59,7 @@ "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" + "expo-dev-client": "~5.2.4" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -66,10 +67,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/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} ); 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/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) => { 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" +>;