From 1ab7c2a180506c99fbf5085377854fabc7fb09aa Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Fri, 18 Jul 2025 23:10:09 +0200 Subject: [PATCH] Big improvements on the app --- xtablo-expo/app/(home)/(tabs)/_layout.tsx | 84 +- xtablo-expo/app/(home)/(tabs)/index.tsx | 379 +++++- xtablo-expo/app/(home)/(tabs)/planning.tsx | 27 +- xtablo-expo/app/(home)/(tabs)/tablos.tsx | 1046 +++++++++++++++++ xtablo-expo/app/(home)/channel/[cid].tsx | 178 ++- .../app/(home)/{(tabs) => user}/profile.tsx | 0 .../components/ui/TabBarBackground.tsx | 70 +- xtablo-expo/constants/Colors.ts | 46 +- xtablo-expo/constants/colors.ts | 52 + xtablo-expo/hooks/tablos.ts | 4 +- xtablo-expo/hooks/useThemeColor.ts | 6 +- 11 files changed, 1826 insertions(+), 66 deletions(-) create mode 100644 xtablo-expo/app/(home)/(tabs)/tablos.tsx rename xtablo-expo/app/(home)/{(tabs) => user}/profile.tsx (100%) create mode 100644 xtablo-expo/constants/colors.ts diff --git a/xtablo-expo/app/(home)/(tabs)/_layout.tsx b/xtablo-expo/app/(home)/(tabs)/_layout.tsx index 1b1d89f..a694b39 100644 --- a/xtablo-expo/app/(home)/(tabs)/_layout.tsx +++ b/xtablo-expo/app/(home)/(tabs)/_layout.tsx @@ -1,46 +1,112 @@ import { Tabs } from "expo-router"; import React from "react"; +import { Platform } from "react-native"; import { HapticTab } from "@/components/HapticTab"; import TabBarBackground from "@/components/ui/TabBarBackground"; import { Colors } from "@/constants/Colors"; import { useColorScheme } from "@/hooks/useColorScheme"; -import { MessageCircle, Calendar, User } from "lucide-react-native"; +import { + MessageCircle, + Calendar, + List, + Home, + Grid3X3, +} from "lucide-react-native"; export default function TabLayout() { const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + return ( ( - + tabBarIcon: ({ focused, color, size }) => ( + ), + tabBarLabel: "Discussions", }} /> ( - + tabBarIcon: ({ focused, color, size }) => ( + ), + tabBarLabel: "Planning", }} /> , + title: "Tablos", + tabBarIcon: ({ focused, color, size }) => ( + + ), + tabBarLabel: "Tablos", + // Optional: Add a badge for notifications + tabBarBadge: undefined, // You can set this to a number for notifications }} /> diff --git a/xtablo-expo/app/(home)/(tabs)/index.tsx b/xtablo-expo/app/(home)/(tabs)/index.tsx index 99a4c20..fc9e6ec 100644 --- a/xtablo-expo/app/(home)/(tabs)/index.tsx +++ b/xtablo-expo/app/(home)/(tabs)/index.tsx @@ -1,17 +1,384 @@ import { router } from "expo-router"; import { ChannelList } from "stream-chat-expo"; 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" }; + // Create a wrapper component for the avatar that has access to tablos data + const AvatarWithTablos = ({ channel }: { channel: any }) => ( + + ); + return ( - { - router.push(`/channel/${channel.cid}`); - }} - /> + + + + {/* Beautiful Header */} + + + + + Discussions + + Gérez les conversations de vos tablos + + + + + + + + + + {/* Decorative Elements + + */} + + + {/* Channel List */} + + { + router.push(`/channel/${channel.cid}`); + }} + 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)/(tabs)/planning.tsx b/xtablo-expo/app/(home)/(tabs)/planning.tsx index 2dfc85e..b84d52a 100644 --- a/xtablo-expo/app/(home)/(tabs)/planning.tsx +++ b/xtablo-expo/app/(home)/(tabs)/planning.tsx @@ -30,6 +30,7 @@ import { useEventsByTablo, useCreateEvent } from "@/hooks/events"; import { EventAndTablo, EventInsert } from "@/types/events.types"; import { useTablosList } from "@/hooks/tablos"; import { UserTablo } from "@/types/tablos.types"; +import { ColorMap } from "@/constants/colors"; type ViewMode = "month" | "week"; @@ -54,7 +55,6 @@ const getDateFormatted = (date: Date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); - console.log({ year, month, day }); return `${year}-${month}-${day}`; }; @@ -175,23 +175,6 @@ export default function PlanningScreen() { setShowCreateEventModal(false); }; - const colorMap: Record = { - "bg-blue-500": "#3b82f6", - "bg-green-500": "#10b981", - "bg-red-500": "#ef4444", - "bg-yellow-500": "#f59e0b", - "bg-purple-500": "#8b5cf6", - "bg-pink-500": "#ec4899", - "bg-gray-500": "#6b7280", - "bg-orange-500": "#f5100b", - "bg-teal-500": "#0d9488", - "bg-indigo-500": "#6366f1", - "bg-lime-500": "#84cc16", - "bg-emerald-500": "#10b981", - "bg-cyan-500": "#06b6d4", - "bg-fuchsia-500": "#d946ef", - }; - const renderTabloOption = ({ item }: { item: UserTablo }) => ( @@ -312,7 +295,7 @@ export default function PlanningScreen() { styles.weekEventCircle, { backgroundColor: - colorMap[event.tablo_color ?? "bg-gray-500"], + ColorMap[event.tablo_color ?? "bg-gray-500"], }, ]} /> @@ -464,7 +447,7 @@ export default function PlanningScreen() { styles.tabloColorDot, { backgroundColor: - colorMap[selectedTablo?.color ?? "bg-gray-500"], + ColorMap[selectedTablo?.color ?? "bg-gray-500"], }, ]} /> @@ -703,7 +686,7 @@ export default function PlanningScreen() { styles.tabloColorDot, { backgroundColor: - colorMap[item.color ?? "bg-gray-500"], + ColorMap[item.color ?? "bg-gray-500"], }, ]} /> diff --git a/xtablo-expo/app/(home)/(tabs)/tablos.tsx b/xtablo-expo/app/(home)/(tabs)/tablos.tsx new file mode 100644 index 0000000..dd58976 --- /dev/null +++ b/xtablo-expo/app/(home)/(tabs)/tablos.tsx @@ -0,0 +1,1046 @@ +import React, { useState } from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + StatusBar, + FlatList, + RefreshControl, + Alert, + Dimensions, + Modal, + TextInput, + ScrollView, + KeyboardAvoidingView, + Platform, +} from "react-native"; +import { LinearGradient } from "expo-linear-gradient"; +import { useCreateTablo, useTablosList } from "@/hooks/tablos"; +import { UserTablo } from "@/types/tablos.types"; +import { + Plus, + Search, + Filter, + Grid3X3, + List, + Calendar, + MessageCircle, + X, +} from "lucide-react-native"; +import { router } from "expo-router"; +import { AVAILABLE_COLORS, ColorMap } from "@/constants/colors"; +import { useAuth } from "@/stores/auth"; + +const { width } = Dimensions.get("window"); +const numColumns = 2; +const itemMargin = 16; +const itemWidth = (width - itemMargin * 3) / numColumns; + +export default function TablosScreen() { + const { data: tablos, isLoading, error, refetch } = useTablosList(); + const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); + const [filterStatus, setFilterStatus] = useState< + "all" | "todo" | "in_progress" | "done" + >("all"); + const [refreshing, setRefreshing] = useState(false); + const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); + const { mutate: createTablo } = useCreateTablo(); + + const [newTablo, setNewTablo] = useState<{ + name: string; + color: string; + status: "todo" | "in_progress" | "done"; + }>({ + name: "", + color: "bg-blue-500", + status: "todo", + }); + + const onRefresh = async () => { + setRefreshing(true); + await refetch(); + setRefreshing(false); + }; + + const getStatusLabel = (status: string) => { + switch (status) { + case "todo": + return "À faire"; + case "in_progress": + return "En cours"; + case "done": + return "Terminé"; + default: + return "À faire"; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "todo": + return "#f3f4f6"; + case "in_progress": + return "#dbeafe"; + case "done": + return "#dcfce7"; + default: + return "#6b7280"; + } + }; + + const getTextColor = (status: string) => { + switch (status) { + case "todo": + return "#364153"; + case "in_progress": + return "#1447e6"; + case "done": + return "#008236"; + default: + return "#000"; + } + }; + + const getBorderColor = (status: string) => { + switch (status) { + case "todo": + return "#d4d4d7"; + case "in_progress": + return "#80b6fc"; + case "done": + return "#62de80"; + } + }; + + const getChannelCid = (tabloId: string) => { + return `messaging:${tabloId}`; + }; + + const filteredTablos = tablos?.filter((tablo) => { + if (filterStatus === "all") return true; + return tablo.status === filterStatus; + }); + + const navigateToTablo = (tablo: UserTablo) => { + // Navigate to tablo details or chat + router.push(`/channel/${getChannelCid(tablo.id)}`); + }; + + const showFilterOptions = () => { + Alert.alert( + "Filtrer par statut", + "Choisissez un statut pour filtrer vos tablos", + [ + { text: "Tous", onPress: () => setFilterStatus("all") }, + { text: "À faire", onPress: () => setFilterStatus("todo") }, + { text: "En cours", onPress: () => setFilterStatus("in_progress") }, + { text: "Terminé", onPress: () => setFilterStatus("done") }, + { text: "Annuler", style: "cancel" }, + ] + ); + }; + + const handleCreateTablo = async () => { + if (!newTablo.name) { + Alert.alert("Erreur", "Le nom du tablo ne peut pas être vide."); + return; + } + + createTablo(newTablo); + setIsCreateModalVisible(false); + setNewTablo({ + name: "", + color: "bg-blue-500", + status: "todo", + }); + }; + + const renderTabloCard = ({ item: tablo }: { item: UserTablo }) => { + const initials = tablo.name?.charAt(0)?.toUpperCase() || "T"; + + return ( + navigateToTablo(tablo)} + activeOpacity={0.8} + > + {/* Tablo Image/Color Header */} + + {tablo.image ? ( + + {/* In a real app, you'd use an Image component here */} + {initials} + + ) : ( + {initials} + )} + + + {/* Tablo Info */} + + + + {tablo.name} + + + + {getStatusLabel(tablo.status)} + + + + + + + + + {tablo.is_admin ? "Admin" : "Membre"} + + + + + + + router.push(`/channel/${getChannelCid(tablo.id)}`) + } + > + + + router.push("/planning")} + > + + + + + + + ); + }; + + const renderListItem = ({ item: tablo }: { item: UserTablo }) => { + return ( + navigateToTablo(tablo)} + activeOpacity={0.8} + > + + + {tablo.name?.charAt(0)?.toUpperCase() || "T"} + + + + + + + {tablo.name} + + + + {getStatusLabel(tablo.status)} + + + + + + + + {tablo.is_admin ? "Admin" : "Membre"} + + + + + + + router.push(`/channel/${getChannelCid(tablo.id)}`) + } + > + + + router.push("/planning")} + > + + + + + + + ); + }; + + if (error) { + return ( + + + + Erreur + + Impossible de charger les tablos. Veuillez réessayer. + + refetch()} + > + Réessayer + + + + ); + } + + return ( + + + + {/* Beautiful Header */} + + + {/* Bottom Row */} + + + Mes Tablos + + {filteredTablos?.length || 0} tablo + {(filteredTablos?.length || 0) > 1 ? "s" : ""} + {filterStatus !== "all" && ` • ${getStatusLabel(filterStatus)}`} + + + + + + + + + + + + setViewMode(viewMode === "grid" ? "list" : "grid") + } + > + {viewMode === "grid" ? ( + + ) : ( + + )} + + + + + + {/* Decorative Elements */} + + + + + {/* Content */} + + {isLoading && !refreshing ? ( + + Chargement de vos tablos... + + ) : filteredTablos && filteredTablos.length > 0 ? ( + item.id} + numColumns={viewMode === "grid" ? numColumns : 1} + key={viewMode} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + refreshControl={ + + } + /> + ) : ( + + Aucun tablo trouvé + + {filterStatus === "all" + ? "Vous n'avez encore aucun tablo. Créez votre premier tablo pour commencer !" + : `Aucun tablo avec le statut "${getStatusLabel( + filterStatus + )}"`} + + {filterStatus === "all" && ( + + + + Créer mon premier tablo + + + )} + + )} + + + {/* Floating Action Button */} + { + setIsCreateModalVisible(true); + }} + activeOpacity={0.8} + > + + + + {/* Create Tablo Modal */} + setIsCreateModalVisible(false)} + > + + + setIsCreateModalVisible(false)} + > + + + Nouveau Tablo + + + Nom du Tablo + + setNewTablo({ ...newTablo, name: text }) + } + /> + + + Couleur + + {AVAILABLE_COLORS.map((color) => ( + setNewTablo({ ...newTablo, color })} + /> + ))} + + + + Statut + + {(["todo", "in_progress", "done"] as const).map((status) => ( + setNewTablo({ ...newTablo, status })} + > + + {getStatusLabel(status)} + + + ))} + + + + + Créer Tablo + + + + + + ); +} + +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: { + flex: 1, + }, + greetingContainer: { + flex: 1, + }, + greeting: { + fontSize: 16, + color: "rgba(255, 255, 255, 0.9)", + fontWeight: "500", + }, + userName: { + fontSize: 20, + color: "white", + fontWeight: "bold", + marginTop: 2, + }, + actionButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: "rgba(255, 255, 255, 0.2)", + justifyContent: "center", + alignItems: "center", + }, + 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", + }, + headerActions: { + flexDirection: "row", + gap: 12, + }, + 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, + }, + filterButton: { + 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, + }, + viewToggle: { + 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)", + }, + contentContainer: { + flex: 1, + backgroundColor: "#f8fafc", + marginTop: -10, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + paddingTop: 10, + }, + listContent: { + padding: 16, + }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + loadingText: { + fontSize: 16, + color: "#6b7280", + fontWeight: "500", + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + paddingHorizontal: 40, + }, + emptyTitle: { + fontSize: 22, + fontWeight: "bold", + color: "#1f2937", + marginBottom: 8, + textAlign: "center", + }, + emptyMessage: { + fontSize: 16, + color: "#6b7280", + textAlign: "center", + lineHeight: 24, + marginBottom: 32, + }, + createButton: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#3b82f6", + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 25, + gap: 8, + }, + createButtonText: { + color: "white", + fontSize: 16, + fontWeight: "600", + }, + tabloCard: { + backgroundColor: "white", + borderRadius: 16, + marginBottom: 16, + marginHorizontal: 8, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 8, + elevation: 3, + overflow: "hidden", + }, + tabloHeader: { + height: 120, + justifyContent: "center", + alignItems: "center", + position: "relative", + }, + imageContainer: { + width: "100%", + height: "100%", + justifyContent: "center", + alignItems: "center", + }, + tabloInitials: { + fontSize: 32, + fontWeight: "bold", + color: "white", + }, + + tabloInfo: { + padding: 16, + }, + nameRow: { + flexDirection: "row", + alignItems: "flex-start", + justifyContent: "space-between", + marginBottom: 12, + gap: 8, + }, + tabloName: { + fontSize: 18, + fontWeight: "bold", + color: "#1f2937", + flex: 1, + }, + nameStatusBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + alignSelf: "flex-start", + }, + nameStatusText: { + fontSize: 10, + fontWeight: "600", + textTransform: "uppercase", + }, + tabloMeta: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + roleContainer: { + flex: 1, + }, + roleBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 8, + alignSelf: "flex-start", + }, + roleText: { + color: "white", + fontSize: 12, + fontWeight: "600", + }, + actionsRow: { + flexDirection: "row", + gap: 8, + }, + cardActionButton: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: "#f3f4f6", + justifyContent: "center", + alignItems: "center", + }, + listItem: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "white", + padding: 16, + marginBottom: 8, + borderRadius: 12, + shadowColor: "#000", + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 2, + }, + listAvatar: { + width: 50, + height: 50, + borderRadius: 13, + justifyContent: "center", + alignItems: "center", + marginRight: 16, + }, + listInitials: { + fontSize: 18, + fontWeight: "bold", + color: "white", + }, + listItemContent: { + flex: 1, + }, + listNameRow: { + flexDirection: "row", + alignItems: "flex-start", + justifyContent: "space-between", + marginBottom: 4, + gap: 8, + }, + listNameContainer: { + flexDirection: "row", + alignItems: "center", + flex: 1, + gap: 10, + }, + listColorIndicator: { + width: 12, + height: 12, + borderRadius: 6, + }, + listName: { + fontSize: 16, + fontWeight: "bold", + color: "#1f2937", + flex: 1, + }, + listMeta: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + listStatusBadge: { + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 8, + }, + listStatusText: { + fontSize: 10, + fontWeight: "600", + }, + listRoleContainer: { + flex: 1, + }, + listRoleBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 8, + alignSelf: "flex-start", + }, + listRoleText: { + color: "white", + fontSize: 12, + fontWeight: "600", + }, + listActionsRow: { + flexDirection: "row", + gap: 8, + }, + listActionButton: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: "#f3f4f6", + justifyContent: "center", + alignItems: "center", + }, + errorContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + paddingHorizontal: 40, + }, + errorTitle: { + fontSize: 24, + fontWeight: "bold", + color: "#dc2626", + marginBottom: 12, + }, + errorMessage: { + fontSize: 16, + color: "#6b7280", + textAlign: "center", + marginBottom: 24, + lineHeight: 24, + }, + retryButton: { + backgroundColor: "#3b82f6", + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, + }, + retryText: { + color: "white", + fontSize: 16, + fontWeight: "600", + }, + fab: { + position: "absolute", + bottom: 120, + right: 30, + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: "#3b82f6", + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + zIndex: 1000, + }, + modalOverlay: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "rgba(0, 0, 0, 0.5)", + }, + modalContent: { + backgroundColor: "white", + borderRadius: 20, + padding: 20, + width: "90%", + alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 10, + elevation: 10, + }, + modalCloseButton: { + alignSelf: "flex-end", + padding: 10, + }, + modalTitle: { + fontSize: 24, + fontWeight: "bold", + color: "#1f2937", + marginBottom: 20, + }, + modalFormGroup: { + marginBottom: 15, + }, + modalLabel: { + fontSize: 16, + color: "#6b7280", + marginBottom: 8, + }, + modalInput: { + borderWidth: 1, + borderColor: "#e5e7eb", + borderRadius: 12, + padding: 12, + fontSize: 16, + color: "#1f2937", + }, + colorPicker: { + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "space-between", + width: "100%", + marginTop: 10, + }, + colorOption: { + width: "18%", + height: 20, + aspectRatio: 1, + borderRadius: 15, + marginBottom: 10, + borderWidth: 2, + borderColor: "transparent", + }, + selectedColorOption: { + borderColor: "#000", + }, + statusPicker: { + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "space-between", + width: "100%", + marginTop: 10, + }, + statusOption: { + width: "28%", + height: 40, + borderRadius: 20, + margin: 5, + borderWidth: 2, + borderColor: "transparent", + justifyContent: "center", + alignItems: "center", + }, + selectedStatusOption: { + borderColor: "#3b82f6", + }, + statusOptionText: { + fontSize: 14, + fontWeight: "600", + }, + modalButton: { + backgroundColor: "#3b82f6", + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 25, + width: "100%", + alignItems: "center", + marginTop: 20, + }, + modalButtonText: { + color: "white", + fontSize: 16, + fontWeight: "600", + }, +}); diff --git a/xtablo-expo/app/(home)/channel/[cid].tsx b/xtablo-expo/app/(home)/channel/[cid].tsx index 1e3578e..5973776 100644 --- a/xtablo-expo/app/(home)/channel/[cid].tsx +++ b/xtablo-expo/app/(home)/channel/[cid].tsx @@ -1,38 +1,192 @@ import { useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; -import { ActivityIndicator, SafeAreaView } from "react-native"; -import { Channel as ChannelType } from "stream-chat"; +import { + ActivityIndicator, + SafeAreaView, + View, + Text, + StyleSheet, +} from "react-native"; import { Channel, MessageInput, MessageList, useChatContext, + useChannelContext, } from "stream-chat-expo"; +import { MessageCircle, Users, Smile } from "lucide-react-native"; +import { useEffect, useState } from "react"; export default function ChannelScreen() { - const [channel, setChannel] = useState(null); const { cid } = useLocalSearchParams<{ cid: string }>(); - + const [type, id] = cid.split(":"); const { client } = useChatContext(); + const channel = client.channel(type, id); + const [hasMessages, setHasMessages] = useState(false); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { - const fetchChannel = async () => { - const channels = await client.queryChannels({ cid }); - setChannel(channels[0]); - }; - fetchChannel(); - }, [cid]); + if (channel) { + const checkMessages = async () => { + try { + // Get channel state to check for messages + await channel.watch(); + const messages = channel.state.messages || []; + setHasMessages(messages.length > 0); + } catch (error) { + } finally { + setIsLoading(false); + } + }; + + checkMessages(); + + // Listen for new messages + const handleNewMessage = () => { + setHasMessages(true); + }; + + channel.on("message.new", handleNewMessage); + + return () => { + channel.off("message.new", handleNewMessage); + }; + } + }, [channel]); if (!channel) { return ; } + const EmptyState = () => ( + + + + + + + + + + + + + + Commencez la conversation + + Soyez le premier à envoyer un message dans ce canal ! + + + ); + return ( - + {isLoading ? ( + + + Chargement des messages... + + ) : hasMessages ? ( + + ) : ( + + )} ); } + +const styles = StyleSheet.create({ + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#f8fafc", + gap: 16, + }, + loadingText: { + fontSize: 16, + color: "#6b7280", + fontWeight: "500", + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + paddingHorizontal: 40, + backgroundColor: "#f8fafc", + }, + emptyIconContainer: { + position: "relative", + marginBottom: 32, + width: 120, + height: 120, + justifyContent: "center", + alignItems: "center", + }, + decorativeElements: { + position: "absolute", + width: "100%", + height: "100%", + }, + floatingIcon1: { + position: "absolute", + top: 10, + right: 5, + backgroundColor: "white", + borderRadius: 15, + padding: 6, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + floatingIcon2: { + position: "absolute", + bottom: 15, + left: 8, + backgroundColor: "white", + borderRadius: 12, + padding: 5, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 2, + }, + 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", + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 20, + borderWidth: 1, + borderColor: "#e5e7eb", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 4, + elevation: 1, + }, + emptyHintText: { + fontSize: 14, + color: "#9ca3af", + fontWeight: "500", + textAlign: "center", + }, +}); diff --git a/xtablo-expo/app/(home)/(tabs)/profile.tsx b/xtablo-expo/app/(home)/user/profile.tsx similarity index 100% rename from xtablo-expo/app/(home)/(tabs)/profile.tsx rename to xtablo-expo/app/(home)/user/profile.tsx diff --git a/xtablo-expo/components/ui/TabBarBackground.tsx b/xtablo-expo/components/ui/TabBarBackground.tsx index 70d1c3c..d1760e3 100644 --- a/xtablo-expo/components/ui/TabBarBackground.tsx +++ b/xtablo-expo/components/ui/TabBarBackground.tsx @@ -1,6 +1,70 @@ -// This is a shim for web and Android where the tab bar is generally opaque. -export default undefined; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import { LinearGradient } from "expo-linear-gradient"; +import { useColorScheme } from "@/hooks/useColorScheme"; + +export default function TabBarBackground() { + const colorScheme = useColorScheme(); + const isDark = colorScheme === "dark"; + + return ( + + + + {/* Subtle top border gradient */} + + + {/* Optional: Add some decorative elements */} + + + ); +} export function useBottomTabOverflow() { - return 0; + return 24; // Account for the rounded corners } + +const styles = StyleSheet.create({ + topBorder: { + position: "absolute", + top: 0, + left: 0, + right: 0, + height: 2, + }, + decorativeCircle: { + position: "absolute", + top: -20, + right: 20, + width: 80, + height: 80, + borderRadius: 40, + }, +}); diff --git a/xtablo-expo/constants/Colors.ts b/xtablo-expo/constants/Colors.ts index 14e6784..abbd2db 100644 --- a/xtablo-expo/constants/Colors.ts +++ b/xtablo-expo/constants/Colors.ts @@ -3,24 +3,50 @@ * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. */ -const tintColorLight = '#0a7ea4'; -const tintColorDark = '#fff'; +const tintColorLight = "#0a7ea4"; +const tintColorDark = "#fff"; export const Colors = { light: { - text: '#11181C', - background: '#fff', + text: "#11181C", + background: "#fff", tint: tintColorLight, - icon: '#687076', - tabIconDefault: '#687076', + icon: "#687076", + tabIconDefault: "#687076", tabIconSelected: tintColorLight, }, dark: { - text: '#ECEDEE', - background: '#151718', + text: "#ECEDEE", + background: "#151718", tint: tintColorDark, - icon: '#9BA1A6', - tabIconDefault: '#9BA1A6', + icon: "#9BA1A6", + tabIconDefault: "#9BA1A6", tabIconSelected: tintColorDark, }, }; + +export const AVAILABLE_COLORS = [ + "bg-blue-500", + "bg-green-500", + "bg-purple-500", + "bg-red-500", + "bg-yellow-500", + "bg-indigo-500", + "bg-pink-500", + "bg-teal-500", + "bg-orange-500", + "bg-cyan-500", +]; + +export const ColorMap: Record<(typeof AVAILABLE_COLORS)[number], string> = { + "bg-blue-500": "#3b82f6", + "bg-green-500": "#10b981", + "bg-red-500": "#ef4444", + "bg-yellow-500": "#f59e0b", + "bg-purple-500": "#8b5cf6", + "bg-pink-500": "#ec4899", + "bg-orange-500": "#f97316", + "bg-teal-500": "#0d9488", + "bg-indigo-500": "#6366f1", + "bg-cyan-500": "#06b6d4", +}; diff --git a/xtablo-expo/constants/colors.ts b/xtablo-expo/constants/colors.ts new file mode 100644 index 0000000..abbd2db --- /dev/null +++ b/xtablo-expo/constants/colors.ts @@ -0,0 +1,52 @@ +/** + * Below are the colors that are used in the app. The colors are defined in the light and dark mode. + * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. + */ + +const tintColorLight = "#0a7ea4"; +const tintColorDark = "#fff"; + +export const Colors = { + light: { + text: "#11181C", + background: "#fff", + tint: tintColorLight, + icon: "#687076", + tabIconDefault: "#687076", + tabIconSelected: tintColorLight, + }, + dark: { + text: "#ECEDEE", + background: "#151718", + tint: tintColorDark, + icon: "#9BA1A6", + tabIconDefault: "#9BA1A6", + tabIconSelected: tintColorDark, + }, +}; + +export const AVAILABLE_COLORS = [ + "bg-blue-500", + "bg-green-500", + "bg-purple-500", + "bg-red-500", + "bg-yellow-500", + "bg-indigo-500", + "bg-pink-500", + "bg-teal-500", + "bg-orange-500", + "bg-cyan-500", +]; + +export const ColorMap: Record<(typeof AVAILABLE_COLORS)[number], string> = { + "bg-blue-500": "#3b82f6", + "bg-green-500": "#10b981", + "bg-red-500": "#ef4444", + "bg-yellow-500": "#f59e0b", + "bg-purple-500": "#8b5cf6", + "bg-pink-500": "#ec4899", + "bg-orange-500": "#f97316", + "bg-teal-500": "#0d9488", + "bg-indigo-500": "#6366f1", + "bg-cyan-500": "#06b6d4", +}; diff --git a/xtablo-expo/hooks/tablos.ts b/xtablo-expo/hooks/tablos.ts index 290ee38..b12e8fc 100644 --- a/xtablo-expo/hooks/tablos.ts +++ b/xtablo-expo/hooks/tablos.ts @@ -4,6 +4,7 @@ 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 { Alert } from "react-native"; // type TabloInsert = Tablo["Insert"]; // type TabloUpdate = Tablo["Update"]; @@ -22,6 +23,7 @@ export const useTablosList = () => { const tablos = data as UserTablo[]; return tablos; }, + refetchInterval: 1000 * 60 * 5, // 5 minutes }); }; @@ -44,7 +46,7 @@ export const useCreateTablo = () => { queryClient.invalidateQueries({ queryKey: ["tablos"] }); }, onError: (error) => { - console.error(error); + Alert.alert("Erreur", "Impossible de créer le tablo."); }, }); }; diff --git a/xtablo-expo/hooks/useThemeColor.ts b/xtablo-expo/hooks/useThemeColor.ts index 0608e73..53abf04 100644 --- a/xtablo-expo/hooks/useThemeColor.ts +++ b/xtablo-expo/hooks/useThemeColor.ts @@ -3,14 +3,14 @@ * https://docs.expo.dev/guides/color-schemes/ */ -import { Colors } from '@/constants/Colors'; -import { useColorScheme } from '@/hooks/useColorScheme'; +import { Colors } from "@/constants/colors"; +import { useColorScheme } from "@/hooks/useColorScheme"; export function useThemeColor( props: { light?: string; dark?: string }, colorName: keyof typeof Colors.light & keyof typeof Colors.dark ) { - const theme = useColorScheme() ?? 'light'; + const theme = useColorScheme() ?? "light"; const colorFromProps = props[theme]; if (colorFromProps) {