Merge branch 'develop'
This commit is contained in:
commit
b285c213c8
36 changed files with 3687 additions and 1166 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
});
|
||||
|
|
|
|||
7
justfile
7
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
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleExportICS}
|
||||
disabled={!tabloEvents || tabloEvents.length === 0}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
||||
title="Exporter en format ICS"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Exporter</span>
|
||||
</button>
|
||||
<div className="flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
|
||||
{(["month", "week", "day"] as ViewType[]).map((view) => (
|
||||
<button
|
||||
key={view}
|
||||
onClick={() => setCurrentView(view)}
|
||||
title={`${
|
||||
view === "month"
|
||||
? "Mois"
|
||||
: view === "week"
|
||||
? "Semaine"
|
||||
: "Jour"
|
||||
} (${
|
||||
view === "month"
|
||||
? "M ou 1"
|
||||
: view === "week"
|
||||
? "W ou 2"
|
||||
: "D ou 3"
|
||||
})`}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors capitalize ${
|
||||
currentView === view
|
||||
? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-sm"
|
||||
|
|
@ -748,6 +842,13 @@ export const PlanningPage = () => {
|
|||
: view === "week"
|
||||
? "Semaine"
|
||||
: "Jour"}
|
||||
<span className="ml-1 text-xs opacity-60">
|
||||
{view === "month"
|
||||
? "(M)"
|
||||
: view === "week"
|
||||
? "(S)"
|
||||
: "(J)"}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"expo": {
|
||||
"name": "xtablo",
|
||||
"slug": "xtablo",
|
||||
"slug": "xtablo-expo",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
|
|
@ -108,6 +108,21 @@ export default function TabLayout() {
|
|||
tabBarBadge: undefined, // You can set this to a number for notifications
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
title: "Paramètres",
|
||||
tabBarIcon: ({ focused, color, size }) => (
|
||||
<Settings
|
||||
size={focused ? 24 : 22}
|
||||
color={color}
|
||||
strokeWidth={focused ? 2.2 : 2}
|
||||
/>
|
||||
),
|
||||
tabBarLabel: "Paramètres",
|
||||
tabBarBadge: undefined,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
865
xtablo-expo/app/(app)/(tabs)/index.tsx
Normal file
865
xtablo-expo/app/(app)/(tabs)/index.tsx
Normal file
|
|
@ -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 (
|
||||
<Text
|
||||
style={[styles.customChannelTitle, { color: textColor }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channelName}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
// Custom Preview Component with Swipe to Archive
|
||||
// Swipe left on any channel to reveal the archive button
|
||||
const CustomChannelPreview = (
|
||||
props: ChannelPreviewMessengerProps<DefaultStreamChatGenerics>
|
||||
) => {
|
||||
return (
|
||||
<SwipeableChannelPreview channel={props.channel}>
|
||||
<ChannelPreviewMessenger {...props} />
|
||||
</SwipeableChannelPreview>
|
||||
);
|
||||
};
|
||||
|
||||
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<Record<string, number>>({});
|
||||
|
||||
// Search animation state
|
||||
// const [isSearchVisible, setIsSearchVisible] = useState(false);
|
||||
// const [searchQuery, setSearchQuery] = useState("");
|
||||
// const [debouncedSearchQuery, setDebouncedSearchQuery] = useState("");
|
||||
const searchAnimation = useSharedValue(0);
|
||||
// const searchInputRef = useRef<TextInput>(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 (
|
||||
<View style={styles.avatarContainer}>
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
style={styles.avatarGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Text style={styles.avatarInitials}>{initials}</Text>
|
||||
|
||||
{/* Member count indicator for group channels */}
|
||||
{memberCount > 2 && (
|
||||
<View style={styles.memberCountBadge}>
|
||||
<Text style={styles.memberCountText}>{memberCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Decorative ring */}
|
||||
<View
|
||||
style={[
|
||||
styles.avatarRing,
|
||||
{ borderColor: `${ColorMap[tabloColor]}30` },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Status indicator (online/active) */}
|
||||
<View
|
||||
style={[styles.statusIndicator, { borderColor: cardBackgroundColor }]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 = () => (
|
||||
// <Animated.View style={[styles.searchHeaderContainer, animatedSearchStyle]}>
|
||||
// <View style={styles.searchInputContainer}>
|
||||
// <Search size={18} color="#6b7280" style={styles.searchIcon} />
|
||||
// <TextInput
|
||||
// ref={searchInputRef}
|
||||
// style={styles.searchInput}
|
||||
// placeholder="Rechercher une conversation..."
|
||||
// placeholderTextColor="#9ca3af"
|
||||
// value={searchQuery}
|
||||
// onChangeText={setSearchQuery}
|
||||
// returnKeyType="search"
|
||||
// autoCapitalize="none"
|
||||
// autoCorrect={false}
|
||||
// onSubmitEditing={() => {
|
||||
// console.log("Searching for:", searchQuery);
|
||||
// Keyboard.dismiss();
|
||||
// }}
|
||||
// />
|
||||
// {searchQuery.length > 0 && (
|
||||
// <TouchableOpacity
|
||||
// onPress={() => {
|
||||
// setSearchQuery("");
|
||||
// setDebouncedSearchQuery("");
|
||||
// searchInputRef.current?.focus();
|
||||
// }}
|
||||
// style={styles.clearButton}
|
||||
// >
|
||||
// <X size={16} color="#6b7280" />
|
||||
// </TouchableOpacity>
|
||||
// )}
|
||||
// </View>
|
||||
|
||||
// {/* Search Results Info */}
|
||||
// {debouncedSearchQuery.trim() && (
|
||||
// <View style={styles.searchInfoContainer}>
|
||||
// <Text style={styles.searchInfoText}>
|
||||
// Recherche: "{debouncedSearchQuery}"
|
||||
// </Text>
|
||||
// </View>
|
||||
// )}
|
||||
// </Animated.View>
|
||||
// );
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor }]}>
|
||||
<StatusBar
|
||||
barStyle={colorScheme === "dark" ? "light-content" : "light-content"}
|
||||
backgroundColor={gradientColors[0]}
|
||||
/>
|
||||
|
||||
{/* Loading Header */}
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerBottom}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.headerTitle}>Discussions</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Chargement de vos conversations...
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} />
|
||||
</LinearGradient>
|
||||
|
||||
{/* Loading Content */}
|
||||
<View
|
||||
style={[
|
||||
styles.loadingContentContainer,
|
||||
{ backgroundColor: loadingColors.container },
|
||||
]}
|
||||
>
|
||||
{/* Loading Skeleton Items */}
|
||||
{[1, 2, 3, 4, 5].map((item) => (
|
||||
<View key={item} style={styles.loadingItem}>
|
||||
<View
|
||||
style={[
|
||||
styles.loadingAvatar,
|
||||
{ backgroundColor: loadingColors.item },
|
||||
]}
|
||||
/>
|
||||
<View style={styles.loadingTextContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.loadingTitle,
|
||||
{ backgroundColor: loadingColors.item },
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
styles.loadingSubtitle,
|
||||
{ backgroundColor: loadingColors.itemSecondary },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor }]}>
|
||||
<StatusBar
|
||||
barStyle={colorScheme === "dark" ? "light-content" : "light-content"}
|
||||
backgroundColor={gradientColors[0]}
|
||||
/>
|
||||
|
||||
{/* Beautiful Header */}
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerBottom}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.headerTitle}>Discussions</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
{/* {debouncedSearchQuery.trim()
|
||||
? `Recherche: "${debouncedSearchQuery}"`
|
||||
: "Gérez les conversations de vos tablos"} */}
|
||||
Gérez les conversations de vos tablos
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* <TouchableOpacity
|
||||
style={styles.searchButton}
|
||||
onPress={toggleSearch}
|
||||
>
|
||||
{isSearchVisible ? (
|
||||
<X size={20} color="#3b82f6" />
|
||||
) : (
|
||||
<Search size={20} color="#3b82f6" />
|
||||
)}
|
||||
</TouchableOpacity> */}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} />
|
||||
</LinearGradient>
|
||||
|
||||
{/* Channel List with animated search */}
|
||||
<View style={[styles.channelListContainer, { backgroundColor }]}>
|
||||
{/* <SearchHeader /> */}
|
||||
<ChannelList
|
||||
filters={filters}
|
||||
onSelect={(channel) => {
|
||||
// Close search when selecting a channel
|
||||
// if (isSearchVisible) {
|
||||
// toggleSearch();
|
||||
// }
|
||||
router.push(`/channel/${channel.cid}`);
|
||||
}}
|
||||
sort={sort}
|
||||
options={options}
|
||||
Preview={(props) => (
|
||||
<CustomChannelPreview
|
||||
{...props}
|
||||
// animations={animations}
|
||||
// cancelOtherAnimations={(id) => {
|
||||
// animations.value = {
|
||||
// [id]: 0,
|
||||
// };
|
||||
// }}
|
||||
/>
|
||||
)}
|
||||
PreviewAvatar={(props) => (
|
||||
<CustomChannelAvatar
|
||||
channel={props.channel}
|
||||
tablos={tablos || []}
|
||||
/>
|
||||
)}
|
||||
PreviewTitle={CustomChannelTitle}
|
||||
// ListHeaderComponent={SearchHeader}
|
||||
// Show loading state during search
|
||||
LoadingIndicator={() => (
|
||||
<View style={styles.searchLoadingContainer}>
|
||||
<Text
|
||||
style={[styles.searchLoadingText, { color: subtitleColor }]}
|
||||
>
|
||||
{/* {debouncedSearchQuery
|
||||
? "Recherche en cours..."
|
||||
: "Chargement..."} */}
|
||||
Chargement...
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
// Show empty state when no results
|
||||
EmptyStateIndicator={() => (
|
||||
<View style={styles.emptySearchContainer}>
|
||||
<Search size={48} color={emptyIconColor} />
|
||||
<Text
|
||||
style={[styles.emptySearchTitle, { color: emptyTextColor }]}
|
||||
>
|
||||
{/* {debouncedSearchQuery
|
||||
? "Aucun résultat"
|
||||
: "Aucune conversation"} */}
|
||||
Aucune conversation
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.emptySearchMessage, { color: subtitleColor }]}
|
||||
>
|
||||
{/* {debouncedSearchQuery
|
||||
? `Aucune conversation trouvée pour "${debouncedSearchQuery}"`
|
||||
: "Vous n'avez pas encore de conversations"} */}
|
||||
Vous n'avez pas encore de conversations
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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%",
|
||||
},
|
||||
});
|
||||
|
|
@ -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 }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.tabloOption}
|
||||
style={[styles.tabloOption, { borderBottomColor: borderColor }]}
|
||||
onPress={() => selectTablo(item)}
|
||||
>
|
||||
<View style={styles.tabloOptionLeft}>
|
||||
|
|
@ -190,7 +218,9 @@ export default function PlanningScreen() {
|
|||
]}
|
||||
/>
|
||||
<View style={styles.tabloOptionContent}>
|
||||
<Text style={styles.tabloOptionName}>{item.name}</Text>
|
||||
<Text style={[styles.tabloOptionName, { color: textColor }]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{selectedTablo?.id === item.id && <Check size={20} color="#3b82f6" />}
|
||||
|
|
@ -198,22 +228,31 @@ export default function PlanningScreen() {
|
|||
);
|
||||
|
||||
const renderEvent = ({ item }: { item: EventAndTablo }) => (
|
||||
<TouchableOpacity style={styles.eventCard}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.eventCard,
|
||||
{ backgroundColor: cardBackgroundColor, borderColor },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[styles.eventColorBar, { backgroundColor: item.tablo_color }]}
|
||||
/>
|
||||
<View style={styles.eventContent}>
|
||||
<Text style={styles.eventTitle}>{item.title}</Text>
|
||||
<Text style={[styles.eventTitle, { color: textColor }]}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<View style={styles.eventDetails}>
|
||||
<View style={styles.eventDetailItem}>
|
||||
<Clock size={14} color="#6b7280" />
|
||||
<Text style={styles.eventDetailText}>
|
||||
<Clock size={14} color={subtitleColor} />
|
||||
<Text style={[styles.eventDetailText, { color: subtitleColor }]}>
|
||||
{item.start_time?.substring(0, 5)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.eventDetailItem}>
|
||||
<MapPin size={14} color="#6b7280" />
|
||||
<Text style={styles.eventDetailText}>{item.tablo_name}</Text>
|
||||
<MapPin size={14} color={subtitleColor} />
|
||||
<Text style={[styles.eventDetailText, { color: subtitleColor }]}>
|
||||
{item.tablo_name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -234,7 +273,8 @@ export default function PlanningScreen() {
|
|||
<Text
|
||||
style={[
|
||||
styles.dayText,
|
||||
!isCurrentMonth(date) && styles.inactiveDayText,
|
||||
{ color: textColor },
|
||||
!isCurrentMonth(date) && { color: inactiveDayColor },
|
||||
isSelected(date) && styles.selectedDayText,
|
||||
isToday(date) && !isSelected(date) && styles.todayDayText,
|
||||
]}
|
||||
|
|
@ -253,6 +293,7 @@ export default function PlanningScreen() {
|
|||
<TouchableOpacity
|
||||
style={[
|
||||
styles.weekDayHeader,
|
||||
{ backgroundColor: cardBackgroundColor },
|
||||
isToday(date) && styles.todayWeekDay,
|
||||
isSelected(date) && styles.selectedWeekDay,
|
||||
isToday(date) && isSelected(date) && styles.todaySelectedWeekDay,
|
||||
|
|
@ -262,6 +303,7 @@ export default function PlanningScreen() {
|
|||
<Text
|
||||
style={[
|
||||
styles.weekDayName,
|
||||
{ color: subtitleColor },
|
||||
isSelected(date) && { color: "white" },
|
||||
isToday(date) && isSelected(date) && { color: "white" },
|
||||
]}
|
||||
|
|
@ -271,6 +313,7 @@ export default function PlanningScreen() {
|
|||
<Text
|
||||
style={[
|
||||
styles.weekDayNumber,
|
||||
{ color: textColor },
|
||||
isSelected(date) && styles.selectedWeekDayText,
|
||||
isToday(date) && !isSelected(date) && styles.todayWeekDayText,
|
||||
isToday(date) &&
|
||||
|
|
@ -301,8 +344,15 @@ export default function PlanningScreen() {
|
|||
/>
|
||||
))}
|
||||
{dayEvents.length > 6 && (
|
||||
<View style={styles.weekEventMoreCircle}>
|
||||
<Text style={styles.weekEventMoreText}>
|
||||
<View
|
||||
style={[
|
||||
styles.weekEventMoreCircle,
|
||||
{ backgroundColor: borderColor },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.weekEventMoreText, { color: subtitleColor }]}
|
||||
>
|
||||
+{dayEvents.length - 6}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -319,14 +369,16 @@ export default function PlanningScreen() {
|
|||
return (
|
||||
<View style={styles.selectedDayEventsContainer}>
|
||||
<View style={styles.selectedDayHeader}>
|
||||
<Text style={styles.selectedDayTitle}>
|
||||
<Text style={[styles.selectedDayTitle, { color: textColor }]}>
|
||||
{selectedDate.toLocaleDateString("fr-FR", {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
})}
|
||||
</Text>
|
||||
<Text style={styles.selectedDayEventsCount}>
|
||||
<Text
|
||||
style={[styles.selectedDayEventsCount, { color: subtitleColor }]}
|
||||
>
|
||||
{selectedDayEvents.length} événement
|
||||
{selectedDayEvents.length > 1 ? "s" : ""}
|
||||
</Text>
|
||||
|
|
@ -343,9 +395,11 @@ export default function PlanningScreen() {
|
|||
/>
|
||||
) : (
|
||||
<View style={styles.selectedDayEmptyState}>
|
||||
<CalendarIcon size={40} color="#d1d5db" />
|
||||
<Text style={styles.emptyStateTitle}>Aucun événement</Text>
|
||||
<Text style={styles.emptyStateText}>
|
||||
<CalendarIcon size={40} color={emptyIconColor} />
|
||||
<Text style={[styles.emptyStateTitle, { color: textColor }]}>
|
||||
Aucun événement
|
||||
</Text>
|
||||
<Text style={[styles.emptyStateText, { color: subtitleColor }]}>
|
||||
Vous n'avez aucun événement prévu pour cette date.
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -359,12 +413,17 @@ export default function PlanningScreen() {
|
|||
const todayEvents = getEventsForDate(selectedDate);
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||
<ScrollView
|
||||
style={[styles.container, { backgroundColor }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.headerTitle}>Planning</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
<Text style={[styles.headerTitle, { color: textColor }]}>
|
||||
Planning
|
||||
</Text>
|
||||
<Text style={[styles.headerSubtitle, { color: subtitleColor }]}>
|
||||
{viewMode === "month"
|
||||
? months[currentMonth.getMonth()] +
|
||||
" " +
|
||||
|
|
@ -387,7 +446,12 @@ export default function PlanningScreen() {
|
|||
|
||||
{/* View Mode Toggle */}
|
||||
<View style={styles.viewModeContainer}>
|
||||
<View style={styles.viewModeToggle}>
|
||||
<View
|
||||
style={[
|
||||
styles.viewModeToggle,
|
||||
{ backgroundColor: viewModeToggleColor },
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.viewModeButton,
|
||||
|
|
@ -397,11 +461,12 @@ export default function PlanningScreen() {
|
|||
>
|
||||
<Grid3x3
|
||||
size={18}
|
||||
color={viewMode === "month" ? "white" : "#6b7280"}
|
||||
color={viewMode === "month" ? "white" : subtitleColor}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.viewModeText,
|
||||
{ color: viewMode === "month" ? "white" : subtitleColor },
|
||||
viewMode === "month" && styles.activeViewModeText,
|
||||
]}
|
||||
>
|
||||
|
|
@ -417,11 +482,12 @@ export default function PlanningScreen() {
|
|||
>
|
||||
<Rows3
|
||||
size={18}
|
||||
color={viewMode === "week" ? "white" : "#6b7280"}
|
||||
color={viewMode === "week" ? "white" : subtitleColor}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.viewModeText,
|
||||
{ color: viewMode === "week" ? "white" : subtitleColor },
|
||||
viewMode === "week" && styles.activeViewModeText,
|
||||
]}
|
||||
>
|
||||
|
|
@ -434,35 +500,48 @@ export default function PlanningScreen() {
|
|||
{/* Tablo Selector */}
|
||||
<View style={styles.tabloSelectorContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.tabloSelector}
|
||||
style={[
|
||||
styles.tabloSelector,
|
||||
{ backgroundColor: cardBackgroundColor, borderColor },
|
||||
]}
|
||||
onPress={() => setShowTabloSelector(true)}
|
||||
>
|
||||
<View style={styles.tabloSelectorLeft}>
|
||||
<Table size={20} color="#6b7280" />
|
||||
<Table size={20} color={subtitleColor} />
|
||||
<View style={styles.tabloSelectorContent}>
|
||||
<Text style={styles.tabloSelectorLabel}>Tablo actuel</Text>
|
||||
<Text
|
||||
style={[styles.tabloSelectorLabel, { color: subtitleColor }]}
|
||||
>
|
||||
Tablo actuel
|
||||
</Text>
|
||||
<View style={styles.tabloSelectorRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.tabloColorDot,
|
||||
{
|
||||
backgroundColor:
|
||||
ColorMap[selectedTablo?.color ?? "bg-gray-500"],
|
||||
backgroundColor: selectedTablo?.color
|
||||
? ColorMap[selectedTablo.color]
|
||||
: subtitleColor,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Text style={styles.tabloSelectorName}>
|
||||
<Text style={[styles.tabloSelectorName, { color: textColor }]}>
|
||||
{selectedTablo?.name ?? "Tous les tablos"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<ChevronDown size={20} color="#6b7280" />
|
||||
<ChevronDown size={20} color={subtitleColor} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Calendar/Week View */}
|
||||
<Card containerStyle={styles.calendarCard}>
|
||||
<Card
|
||||
containerStyle={[
|
||||
styles.calendarCard,
|
||||
{ backgroundColor: cardBackgroundColor },
|
||||
]}
|
||||
>
|
||||
<View style={styles.calendarHeader}>
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
|
|
@ -473,7 +552,7 @@ export default function PlanningScreen() {
|
|||
>
|
||||
<ChevronLeft size={24} color="#3b82f6" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.calendarTitle}>
|
||||
<Text style={[styles.calendarTitle, { color: textColor }]}>
|
||||
{viewMode === "month"
|
||||
? `${
|
||||
months[currentMonth.getMonth()]
|
||||
|
|
@ -495,9 +574,17 @@ export default function PlanningScreen() {
|
|||
|
||||
{viewMode === "month" ? (
|
||||
<>
|
||||
<View style={styles.weekHeader}>
|
||||
<View
|
||||
style={[
|
||||
styles.weekHeader,
|
||||
{ borderBottomColor: weekHeaderBorderColor },
|
||||
]}
|
||||
>
|
||||
{daysOfWeek.map((day, i) => (
|
||||
<Text key={`${day}-${i}`} style={styles.weekDay}>
|
||||
<Text
|
||||
key={`${day}-${i}`}
|
||||
style={[styles.weekDay, { color: subtitleColor }]}
|
||||
>
|
||||
{day}
|
||||
</Text>
|
||||
))}
|
||||
|
|
@ -524,7 +611,7 @@ export default function PlanningScreen() {
|
|||
<View style={styles.eventsSection}>
|
||||
<View style={styles.eventsSectionHeader}>
|
||||
<CalendarIcon size={20} color="#3b82f6" />
|
||||
<Text style={styles.eventsSectionTitle}>
|
||||
<Text style={[styles.eventsSectionTitle, { color: textColor }]}>
|
||||
Événements du jour ({todayEvents.length})
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -539,9 +626,11 @@ export default function PlanningScreen() {
|
|||
/>
|
||||
) : (
|
||||
<View style={styles.selectedDayEmptyState}>
|
||||
<CalendarIcon size={40} color="#d1d5db" />
|
||||
<Text style={styles.emptyStateTitle}>Aucun événement</Text>
|
||||
<Text style={styles.emptyStateText}>
|
||||
<CalendarIcon size={40} color={emptyIconColor} />
|
||||
<Text style={[styles.emptyStateTitle, { color: textColor }]}>
|
||||
Aucun événement
|
||||
</Text>
|
||||
<Text style={[styles.emptyStateText, { color: subtitleColor }]}>
|
||||
Vous n'avez aucun événement prévu pour cette date.
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -557,13 +646,22 @@ export default function PlanningScreen() {
|
|||
onRequestClose={() => setShowTabloSelector(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
style={[styles.modalOverlay, { backgroundColor: modalOverlayColor }]}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowTabloSelector(false)}
|
||||
>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Choisir un tablo</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.modalContent,
|
||||
{ backgroundColor: cardBackgroundColor, borderColor },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[styles.modalHeader, { borderBottomColor: borderColor }]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, { color: textColor }]}>
|
||||
Choisir un tablo
|
||||
</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data={tablos ?? []}
|
||||
|
|
@ -572,7 +670,10 @@ export default function PlanningScreen() {
|
|||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={
|
||||
<TouchableOpacity
|
||||
style={styles.tabloOption}
|
||||
style={[
|
||||
styles.tabloOption,
|
||||
{ borderBottomColor: borderColor },
|
||||
]}
|
||||
onPress={() => selectTablo(null)}
|
||||
>
|
||||
<View style={styles.tabloOptionLeft}>
|
||||
|
|
@ -580,12 +681,14 @@ export default function PlanningScreen() {
|
|||
style={[
|
||||
styles.tabloColorDot,
|
||||
{
|
||||
backgroundColor: "#6b7280",
|
||||
backgroundColor: subtitleColor,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View style={styles.tabloOptionContent}>
|
||||
<Text style={styles.tabloOptionName}>
|
||||
<Text
|
||||
style={[styles.tabloOptionName, { color: textColor }]}
|
||||
>
|
||||
Tous les tablos
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -606,15 +709,34 @@ export default function PlanningScreen() {
|
|||
animationType="slide"
|
||||
onRequestClose={() => setShowCreateEventModal(false)}
|
||||
>
|
||||
<View style={styles.createEventModalOverlay}>
|
||||
<View style={styles.createEventModalContent}>
|
||||
<View style={styles.createEventModalHeader}>
|
||||
<Text style={styles.createEventModalTitle}>Nouvel événement</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.createEventModalOverlay,
|
||||
{ backgroundColor: modalOverlayColor },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.createEventModalContent,
|
||||
{ backgroundColor: cardBackgroundColor },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.createEventModalHeader,
|
||||
{ borderBottomColor: borderColor },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[styles.createEventModalTitle, { color: textColor }]}
|
||||
>
|
||||
Nouvel événement
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowCreateEventModal(false)}
|
||||
style={styles.createEventCloseButton}
|
||||
>
|
||||
<X size={24} color="#6b7280" />
|
||||
<X size={24} color={subtitleColor} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
|
|
@ -624,57 +746,92 @@ export default function PlanningScreen() {
|
|||
>
|
||||
{/* Title Field */}
|
||||
<View style={styles.formField}>
|
||||
<Text style={styles.formLabel}>Titre *</Text>
|
||||
<Text style={[styles.formLabel, { color: subtitleColor }]}>
|
||||
Titre *
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
style={[
|
||||
styles.formInput,
|
||||
{
|
||||
backgroundColor: cardBackgroundColor,
|
||||
borderColor,
|
||||
color: textColor,
|
||||
},
|
||||
]}
|
||||
value={newEvent.title}
|
||||
onChangeText={(text) =>
|
||||
setNewEvent({ ...newEvent, title: text })
|
||||
}
|
||||
placeholder="Titre de l'événement"
|
||||
placeholderTextColor="#9ca3af"
|
||||
placeholderTextColor={subtitleColor}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Date Field */}
|
||||
<View style={styles.formField}>
|
||||
<Text style={styles.formLabel}>Date</Text>
|
||||
<Text style={[styles.formLabel, { color: subtitleColor }]}>
|
||||
Date
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
style={[
|
||||
styles.formInput,
|
||||
{
|
||||
backgroundColor: cardBackgroundColor,
|
||||
borderColor,
|
||||
color: textColor,
|
||||
},
|
||||
]}
|
||||
value={newEvent.start_date}
|
||||
onChangeText={(text) =>
|
||||
setNewEvent({ ...newEvent, start_date: text })
|
||||
}
|
||||
placeholder="YYYY-MM-DD"
|
||||
placeholderTextColor="#9ca3af"
|
||||
placeholderTextColor={subtitleColor}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Time Field */}
|
||||
<View style={styles.formField}>
|
||||
<Text style={styles.formLabel}>Heure</Text>
|
||||
<Text style={[styles.formLabel, { color: subtitleColor }]}>
|
||||
Heure
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.formInput}
|
||||
style={[
|
||||
styles.formInput,
|
||||
{
|
||||
backgroundColor: cardBackgroundColor,
|
||||
borderColor,
|
||||
color: textColor,
|
||||
},
|
||||
]}
|
||||
value={newEvent.start_time}
|
||||
onChangeText={(text) =>
|
||||
setNewEvent({ ...newEvent, start_time: text })
|
||||
}
|
||||
placeholder="HH:MM"
|
||||
placeholderTextColor="#9ca3af"
|
||||
placeholderTextColor={subtitleColor}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Tablo Selector */}
|
||||
<View style={styles.formField}>
|
||||
<Text style={styles.formLabel}>Tablo *</Text>
|
||||
<Text style={[styles.formLabel, { color: subtitleColor }]}>
|
||||
Tablo *
|
||||
</Text>
|
||||
<FlatList
|
||||
style={[
|
||||
styles.tabloListInForm,
|
||||
{ backgroundColor: cardBackgroundColor, borderColor },
|
||||
]}
|
||||
data={tablos ?? []}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tabloOptionInForm,
|
||||
newEvent.tablo_id === item.id &&
|
||||
styles.selectedTabloOptionInForm,
|
||||
{ borderBottomColor: borderColor },
|
||||
newEvent.tablo_id === item.id && {
|
||||
backgroundColor: selectedOptionBgColor,
|
||||
},
|
||||
]}
|
||||
onPress={() =>
|
||||
setNewEvent({ ...newEvent, tablo_id: item.id })
|
||||
|
|
@ -693,6 +850,12 @@ export default function PlanningScreen() {
|
|||
<Text
|
||||
style={[
|
||||
styles.tabloOptionNameInForm,
|
||||
{
|
||||
color:
|
||||
newEvent.tablo_id === item.id
|
||||
? "#3b82f6"
|
||||
: textColor,
|
||||
},
|
||||
newEvent.tablo_id === item.id &&
|
||||
styles.selectedTabloOptionNameInForm,
|
||||
]}
|
||||
|
|
@ -708,17 +871,23 @@ export default function PlanningScreen() {
|
|||
keyExtractor={(item) => item.id}
|
||||
showsVerticalScrollIndicator={false}
|
||||
scrollEnabled={false}
|
||||
style={styles.tabloListInForm}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.createEventModalActions}>
|
||||
<View
|
||||
style={[
|
||||
styles.createEventModalActions,
|
||||
{ borderTopColor: borderColor },
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.cancelButton}
|
||||
style={[styles.cancelButton, { borderColor }]}
|
||||
onPress={() => setShowCreateEventModal(false)}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Annuler</Text>
|
||||
<Text style={[styles.cancelButtonText, { color: textColor }]}>
|
||||
Annuler
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.saveButton}
|
||||
|
|
@ -738,7 +907,7 @@ export default function PlanningScreen() {
|
|||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f8fafc",
|
||||
// backgroundColor is set dynamically
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
|
|
@ -751,12 +920,12 @@ const styles = StyleSheet.create({
|
|||
headerTitle: {
|
||||
fontSize: 28,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
// color is set dynamically
|
||||
marginBottom: 4,
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: 16,
|
||||
color: "#6b7280",
|
||||
// color is set dynamically
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
addButton: {
|
||||
|
|
@ -778,7 +947,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
viewModeToggle: {
|
||||
flexDirection: "row",
|
||||
backgroundColor: "#f3f4f6",
|
||||
// backgroundColor is set dynamically
|
||||
borderRadius: 12,
|
||||
padding: 4,
|
||||
},
|
||||
|
|
@ -798,7 +967,7 @@ const styles = StyleSheet.create({
|
|||
viewModeText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
color: "#6b7280",
|
||||
// color is set dynamically
|
||||
},
|
||||
activeViewModeText: {
|
||||
color: "white",
|
||||
|
|
@ -808,7 +977,7 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 20,
|
||||
},
|
||||
tabloSelector: {
|
||||
backgroundColor: "white",
|
||||
// backgroundColor is set dynamically
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
|
|
@ -831,7 +1000,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
tabloSelectorLabel: {
|
||||
fontSize: 12,
|
||||
color: "#6b7280",
|
||||
// color is set dynamically
|
||||
marginBottom: 2,
|
||||
},
|
||||
tabloSelectorRow: {
|
||||
|
|
@ -841,7 +1010,7 @@ const styles = StyleSheet.create({
|
|||
tabloSelectorName: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#1f2937",
|
||||
// color is set dynamically
|
||||
marginLeft: 8,
|
||||
},
|
||||
tabloColorDot: {
|
||||
|
|
@ -870,7 +1039,7 @@ const styles = StyleSheet.create({
|
|||
calendarTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#1f2937",
|
||||
// color is set dynamically
|
||||
},
|
||||
weekHeader: {
|
||||
flexDirection: "row",
|
||||
|
|
@ -878,12 +1047,12 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 16,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#e5e7eb",
|
||||
// borderBottomColor is set dynamically
|
||||
},
|
||||
weekDay: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
color: "#6b7280",
|
||||
// color is set dynamically
|
||||
textAlign: "center",
|
||||
width: 40,
|
||||
},
|
||||
|
|
@ -900,11 +1069,11 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
dayText: {
|
||||
fontSize: 16,
|
||||
color: "#1f2937",
|
||||
// color is set dynamically
|
||||
fontWeight: "500",
|
||||
},
|
||||
inactiveDayText: {
|
||||
color: "#d1d5db",
|
||||
// color is set dynamically
|
||||
},
|
||||
selectedDay: {
|
||||
backgroundColor: "#3b82f6",
|
||||
|
|
@ -947,7 +1116,7 @@ const styles = StyleSheet.create({
|
|||
marginHorizontal: 2,
|
||||
},
|
||||
weekDayHeader: {
|
||||
backgroundColor: "#f8fafc",
|
||||
// backgroundColor is set dynamically
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
alignItems: "center",
|
||||
|
|
@ -964,12 +1133,12 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
weekDayName: {
|
||||
fontSize: 12,
|
||||
color: "#6b7280",
|
||||
// color is set dynamically
|
||||
fontWeight: "500",
|
||||
},
|
||||
weekDayNumber: {
|
||||
fontSize: 16,
|
||||
color: "#1f2937",
|
||||
// color is set dynamically
|
||||
fontWeight: "600",
|
||||
marginTop: 2,
|
||||
},
|
||||
|
|
@ -1037,14 +1206,14 @@ const styles = StyleSheet.create({
|
|||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "#e5e7eb",
|
||||
// backgroundColor is set dynamically
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
margin: 2,
|
||||
},
|
||||
weekEventMoreText: {
|
||||
fontSize: 10,
|
||||
color: "#6b7280",
|
||||
// color is set dynamically
|
||||
},
|
||||
weekEmptySlot: {
|
||||
flex: 1,
|
||||
|
|
@ -1069,11 +1238,10 @@ const styles = StyleSheet.create({
|
|||
eventsSectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#1f2937",
|
||||
// color is set dynamically
|
||||
marginLeft: 8,
|
||||
},
|
||||
eventCard: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 12,
|
||||
marginBottom: 12,
|
||||
flexDirection: "row",
|
||||
|
|
@ -1082,6 +1250,7 @@ const styles = StyleSheet.create({
|
|||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
borderWidth: 1,
|
||||
},
|
||||
eventColorBar: {
|
||||
width: 4,
|
||||
|
|
@ -1095,7 +1264,6 @@ const styles = StyleSheet.create({
|
|||
eventTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#1f2937",
|
||||
marginBottom: 8,
|
||||
},
|
||||
eventDetails: {
|
||||
|
|
@ -1108,7 +1276,6 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
eventDetailText: {
|
||||
fontSize: 14,
|
||||
color: "#6b7280",
|
||||
},
|
||||
emptyState: {
|
||||
display: "flex",
|
||||
|
|
@ -1121,25 +1288,24 @@ const styles = StyleSheet.create({
|
|||
emptyStateTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#6b7280",
|
||||
// color is set dynamically
|
||||
marginTop: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyStateText: {
|
||||
fontSize: 14,
|
||||
color: "#9ca3af",
|
||||
// color is set dynamically
|
||||
textAlign: "center",
|
||||
lineHeight: 20,
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
// backgroundColor is set dynamically
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 16,
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
|
|
@ -1149,16 +1315,15 @@ const styles = StyleSheet.create({
|
|||
shadowOpacity: 0.25,
|
||||
shadowRadius: 20,
|
||||
elevation: 10,
|
||||
borderWidth: 1,
|
||||
},
|
||||
modalHeader: {
|
||||
padding: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#e5e7eb",
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#1f2937",
|
||||
textAlign: "center",
|
||||
},
|
||||
tabloOption: {
|
||||
|
|
@ -1167,7 +1332,6 @@ const styles = StyleSheet.create({
|
|||
alignItems: "center",
|
||||
padding: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f3f4f6",
|
||||
},
|
||||
tabloOptionLeft: {
|
||||
flexDirection: "row",
|
||||
|
|
@ -1181,7 +1345,6 @@ const styles = StyleSheet.create({
|
|||
tabloOptionName: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
color: "#1f2937",
|
||||
marginBottom: 2,
|
||||
},
|
||||
tabloOptionCount: {
|
||||
|
|
@ -1204,11 +1367,11 @@ const styles = StyleSheet.create({
|
|||
selectedDayTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#1f2937",
|
||||
// color is set dynamically
|
||||
},
|
||||
selectedDayEventsCount: {
|
||||
fontSize: 14,
|
||||
color: "#6b7280",
|
||||
// color is set dynamically
|
||||
},
|
||||
selectedDayEventsList: {
|
||||
paddingBottom: 10,
|
||||
|
|
@ -1222,11 +1385,10 @@ const styles = StyleSheet.create({
|
|||
// Create Event Modal Styles
|
||||
createEventModalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
// backgroundColor is set dynamically
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
createEventModalContent: {
|
||||
backgroundColor: "white",
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
maxHeight: "90%",
|
||||
|
|
@ -1242,12 +1404,10 @@ const styles = StyleSheet.create({
|
|||
alignItems: "center",
|
||||
padding: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#e5e7eb",
|
||||
},
|
||||
createEventModalTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: "600",
|
||||
color: "#1f2937",
|
||||
},
|
||||
createEventCloseButton: {
|
||||
padding: 4,
|
||||
|
|
@ -1262,23 +1422,17 @@ const styles = StyleSheet.create({
|
|||
formLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
color: "#374151",
|
||||
marginBottom: 8,
|
||||
},
|
||||
formInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#d1d5db",
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
color: "#1f2937",
|
||||
backgroundColor: "white",
|
||||
},
|
||||
tabloListInForm: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#d1d5db",
|
||||
borderRadius: 8,
|
||||
backgroundColor: "white",
|
||||
maxHeight: 150,
|
||||
},
|
||||
tabloOptionInForm: {
|
||||
|
|
@ -1287,15 +1441,13 @@ const styles = StyleSheet.create({
|
|||
alignItems: "center",
|
||||
padding: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#f3f4f6",
|
||||
},
|
||||
selectedTabloOptionInForm: {
|
||||
backgroundColor: "#eff6ff",
|
||||
// backgroundColor is set dynamically
|
||||
},
|
||||
tabloOptionNameInForm: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
color: "#1f2937",
|
||||
marginLeft: 8,
|
||||
},
|
||||
selectedTabloOptionNameInForm: {
|
||||
|
|
@ -1306,7 +1458,6 @@ const styles = StyleSheet.create({
|
|||
justifyContent: "space-between",
|
||||
padding: 20,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: "#e5e7eb",
|
||||
gap: 12,
|
||||
},
|
||||
cancelButton: {
|
||||
|
|
@ -1315,14 +1466,12 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "#d1d5db",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
cancelButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
color: "#6b7280",
|
||||
},
|
||||
saveButton: {
|
||||
flex: 1,
|
||||
504
xtablo-expo/app/(app)/(tabs)/settings.tsx
Normal file
504
xtablo-expo/app/(app)/(tabs)/settings.tsx
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
StatusBar,
|
||||
ScrollView,
|
||||
Switch,
|
||||
Alert,
|
||||
Linking,
|
||||
} from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import {
|
||||
User,
|
||||
Bell,
|
||||
Moon,
|
||||
Shield,
|
||||
HelpCircle,
|
||||
Info,
|
||||
MessageSquare,
|
||||
LogOut,
|
||||
ChevronRight,
|
||||
Smartphone,
|
||||
Globe,
|
||||
Lock,
|
||||
Heart,
|
||||
} from "lucide-react-native";
|
||||
import { router } from "expo-router";
|
||||
import { useThemeColor } from "@/hooks/useThemeColor";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const signOut = useAuthStore((state) => 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) => (
|
||||
<View style={styles.section}>
|
||||
<Text style={[styles.sectionTitle, { color: subtitleColor }]}>
|
||||
{title}
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.sectionContent,
|
||||
{ backgroundColor: cardBackgroundColor, borderColor },
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderSettingsItem = (
|
||||
icon: React.ReactNode,
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
onPress?: () => void,
|
||||
rightComponent?: React.ReactNode,
|
||||
showArrow: boolean = true
|
||||
) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.settingsItem, { borderBottomColor: borderColor }]}
|
||||
onPress={onPress}
|
||||
disabled={!onPress}
|
||||
activeOpacity={onPress ? 0.7 : 1}
|
||||
>
|
||||
<View style={styles.settingsItemLeft}>
|
||||
<View style={styles.iconContainer}>{icon}</View>
|
||||
<View style={styles.settingsItemContent}>
|
||||
<Text style={[styles.settingsItemTitle, { color: textColor }]}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text
|
||||
style={[styles.settingsItemSubtitle, { color: subtitleColor }]}
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.settingsItemRight}>
|
||||
{rightComponent}
|
||||
{showArrow && onPress && (
|
||||
<ChevronRight
|
||||
size={20}
|
||||
color={subtitleColor}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const renderSwitchItem = (
|
||||
icon: React.ReactNode,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
value: boolean,
|
||||
onValueChange: (value: boolean) => void
|
||||
) =>
|
||||
renderSettingsItem(
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
undefined,
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
trackColor={{
|
||||
false: colorScheme === "dark" ? "#374151" : "#e5e7eb",
|
||||
true: "#3b82f6",
|
||||
}}
|
||||
thumbColor={value ? "#ffffff" : "#ffffff"}
|
||||
ios_backgroundColor={colorScheme === "dark" ? "#374151" : "#e5e7eb"}
|
||||
/>,
|
||||
false
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor }]}>
|
||||
<StatusBar
|
||||
barStyle={colorScheme === "dark" ? "light-content" : "light-content"}
|
||||
backgroundColor={gradientColors[0]}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<Text style={styles.headerTitle}>Paramètres</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Gérez vos préférences et votre compte
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} />
|
||||
</LinearGradient>
|
||||
|
||||
<ScrollView
|
||||
style={[styles.content, { backgroundColor }]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{renderSettingsSection(
|
||||
"Compte",
|
||||
<>
|
||||
{renderSettingsItem(
|
||||
<User size={20} color="#3b82f6" />,
|
||||
"Profil utilisateur",
|
||||
`${user.name || "Non défini"} • ${user.email}`,
|
||||
() => router.push("/user"),
|
||||
undefined,
|
||||
true
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* {renderSettingsSection(
|
||||
"Notifications",
|
||||
<>
|
||||
{renderSwitchItem(
|
||||
<Bell size={20} color="#10b981" />,
|
||||
"Notifications push",
|
||||
"Recevoir des notifications sur votre appareil",
|
||||
pushNotifications,
|
||||
setPushNotifications
|
||||
)}
|
||||
{renderSwitchItem(
|
||||
<Smartphone size={20} color="#10b981" />,
|
||||
"Notifications par email",
|
||||
"Recevoir des notifications par email",
|
||||
emailNotifications,
|
||||
setEmailNotifications
|
||||
)}
|
||||
</>
|
||||
)} */}
|
||||
|
||||
{/* Appearance Section */}
|
||||
{renderSettingsSection(
|
||||
"Apparence",
|
||||
<>
|
||||
{renderSwitchItem(
|
||||
<Moon size={20} color="#6366f1" />,
|
||||
"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(
|
||||
<Lock size={20} color="#ef4444" />,
|
||||
"Authentification biométrique",
|
||||
"Utiliser votre empreinte ou Face ID",
|
||||
biometricAuth,
|
||||
setBiometricAuth
|
||||
)}
|
||||
{renderSettingsItem(
|
||||
<Shield size={20} color="#ef4444" />,
|
||||
"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(
|
||||
<HelpCircle size={20} color="#f59e0b" />,
|
||||
"Centre d'aide",
|
||||
"FAQ et guides d'utilisation",
|
||||
() => Linking.openURL("https://xtablo.com/help"),
|
||||
undefined,
|
||||
true
|
||||
)} */}
|
||||
{renderSettingsItem(
|
||||
<MessageSquare size={20} color="#f59e0b" />,
|
||||
"Contacter le support",
|
||||
"Envoyez-nous un email",
|
||||
handleContactSupport,
|
||||
undefined,
|
||||
true
|
||||
)}
|
||||
{/* {renderSettingsItem(
|
||||
<Heart size={20} color="#ec4899" />,
|
||||
"Évaluer l'application",
|
||||
"Aidez-nous à améliorer XTablo",
|
||||
handleRateApp,
|
||||
undefined,
|
||||
true
|
||||
)} */}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* About Section */}
|
||||
{renderSettingsSection(
|
||||
"À propos",
|
||||
<>
|
||||
{renderSettingsItem(
|
||||
<Info size={20} color="#6b7280" />,
|
||||
"Version de l'application",
|
||||
"1.0.0 (Build 1)",
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
)}
|
||||
{/* {renderSettingsItem(
|
||||
<Globe size={20} color="#6b7280" />,
|
||||
"Site web",
|
||||
"Visitez notre site web",
|
||||
() => Linking.openURL("https://app.xtablo.com"),
|
||||
undefined,
|
||||
true
|
||||
)} */}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Sign Out Section */}
|
||||
<View style={styles.signOutSection}>
|
||||
<TouchableOpacity
|
||||
style={styles.signOutButton}
|
||||
onPress={handleSignOut}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={["#ef4444", "#dc2626"]}
|
||||
style={styles.signOutGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<LogOut size={20} color="white" />
|
||||
<Text style={styles.signOutText}>Se déconnecter</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Bottom Spacing */}
|
||||
<View style={styles.bottomSpacing} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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() {
|
|||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tabloCard,
|
||||
{ width: viewMode === "grid" ? itemWidth : "100%" },
|
||||
{
|
||||
width: viewMode === "grid" ? itemWidth : "100%",
|
||||
backgroundColor: cardBackgroundColor,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => navigateToTablo(tablo)}
|
||||
onLongPress={() => handleDeleteTablo(tablo)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{/* Tablo Image/Color Header */}
|
||||
|
|
@ -188,7 +248,10 @@ export default function TablosScreen() {
|
|||
{/* Tablo Info */}
|
||||
<View style={styles.tabloInfo}>
|
||||
<View style={styles.nameRow}>
|
||||
<Text style={styles.tabloName} numberOfLines={2}>
|
||||
<Text
|
||||
style={[styles.tabloName, { color: textColor }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{tablo.name}
|
||||
</Text>
|
||||
<View
|
||||
|
|
@ -229,13 +292,13 @@ export default function TablosScreen() {
|
|||
router.push(`/channel/${getChannelCid(tablo.id)}`)
|
||||
}
|
||||
>
|
||||
<MessageCircle size={16} color="#6b7280" />
|
||||
<MessageCircle size={16} color={subtitleColor} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.cardActionButton}
|
||||
onPress={() => router.push("/planning")}
|
||||
>
|
||||
<Calendar size={16} color="#6b7280" />
|
||||
<Calendar size={16} color={subtitleColor} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -247,8 +310,12 @@ export default function TablosScreen() {
|
|||
const renderListItem = ({ item: tablo }: { item: UserTablo }) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.listItem}
|
||||
style={[
|
||||
styles.listItem,
|
||||
{ backgroundColor: cardBackgroundColor, borderColor: borderColor },
|
||||
]}
|
||||
onPress={() => navigateToTablo(tablo)}
|
||||
onLongPress={() => handleDeleteTablo(tablo)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View
|
||||
|
|
@ -265,7 +332,9 @@ export default function TablosScreen() {
|
|||
<View style={styles.listItemContent}>
|
||||
<View style={styles.listNameRow}>
|
||||
<View style={styles.listNameContainer}>
|
||||
<Text style={styles.listName}>{tablo.name}</Text>
|
||||
<Text style={[styles.listName, { color: textColor }]}>
|
||||
{tablo.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
|
|
@ -304,13 +373,13 @@ export default function TablosScreen() {
|
|||
router.push(`/channel/${getChannelCid(tablo.id)}`)
|
||||
}
|
||||
>
|
||||
<MessageCircle size={16} color="#6b7280" />
|
||||
<MessageCircle size={16} color={subtitleColor} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.listActionButton}
|
||||
onPress={() => router.push("/planning")}
|
||||
>
|
||||
<Calendar size={16} color="#6b7280" />
|
||||
<Calendar size={16} color={subtitleColor} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -340,12 +409,15 @@ export default function TablosScreen() {
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={[styles.container, { backgroundColor }]}>
|
||||
<StatusBar
|
||||
barStyle={colorScheme === "dark" ? "light-content" : "light-content"}
|
||||
backgroundColor={gradientColors[0]}
|
||||
/>
|
||||
|
||||
{/* Beautiful Header */}
|
||||
<LinearGradient
|
||||
colors={["#1e3a8a", "#3b82f6", "#60a5fa"]}
|
||||
colors={gradientColors}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
|
|
@ -394,10 +466,12 @@ export default function TablosScreen() {
|
|||
</LinearGradient>
|
||||
|
||||
{/* Content */}
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={[styles.contentContainer, { backgroundColor }]}>
|
||||
{isLoading && !refreshing ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>Chargement de vos tablos...</Text>
|
||||
<Text style={[styles.loadingText, { color: subtitleColor }]}>
|
||||
Chargement de vos tablos...
|
||||
</Text>
|
||||
</View>
|
||||
) : filteredTablos && filteredTablos.length > 0 ? (
|
||||
<FlatList
|
||||
|
|
@ -414,8 +488,10 @@ export default function TablosScreen() {
|
|||
/>
|
||||
) : (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyTitle}>Aucun tablo trouvé</Text>
|
||||
<Text style={styles.emptyMessage}>
|
||||
<Text style={[styles.emptyTitle, { color: textColor }]}>
|
||||
Aucun tablo trouvé
|
||||
</Text>
|
||||
<Text style={[styles.emptyMessage, { color: subtitleColor }]}>
|
||||
{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}
|
||||
>
|
||||
<View style={styles.modalContent}>
|
||||
<View
|
||||
style={[
|
||||
styles.modalContent,
|
||||
{ backgroundColor: cardBackgroundColor, borderColor },
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalCloseButton}
|
||||
onPress={() => setIsCreateModalVisible(false)}
|
||||
>
|
||||
<X size={24} color="#6b7280" />
|
||||
<X size={24} color={subtitleColor} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.modalTitle}>Nouveau Tablo</Text>
|
||||
<Text style={[styles.modalTitle, { color: textColor }]}>
|
||||
Nouveau Tablo
|
||||
</Text>
|
||||
<ScrollView>
|
||||
<View style={styles.modalFormGroup}>
|
||||
<Text style={styles.modalLabel}>Nom du Tablo</Text>
|
||||
<Text style={[styles.modalLabel, { color: subtitleColor }]}>
|
||||
Nom du Tablo
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.modalInput}
|
||||
style={[
|
||||
styles.modalInput,
|
||||
{
|
||||
backgroundColor: cardBackgroundColor,
|
||||
borderColor,
|
||||
color: textColor,
|
||||
},
|
||||
]}
|
||||
placeholder="Ex: Projet React Native"
|
||||
placeholderTextColor={subtitleColor}
|
||||
value={newTablo.name}
|
||||
onChangeText={(text) =>
|
||||
setNewTablo({ ...newTablo, name: text })
|
||||
|
|
@ -477,7 +570,9 @@ export default function TablosScreen() {
|
|||
/>
|
||||
</View>
|
||||
<View style={styles.modalFormGroup}>
|
||||
<Text style={styles.modalLabel}>Couleur</Text>
|
||||
<Text style={[styles.modalLabel, { color: subtitleColor }]}>
|
||||
Couleur
|
||||
</Text>
|
||||
<View style={styles.colorPicker}>
|
||||
{AVAILABLE_COLORS.map((color) => (
|
||||
<TouchableOpacity
|
||||
|
|
@ -493,7 +588,9 @@ export default function TablosScreen() {
|
|||
</View>
|
||||
</View>
|
||||
<View style={styles.modalFormGroup}>
|
||||
<Text style={styles.modalLabel}>Statut</Text>
|
||||
<Text style={[styles.modalLabel, { color: subtitleColor }]}>
|
||||
Statut
|
||||
</Text>
|
||||
<View style={styles.statusPicker}>
|
||||
{(["todo", "in_progress", "done"] as const).map((status) => (
|
||||
<TouchableOpacity
|
||||
|
|
@ -663,8 +760,8 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
backgroundColor: "#f8fafc",
|
||||
marginTop: -10,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
paddingTop: 10,
|
||||
},
|
||||
listContent: {
|
||||
|
|
@ -949,7 +1046,6 @@ const styles = StyleSheet.create({
|
|||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
width: "90%",
|
||||
|
|
@ -959,6 +1055,7 @@ const styles = StyleSheet.create({
|
|||
shadowOpacity: 0.3,
|
||||
shadowRadius: 10,
|
||||
elevation: 10,
|
||||
borderWidth: 1,
|
||||
},
|
||||
modalCloseButton: {
|
||||
alignSelf: "flex-end",
|
||||
|
|
@ -967,7 +1064,6 @@ const styles = StyleSheet.create({
|
|||
modalTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
marginBottom: 20,
|
||||
},
|
||||
modalFormGroup: {
|
||||
|
|
@ -975,16 +1071,13 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
modalLabel: {
|
||||
fontSize: 16,
|
||||
color: "#6b7280",
|
||||
marginBottom: 8,
|
||||
},
|
||||
modalInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#e5e7eb",
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
color: "#1f2937",
|
||||
},
|
||||
colorPicker: {
|
||||
flexDirection: "row",
|
||||
|
|
@ -17,6 +17,8 @@ import {
|
|||
import { MessageCircle, Users, Smile } from "lucide-react-native";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useHeaderHeight } from "@react-navigation/elements";
|
||||
import { useThemeColor } from "@/hooks/useThemeColor";
|
||||
import { useColorScheme } from "@/hooks/useColorScheme";
|
||||
|
||||
export default function ChannelScreen() {
|
||||
const { cid } = useLocalSearchParams<{ cid: string }>();
|
||||
|
|
@ -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 = () => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<View style={styles.emptyIconContainer}>
|
||||
<MessageCircle size={64} color="#d1d5db" strokeWidth={1.5} />
|
||||
<MessageCircle size={64} color={iconColor} strokeWidth={1.5} />
|
||||
<View style={styles.decorativeElements}>
|
||||
<View style={styles.floatingIcon1}>
|
||||
<Users size={20} color="#e5e7eb" />
|
||||
<Users size={20} color={iconSecondaryColor} />
|
||||
</View>
|
||||
<View style={styles.floatingIcon2}>
|
||||
<Smile size={18} color="#e5e7eb" />
|
||||
<Smile size={18} color={iconSecondaryColor} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.emptyTitle}>Commencez la conversation</Text>
|
||||
<Text style={styles.emptyMessage}>
|
||||
<Text style={[styles.emptyTitle, { color: textColor }]}>
|
||||
Commencez la conversation
|
||||
</Text>
|
||||
<Text style={[styles.emptyMessage, { color: subtitleColor }]}>
|
||||
Soyez le premier à envoyer un message dans ce canal !
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: "#f8fafc" }}>
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor }}>
|
||||
<Channel channel={channel} keyboardVerticalOffset={headerHeight}>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#3b82f6" />
|
||||
<Text style={styles.loadingText}>Chargement des messages...</Text>
|
||||
<Text style={[styles.loadingText, { color: subtitleColor }]}>
|
||||
Chargement des messages...
|
||||
</Text>
|
||||
</View>
|
||||
) : hasMessages ? (
|
||||
<MessageList />
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
328
xtablo-expo/app/(app)/user/index.tsx
Normal file
328
xtablo-expo/app/(app)/user/index.tsx
Normal file
|
|
@ -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 (
|
||||
<View style={styles.container}>
|
||||
<Stack.Screen options={{ headerShown: false }} />
|
||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||
<LinearGradient
|
||||
colors={["#1e3a8a", "#3b82f6", "#60a5fa"]}
|
||||
style={[styles.headerGradient, { paddingTop: insets.top + 60 }]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<Avatar
|
||||
size={120}
|
||||
rounded
|
||||
source={user.avatar_url ? { uri: user.avatar_url } : undefined}
|
||||
icon={
|
||||
!user.avatar_url
|
||||
? { name: "user", type: "font-awesome" }
|
||||
: undefined
|
||||
}
|
||||
containerStyle={styles.avatar}
|
||||
/>
|
||||
<Text style={styles.userName}>{user.name || "Utilisateur"}</Text>
|
||||
<Text style={styles.userEmail}>{user.email}</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Contenu principal */}
|
||||
<View style={styles.content}>
|
||||
{/* Carte d'informations personnelles */}
|
||||
<Card containerStyle={styles.mainCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<User size={20} color="#3b82f6" />
|
||||
<Text style={styles.cardHeaderTitle}>
|
||||
Informations personnelles
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoItem}>
|
||||
<View style={styles.infoIconContainer}>
|
||||
<Mail size={18} color="#6b7280" />
|
||||
</View>
|
||||
<View style={styles.infoContent}>
|
||||
<Text style={styles.infoLabel}>Adresse e-mail</Text>
|
||||
<Text style={styles.infoValue}>{user.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.infoItem}>
|
||||
<View style={styles.infoIconContainer}>
|
||||
<User size={18} color="#6b7280" />
|
||||
</View>
|
||||
<View style={styles.infoContent}>
|
||||
<Text style={styles.infoLabel}>Nom d'affichage</Text>
|
||||
<Text style={styles.infoValue}>{user.name}</Text>
|
||||
{/* {isEditing ? (
|
||||
<Input
|
||||
placeholder="Entrez le nom d'affichage"
|
||||
value={displayName}
|
||||
onChangeText={setDisplayName}
|
||||
containerStyle={styles.inputContainer}
|
||||
inputStyle={styles.inputText}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.infoValue}>
|
||||
{user.name || "Non défini"}
|
||||
</Text>
|
||||
)} */}
|
||||
</View>
|
||||
{/* <TouchableOpacity
|
||||
style={styles.editButton}
|
||||
onPress={() => {
|
||||
if (isEditing) {
|
||||
handleSaveDisplayName();
|
||||
} else {
|
||||
setDisplayName(user.name ?? "");
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<Check size={18} color="#10b981" />
|
||||
) : (
|
||||
<Edit3 size={18} color="#6b7280" />
|
||||
)}
|
||||
</TouchableOpacity> */}
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* Bouton de déconnexion */}
|
||||
<TouchableOpacity style={styles.signOutContainer} onPress={signOut}>
|
||||
<LinearGradient
|
||||
colors={["#ef4444", "#dc2626"]}
|
||||
style={styles.signOutGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<LogOut size={20} color="white" />
|
||||
<Text style={styles.signOutText}>Se déconnecter</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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 <ActivityIndicator />;
|
||||
}
|
||||
if (user) {
|
||||
return <Redirect href="/(home)/(tabs)" />;
|
||||
}
|
||||
return <Slot />;
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<View style={styles.avatarContainer}>
|
||||
<LinearGradient
|
||||
colors={gradientColors}
|
||||
style={styles.avatarGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Text style={styles.avatarInitials}>{initials}</Text>
|
||||
|
||||
{/* Member count indicator for group channels */}
|
||||
{memberCount > 2 && (
|
||||
<View style={styles.memberCountBadge}>
|
||||
<Text style={styles.memberCountText}>{memberCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Decorative ring */}
|
||||
<View
|
||||
style={[
|
||||
styles.avatarRing,
|
||||
{ borderColor: `${ColorMap[tabloColor]}30` },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Status indicator (online/active) */}
|
||||
<View style={styles.statusIndicator} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 }) => (
|
||||
<CustomChannelAvatar channel={channel} tablos={tablos || []} />
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
|
||||
{/* Beautiful Header */}
|
||||
<LinearGradient
|
||||
colors={["#1e3a8a", "#3b82f6", "#60a5fa"]}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<View style={styles.headerBottom}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.headerTitle}>Discussions</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Gérez les conversations de vos tablos
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.searchButton}>
|
||||
<Search size={20} color="#3b82f6" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Decorative Elements
|
||||
<View style={styles.decorativeCircle1} />
|
||||
<View style={styles.decorativeCircle2} /> */}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Channel List */}
|
||||
<View style={styles.channelListContainer}>
|
||||
<ChannelList
|
||||
filters={filters}
|
||||
onSelect={(channel) => {
|
||||
router.push(`/channel/${channel.cid}`);
|
||||
}}
|
||||
sort={sort}
|
||||
options={options}
|
||||
PreviewAvatar={AvatarWithTablos}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
|
@ -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 (
|
||||
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
|
||||
<LinearGradient
|
||||
colors={["#1e3a8a", "#3b82f6", "#60a5fa"]}
|
||||
style={styles.headerGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<View style={styles.headerContent}>
|
||||
<Avatar
|
||||
size={120}
|
||||
rounded
|
||||
source={user.avatar_url ? { uri: user.avatar_url } : undefined}
|
||||
icon={
|
||||
!user.avatar_url
|
||||
? { name: "user", type: "font-awesome" }
|
||||
: undefined
|
||||
}
|
||||
containerStyle={styles.avatar}
|
||||
/>
|
||||
<Text style={styles.userName}>{user.name || "Utilisateur"}</Text>
|
||||
<Text style={styles.userEmail}>{user.email}</Text>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Contenu principal */}
|
||||
<View style={styles.content}>
|
||||
{/* Carte d'informations personnelles */}
|
||||
<Card containerStyle={styles.mainCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<User size={20} color="#3b82f6" />
|
||||
<Text style={styles.cardHeaderTitle}>
|
||||
Informations personnelles
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoItem}>
|
||||
<View style={styles.infoIconContainer}>
|
||||
<Mail size={18} color="#6b7280" />
|
||||
</View>
|
||||
<View style={styles.infoContent}>
|
||||
<Text style={styles.infoLabel}>Adresse e-mail</Text>
|
||||
<Text style={styles.infoValue}>{user.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<View style={styles.infoItem}>
|
||||
<View style={styles.infoIconContainer}>
|
||||
<User size={18} color="#6b7280" />
|
||||
</View>
|
||||
<View style={styles.infoContent}>
|
||||
<Text style={styles.infoLabel}>Nom d'affichage</Text>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
placeholder="Entrez le nom d'affichage"
|
||||
value={displayName}
|
||||
onChangeText={setDisplayName}
|
||||
containerStyle={styles.inputContainer}
|
||||
inputStyle={styles.inputText}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.infoValue}>
|
||||
{user.name || "Non défini"}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.editButton}
|
||||
onPress={() => {
|
||||
if (isEditing) {
|
||||
handleSaveDisplayName();
|
||||
} else {
|
||||
setDisplayName(user.name ?? "");
|
||||
setIsEditing(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<Check size={18} color="#10b981" />
|
||||
) : (
|
||||
<Edit3 size={18} color="#6b7280" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* Éléments de menu */}
|
||||
<Card containerStyle={styles.menuCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Settings size={20} color="#3b82f6" />
|
||||
<Text style={styles.cardHeaderTitle}>Préférences</Text>
|
||||
</View>
|
||||
|
||||
{menuItems.map((item, index) => (
|
||||
<View key={index}>
|
||||
<TouchableOpacity style={styles.menuItem} onPress={item.onPress}>
|
||||
<View style={styles.menuIconContainer}>
|
||||
<item.icon size={20} color="#6b7280" />
|
||||
</View>
|
||||
<View style={styles.menuContent}>
|
||||
<Text style={styles.menuTitle}>{item.title}</Text>
|
||||
<Text style={styles.menuSubtitle}>{item.subtitle}</Text>
|
||||
</View>
|
||||
<Text style={styles.menuArrow}>›</Text>
|
||||
</TouchableOpacity>
|
||||
{index < menuItems.length - 1 && <View style={styles.divider} />}
|
||||
</View>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
{/* Bouton de déconnexion */}
|
||||
<TouchableOpacity style={styles.signOutContainer} onPress={signOut}>
|
||||
<LinearGradient
|
||||
colors={["#ef4444", "#dc2626"]}
|
||||
style={styles.signOutGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<LogOut size={20} color="white" />
|
||||
<Text style={styles.signOutText}>Se déconnecter</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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 <ActivityIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
|
|
@ -53,14 +40,34 @@ export default function RootLayout() {
|
|||
<ThemeProvider
|
||||
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
|
||||
>
|
||||
<Stack>
|
||||
<Stack.Screen name="(home)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<SplashScreenController />
|
||||
<RootNavigator />
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
|
||||
const RootNavigator = () => {
|
||||
const { isLoading, isLoggedIn } = useInitializeApp();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Protected guard={isLoggedIn}>
|
||||
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
||||
</Stack.Protected>
|
||||
|
||||
<Stack.Protected guard={!isLoggedIn}>
|
||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="signup" options={{ headerShown: false }} />
|
||||
</Stack.Protected>
|
||||
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View style={styles.container}>
|
||||
<Image source={require("@/assets/images/logo.png")} style={styles.logo} />
|
||||
<Text style={styles.title}>Connexion XTablo</Text>
|
||||
<Text style={styles.subtitle}>Connectez-vous à votre compte</Text>
|
||||
<View style={[styles.container, { backgroundColor }]}>
|
||||
<Image source={logo} style={styles.logo} />
|
||||
<Text style={[styles.title, { color: textColor }]}>Connexion XTablo</Text>
|
||||
<Text style={[styles.subtitle, { color: subtitleColor }]}>
|
||||
Connectez-vous à votre compte
|
||||
</Text>
|
||||
<View style={[styles.verticallySpaced, styles.mt10]}>
|
||||
<Input
|
||||
label="Adresse email"
|
||||
|
|
@ -51,9 +86,9 @@ export default function Auth() {
|
|||
/>
|
||||
</View>
|
||||
<View style={styles.separatorContainer}>
|
||||
<View style={styles.separator} />
|
||||
<Text style={styles.separatorText}>ou</Text>
|
||||
<View style={styles.separator} />
|
||||
<View style={[styles.separator, { backgroundColor: separatorColor }]} />
|
||||
<Text style={[styles.separatorText, { color: subtitleColor }]}>ou</Text>
|
||||
<View style={[styles.separator, { backgroundColor: separatorColor }]} />
|
||||
</View>
|
||||
<View style={styles.verticallySpaced}>
|
||||
<GoogleLoginButton onPress={() => performOAuth("google")} />
|
||||
|
|
@ -62,8 +97,10 @@ export default function Auth() {
|
|||
<AppleLoginButton onPress={() => performOAuth("apple")} />
|
||||
</View>
|
||||
<View style={styles.linkContainer}>
|
||||
<Text style={styles.linkText}>Pas encore de compte ? </Text>
|
||||
<Link href="/signup" style={styles.link}>
|
||||
<Text style={[styles.linkText, { color: subtitleColor }]}>
|
||||
Pas encore de compte ?{" "}
|
||||
</Text>
|
||||
<Link href="/signup" style={[styles.link, { color: linkColor }]}>
|
||||
S'inscrire
|
||||
</Link>
|
||||
</View>
|
||||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
@ -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 (
|
||||
<View style={styles.container}>
|
||||
<Image source={require("@/assets/images/logo.png")} style={styles.logo} />
|
||||
<Text style={styles.title}>Créer un compte XTablo</Text>
|
||||
<Text style={styles.subtitle}>Rejoignez-nous !</Text>
|
||||
<View style={[styles.container, { backgroundColor }]}>
|
||||
<Image source={logo} style={styles.logo} />
|
||||
<Text style={[styles.title, { color: textColor }]}>
|
||||
Créer un compte XTablo
|
||||
</Text>
|
||||
<Text style={[styles.subtitle, { color: subtitleColor }]}>
|
||||
Rejoignez-nous !
|
||||
</Text>
|
||||
<View style={[styles.verticallySpaced, styles.mt10]}>
|
||||
<Input
|
||||
label="Prénom"
|
||||
|
|
@ -84,8 +111,10 @@ export default function SignUp() {
|
|||
/>
|
||||
</View>
|
||||
<View style={styles.linkContainer}>
|
||||
<Text style={styles.linkText}>Vous avez déjà un compte ? </Text>
|
||||
<Link href="/login" style={styles.link}>
|
||||
<Text style={[styles.linkText, { color: subtitleColor }]}>
|
||||
Vous avez déjà un compte ?{" "}
|
||||
</Text>
|
||||
<Link href="/login" style={[styles.link, { color: linkColor }]}>
|
||||
Se connecter
|
||||
</Link>
|
||||
</View>
|
||||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
BIN
xtablo-expo/assets/images/logo_white.png
Normal file
BIN
xtablo-expo/assets/images/logo_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
|
|
@ -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 (
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import { StyleSheet, View, Text, TouchableOpacity, Image } from "react-native";
|
||||
import { useAuth } from "@/stores/auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
export const GoogleLoginButton = ({ onPress }: { onPress: () => void }) => {
|
||||
const authLoading = useAuth((state) => state.loading);
|
||||
const authLoading = useAuthStore((state) => state.loading);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
105
xtablo-expo/components/LoadingView.tsx
Normal file
105
xtablo-expo/components/LoadingView.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { StyleSheet, View } from "react-native";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withRepeat,
|
||||
withTiming,
|
||||
useDerivedValue,
|
||||
Easing,
|
||||
} from "react-native-reanimated";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { useThemeColor } from "@/hooks/useThemeColor";
|
||||
|
||||
export const LoadingView = () => {
|
||||
const rotation = useSharedValue(0);
|
||||
|
||||
// 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 (
|
||||
<ThemedView style={[styles.loadingContainer, { backgroundColor }]}>
|
||||
<View style={styles.loadingContent}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Animated.Image
|
||||
source={require("@/assets/images/logo.png")}
|
||||
style={[styles.logo, animatedStyle]}
|
||||
/>
|
||||
</View>
|
||||
<ThemedText type="title" style={[styles.title, { color: textColor }]}>
|
||||
XTablo
|
||||
</ThemedText>
|
||||
<ThemedText
|
||||
type="subtitle"
|
||||
style={[styles.subtitle, { color: subtitleColor }]}
|
||||
>
|
||||
Initialisation de l'application...
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
17
xtablo-expo/components/Splash.tsx
Normal file
17
xtablo-expo/components/Splash.tsx
Normal file
|
|
@ -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;
|
||||
}
|
||||
217
xtablo-expo/components/SwipeableChannelPreview.tsx
Normal file
217
xtablo-expo/components/SwipeableChannelPreview.tsx
Normal file
|
|
@ -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<DefaultStreamChatGenerics>;
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
{/* Right Actions Background */}
|
||||
{/* <View style={styles.rightActionsContainer}>
|
||||
<Pressable
|
||||
style={[styles.deleteButton, { backgroundColor: deleteButtonColor }]}
|
||||
onPress={onDeletePress}
|
||||
>
|
||||
<Animated.View style={[styles.actionContent, actionAnimatedStyle]}>
|
||||
<Trash size={24} color={iconColor} />
|
||||
<Text style={[styles.actionText, { color: textColor }]}>
|
||||
Quitter
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
</View> */}
|
||||
|
||||
{/* Channel Content */}
|
||||
{/* <GestureDetector gesture={gestureHandler}> */}
|
||||
<View
|
||||
style={[
|
||||
styles.channelContainer,
|
||||
channelAnimatedStyle,
|
||||
{ backgroundColor },
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
{/* </GestureDetector> */}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
18
xtablo-expo/hooks/auth.ts
Normal file
18
xtablo-expo/hooks/auth.ts
Normal file
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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."
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<User | null>({
|
||||
queryKey: ["user"],
|
||||
queryFn: async () => {
|
||||
|
|
|
|||
810
xtablo-expo/package-lock.json
generated
810
xtablo-expo/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Theme> = 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 (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<Text>Chat Indisponible</Text>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: textColor }}>Chat Indisponible</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -33,7 +130,7 @@ export default function ChatProvider({
|
|||
}
|
||||
|
||||
return (
|
||||
<OverlayProvider>
|
||||
<OverlayProvider value={{ style: streamChatTheme }}>
|
||||
<Chat client={client}>{children}</Chat>
|
||||
</OverlayProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<StoreApi<User> | null>(null);
|
||||
|
||||
|
|
@ -23,7 +18,7 @@ export const UserStoreProvider = ({
|
|||
}
|
||||
|
||||
if (!user) {
|
||||
return <Redirect href="/(auth)/login" />;
|
||||
return null;
|
||||
}
|
||||
|
||||
const store = createStore<User>()(() => user);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { QueryClient } from "@tanstack/react-query";
|
|||
interface AuthState {
|
||||
session: Session | null;
|
||||
loading: boolean;
|
||||
initialized: boolean;
|
||||
initialize: (queryClient: QueryClient) => Promise<void>;
|
||||
setSession: (session: Session | null) => void;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
|
|
@ -28,8 +29,9 @@ interface AuthState {
|
|||
WebBrowser.maybeCompleteAuthSession();
|
||||
const redirectTo = makeRedirectUri({ path: "/(home)/(tabs)" });
|
||||
|
||||
export const useAuth = create<AuthState>((set, get) => ({
|
||||
export const useAuthStore = create<AuthState>((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<AuthState>((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) => {
|
||||
|
|
|
|||
9
xtablo-expo/types/user.types.ts
Normal file
9
xtablo-expo/types/user.types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Tables } from "./database.types";
|
||||
import { RemoveNullFromObject } from "./removeNull";
|
||||
|
||||
export type User = RemoveNullFromObject<
|
||||
Tables<"profiles"> & {
|
||||
streamToken: string | null;
|
||||
},
|
||||
"email" | "name"
|
||||
>;
|
||||
Loading…
Reference in a new issue