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"
+>;