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) {