Merge pull request #4 from artslidd/develop

feat: Implement purple theme with animated backgrounds for login/signup pages
This commit is contained in:
Arthur Belleville 2025-10-07 22:49:19 +02:00 committed by GitHub
commit 85dfe7bae8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 2188 additions and 507 deletions

View file

@ -104,7 +104,7 @@ publicRouter.get("/slots/:shortUserId/:standardName", async (c) => {
// Use CET time for availability calculations
const now = getCETTime();
const nextMonth = new Date(now);
nextMonth.setMonth(now.getMonth() + 1);
nextMonth.setMonth(now.getMonth() + 2);
const { data: eventsData, error: eventsError } = await supabase
.from("events")

BIN
ui/public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
ui/public/logo_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -1,10 +1,8 @@
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
import { ThemeProvider } from "@ui/contexts/ThemeContext";
import { twMerge } from "tailwind-merge";
import { SessionProvider } from "@ui/contexts/SessionContext";
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
import { UserStoreProvider } from "@ui/providers/UserStoreProvider";
import { isProd } from "@ui/utils/helpers";
import { DatadogRumProvider } from "@ui/providers/DatadogRumProvider";
import { routes } from "@ui/lib/routes";
@ -23,13 +21,7 @@ export const App = () => {
<UserStoreProvider>
<Router>
<DatadogRumProvider>
<div
className={twMerge(
"min-h-screen",
!isProd ? "bg-orange-50" : "bg-white",
"dark:bg-gray-900"
)}
>
<div className="min-h-screen bg-background">
<AppRoutes />
<style>
{`

BIN
ui/src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -0,0 +1,282 @@
export const AnimatedBackground = () => {
return (
<div className="absolute inset-0 pointer-events-none overflow-hidden">
{/* Horizontal moving logos */}
<div className="absolute top-1/4 left-0 animate-move-right-slow opacity-4 dark:opacity-8">
<img
src="/icon.png"
alt="Xtablo"
className="w-16 h-16 object-contain animate-spin-slow block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="w-16 h-16 object-contain animate-spin-slow hidden dark:block"
/>
</div>
<div className="absolute top-1/3 left-0 animate-move-right-medium opacity-3 dark:opacity-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-12 h-12 object-contain animate-bounce-gentle block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="w-12 h-12 object-contain animate-bounce-gentle hidden dark:block"
/>
</div>
<div className="absolute top-1/2 left-0 animate-move-right-fast opacity-5 dark:opacity-10">
<img
src="/icon.png"
alt="Xtablo"
className="w-20 h-20 object-contain animate-pulse-gentle block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="w-20 h-20 object-contain animate-pulse-gentle hidden dark:block"
/>
</div>
<div className="absolute top-2/3 left-0 animate-move-right-slow opacity-2 dark:opacity-4">
<img
src="/icon.png"
alt="Xtablo"
className="w-14 h-14 object-contain animate-wiggle block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="w-14 h-14 object-contain animate-wiggle hidden dark:block"
/>
</div>
<div className="absolute top-3/4 left-0 animate-move-right-medium opacity-3 dark:opacity-7">
<img
src="/icon.png"
alt="Xtablo"
className="w-18 h-18 object-contain animate-float-gentle"
/>
</div>
{/* Diagonal moving logos */}
<div className="absolute top-0 left-1/4 animate-move-diagonal-1 opacity-3 dark:opacity-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-10 h-10 object-contain animate-spin-reverse"
/>
</div>
<div className="absolute top-0 left-1/2 animate-move-diagonal-2 opacity-4 dark:opacity-8">
<img
src="/icon.png"
alt="Xtablo"
className="w-16 h-16 object-contain animate-scale-gentle"
/>
</div>
<div className="absolute top-0 left-3/4 animate-move-diagonal-3 opacity-2 dark:opacity-5">
<img
src="/icon.png"
alt="Xtablo"
className="w-12 h-12 object-contain animate-rotate-gentle"
/>
</div>
{/* Vertical moving logos */}
<div className="absolute left-1/6 top-0 animate-move-down-slow opacity-3 dark:opacity-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-14 h-14 object-contain animate-bounce-soft"
/>
</div>
<div className="absolute left-5/6 top-0 animate-move-down-medium opacity-4 dark:opacity-7">
<img
src="/icon.png"
alt="Xtablo"
className="w-16 h-16 object-contain animate-sway"
/>
</div>
{/* Circular moving logos */}
<div className="absolute top-1/2 left-1/2 animate-orbit-1 opacity-2 dark:opacity-4">
<img src="/icon.png" alt="Xtablo" className="w-8 h-8 object-contain" />
</div>
<div className="absolute top-1/2 left-1/2 animate-orbit-2 opacity-3 dark:opacity-5">
<img
src="/icon.png"
alt="Xtablo"
className="w-10 h-10 object-contain"
/>
</div>
<div className="absolute top-1/2 left-1/2 animate-orbit-3 opacity-2 dark:opacity-4">
<img src="/icon.png" alt="Xtablo" className="w-6 h-6 object-contain" />
</div>
<div className="absolute top-1/2 left-1/2 animate-orbit-4 opacity-3 dark:opacity-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-12 h-12 object-contain animate-spin-fast"
/>
</div>
<div className="absolute top-1/2 left-1/2 animate-orbit-5 opacity-2 dark:opacity-5">
<img
src="/icon.png"
alt="Xtablo"
className="w-7 h-7 object-contain animate-pulse-fast"
/>
</div>
{/* Zigzag moving logos */}
<div className="absolute top-1/4 left-0 animate-zigzag-1 opacity-4 dark:opacity-8">
<img
src="/icon.png"
alt="Xtablo"
className="w-14 h-14 object-contain animate-wobble"
/>
</div>
<div className="absolute top-1/2 left-0 animate-zigzag-2 opacity-3 dark:opacity-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-11 h-11 object-contain animate-shake"
/>
</div>
<div className="absolute top-3/4 left-0 animate-zigzag-3 opacity-5 dark:opacity-9">
<img
src="/icon.png"
alt="Xtablo"
className="w-16 h-16 object-contain animate-bounce-crazy"
/>
</div>
{/* Spiral moving logos */}
<div className="absolute top-0 left-1/4 animate-spiral-1 opacity-3 dark:opacity-7">
<img
src="/icon.png"
alt="Xtablo"
className="w-9 h-9 object-contain animate-spin-wobble"
/>
</div>
<div className="absolute top-0 left-3/4 animate-spiral-2 opacity-4 dark:opacity-8">
<img
src="/icon.png"
alt="Xtablo"
className="w-13 h-13 object-contain animate-flip"
/>
</div>
{/* Random floating logos */}
<div className="absolute top-1/6 left-1/3 animate-float-random-1 opacity-2 dark:opacity-5">
<img
src="/icon.png"
alt="Xtablo"
className="w-8 h-8 object-contain animate-twirl"
/>
</div>
<div className="absolute top-1/3 left-2/3 animate-float-random-2 opacity-3 dark:opacity-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-10 h-10 object-contain animate-dance"
/>
</div>
<div className="absolute top-2/3 left-1/4 animate-float-random-3 opacity-4 dark:opacity-7">
<img
src="/icon.png"
alt="Xtablo"
className="w-12 h-12 object-contain animate-jiggle"
/>
</div>
<div className="absolute top-5/6 left-3/4 animate-float-random-4 opacity-2 dark:opacity-4">
<img
src="/icon.png"
alt="Xtablo"
className="w-9 h-9 object-contain animate-vibrate"
/>
</div>
{/* Wave pattern logos */}
<div className="absolute top-1/8 left-0 animate-wave-1 opacity-3 dark:opacity-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-11 h-11 object-contain animate-swing"
/>
</div>
<div className="absolute top-3/8 left-0 animate-wave-2 opacity-4 dark:opacity-8">
<img
src="/icon.png"
alt="Xtablo"
className="w-13 h-13 object-contain animate-pendulum"
/>
</div>
<div className="absolute top-5/8 left-0 animate-wave-3 opacity-2 dark:opacity-5">
<img
src="/icon.png"
alt="Xtablo"
className="w-10 h-10 object-contain animate-elastic"
/>
</div>
<div className="absolute top-7/8 left-0 animate-wave-4 opacity-5 dark:opacity-9">
<img
src="/icon.png"
alt="Xtablo"
className="w-15 h-15 object-contain animate-rubber"
/>
</div>
{/* Corner shooters */}
<div className="absolute top-0 left-0 animate-corner-shoot-1 opacity-3 dark:opacity-7">
<img
src="/icon.png"
alt="Xtablo"
className="w-12 h-12 object-contain animate-rocket"
/>
</div>
<div className="absolute top-0 right-0 animate-corner-shoot-2 opacity-4 dark:opacity-8">
<img
src="/icon.png"
alt="Xtablo"
className="w-14 h-14 object-contain animate-comet"
/>
</div>
<div className="absolute bottom-0 left-0 animate-corner-shoot-3 opacity-2 dark:opacity-5">
<img
src="/icon.png"
alt="Xtablo"
className="w-10 h-10 object-contain animate-meteor"
/>
</div>
<div className="absolute bottom-0 right-0 animate-corner-shoot-4 opacity-5 dark:opacity-10">
<img
src="/icon.png"
alt="Xtablo"
className="w-16 h-16 object-contain animate-blast"
/>
</div>
{/* Bouncing balls */}
<div className="absolute top-1/5 left-1/5 animate-bounce-ball-1 opacity-4 dark:opacity-8">
<img
src="/icon.png"
alt="Xtablo"
className="w-8 h-8 object-contain animate-spin-bounce"
/>
</div>
<div className="absolute top-2/5 left-4/5 animate-bounce-ball-2 opacity-3 dark:opacity-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-11 h-11 object-contain animate-flip-bounce"
/>
</div>
<div className="absolute top-4/5 left-2/5 animate-bounce-ball-3 opacity-5 dark:opacity-9">
<img
src="/icon.png"
alt="Xtablo"
className="w-13 h-13 object-contain animate-scale-bounce"
/>
</div>
</div>
);
};

View file

@ -159,7 +159,7 @@ export function AvailabilityCard({
size="sm"
variant="outline"
onPress={() => onCopyToOtherDays(day, enabled, timeRanges)}
className="h-6 px-2 text-xs border-gray-300 hover:border-primary hover:bg-primary/5 text-gray-600 hover:text-primary"
className="h-6 px-2 text-xs border-gray-300 dark:border-gray-600 hover:border-primary hover:bg-primary/5 dark:hover:bg-primary/10 text-gray-600 dark:text-gray-300 hover:text-primary"
>
<CopyIcon className="size-3 mr-1" />
Copier
@ -174,7 +174,9 @@ export function AvailabilityCard({
>
<Text
className={`font-medium text-sm ${
enabled ? "text-gray-900" : "text-gray-500"
enabled
? "text-gray-900 dark:text-gray-100"
: "text-gray-500 dark:text-gray-400"
}`}
>
{enabled ? "Disponible" : "Indisponible"}
@ -191,7 +193,7 @@ export function AvailabilityCard({
className={`flex items-center gap-1 rounded-md px-1.5 py-1 cursor-pointer transition-all duration-200 ${
selectedRangeIndex === index
? "bg-primary/10 dark:bg-primary/20"
: "bg-gray-50/80 dark:bg-gray-900/80 hover:bg-gray-100 dark:hover:bg-gray-800/80"
: "bg-gray-50/80 dark:bg-gray-800/60 hover:bg-gray-100 dark:hover:bg-gray-700/60"
}`}
>
<div className="flex items-center text-xs w-fit">
@ -223,7 +225,9 @@ export function AvailabilityCard({
</SelectListBox>
</SelectPopover>
</Select>
<Text className="text-gray-500 text-[10px] mx-2">-</Text>
<Text className="text-gray-500 dark:text-gray-400 text-[10px] mx-2">
-
</Text>
<Select
aria-label="Heure de fin"
selectedKey={range.end}
@ -256,7 +260,9 @@ export function AvailabilityCard({
<Text className="font-medium text-xs px-1">
{range.start}
</Text>
<Text className="text-gray-500 text-[10px]"></Text>
<Text className="text-gray-500 dark:text-gray-400 text-[10px]">
</Text>
<Text className="font-medium text-xs px-1">{range.end}</Text>
</>
)}
@ -281,7 +287,7 @@ export function AvailabilityCard({
isDisabled={!enabled}
variant="outline"
size="sm"
className="h-5 px-1.5 flex items-center text-xs border-0 bg-gray-100/50 dark:bg-gray-800/50 hover:bg-gray-200/50 dark:hover:bg-gray-700/50"
className="h-5 px-1.5 flex items-center text-xs border-0 bg-gray-100/50 dark:bg-gray-700/50 hover:bg-gray-200/50 dark:hover:bg-gray-600/50"
>
<PlusIcon className="size-2.5" />
</Button>

View file

@ -64,10 +64,10 @@ export const AvailabilityVisualization = ({
}) => {
const timeSlots = generateTimeSlots(slotDurationMinutes);
return (
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-white dark:bg-gray-700/40 rounded-xl shadow-sm dark:shadow-gray-900/20 border border-gray-200 dark:border-gray-600/50 overflow-hidden">
{/* Weekly Calendar Header */}
<div className="grid grid-cols-8 border-b-2 border-gray-200 dark:border-gray-600">
<div className="p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900 border-r border-gray-200 dark:border-gray-600">
<div className="p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700/60 dark:to-slate-800/60 border-r border-gray-200 dark:border-gray-600">
<Text className="font-bold text-sm text-slate-700 dark:text-slate-300">
Heure
</Text>
@ -75,7 +75,7 @@ export const AvailabilityVisualization = ({
{DAYS_OF_WEEK.map((day) => (
<div
key={day}
className="p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900 border-r border-gray-200 dark:border-gray-600 last:border-r-0 text-center"
className="p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700/60 dark:to-slate-800/60 border-r border-gray-200 dark:border-gray-600 last:border-r-0 text-center"
>
<Text className="font-bold text-sm text-slate-700 dark:text-slate-300">
{DAYS_OF_WEEK_DISPLAY[day]}
@ -105,7 +105,7 @@ export const AvailabilityVisualization = ({
{DAYS_OF_WEEK.map((day) => (
<div
key={`${day}-${timeSlot}`}
className="p-3 border-r border-gray-200 dark:border-gray-600 last:border-r-0 min-h-[3rem] flex items-center justify-center bg-gradient-to-br from-white to-slate-50/30 dark:from-gray-800 dark:to-slate-900/30"
className="p-3 border-r border-gray-200 dark:border-gray-600 last:border-r-0 min-h-[3rem] flex items-center justify-center bg-gradient-to-br from-white to-slate-50/30 dark:from-gray-700/40 dark:to-slate-800/40"
>
{isTimeSlotAvailable(day, timeSlot, draftAvailabilities) ? (
<div className="w-full h-8 bg-gradient-to-r from-emerald-400 via-emerald-500 to-emerald-600 dark:from-emerald-500 dark:via-emerald-600 dark:to-emerald-700 rounded-lg shadow-sm border border-emerald-300 dark:border-emerald-600 flex items-center justify-center group hover:shadow-md transition-all duration-200 hover:scale-105">

View file

@ -11,12 +11,15 @@ interface EventDetailsModalProps {
isOpen: boolean;
onClose: () => void;
onEdit?: () => void;
canEdit?: boolean;
}
export const EventDetailsModal = ({
event,
isOpen,
onClose,
onEdit,
canEdit = false,
}: EventDetailsModalProps) => {
if (!event) return null;
@ -139,6 +142,7 @@ export const EventDetailsModal = ({
<Button variant="outline" onPress={onClose}>
Fermer
</Button>
{canEdit && onEdit && <Button onPress={onEdit}>Modifier</Button>}
</div>
</CustomModal>
);

View file

@ -1,7 +1,7 @@
import { Event, EventInsert } from "@ui/types/events.types";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useTablosList } from "@ui/hooks/tablos";
import { useCreateEvents } from "@ui/hooks/events";
import { useCreateEvents, useEvent, useUpdateEvent } from "@ui/hooks/events";
import { useUser } from "@ui/providers/UserStoreProvider";
import {
Select,
@ -14,16 +14,21 @@ import { useTimePicker } from "@ui/ui-library/time-picker";
import { DatePicker, DatePickerButton } from "@ui/ui-library/date-picker";
import { Group } from "react-aria-components";
import { getLocalTimeZone, parseDate, today } from "@internationalized/date";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
const { event_id } = useParams();
const { data: event } = useEvent(event_id as string);
export const CreateEventModal = () => {
const user = useUser();
const [searchParams] = useSearchParams();
const tablo_id = searchParams.get("tablo_id");
const date = new Date(searchParams.get("date") || "");
const dateFromParams = searchParams.get("date");
const date = dateFromParams ? new Date(dateFromParams) : new Date();
const { data: tablos, isLoading: tablosLoading } = useTablosList();
const createEvents = useCreateEvents();
const updateEvent = useUpdateEvent();
const timeOptions = useTimePicker({ intervalInMinute: 15 });
const navigate = useNavigate();
@ -62,7 +67,7 @@ export const CreateEventModal = () => {
return nearestOption?.id || "";
};
const [createdEvent, setCreatedEvent] = useState<EventInsert>({
const [formEvent, setFormEvent] = useState<EventInsert>({
start_date: date ? getLocalDateString(date) : "",
start_time: date ? getNearestTimeOption(date, "start") : "",
end_time: date ? getNearestTimeOption(date, "end") : "",
@ -71,13 +76,30 @@ export const CreateEventModal = () => {
created_by: user.id,
});
// Initialize form data when in edit mode
useEffect(() => {
if (mode === "edit" && event) {
setFormEvent({
start_date: event.start_date,
start_time: event.start_time || "",
end_time: event.end_time || "",
tablo_id: event.tablo_id,
title: event.title,
description: event.description || "",
created_by: event.created_by,
});
}
}, [mode, event]);
return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden">
{/* Header with colored accent */}
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-6 text-white">
<div className="flex items-center justify-between">
<h2 className="text-xl font-medium">Nouvel événement</h2>
<h2 className="text-xl font-medium">
{mode === "edit" ? "Modifier l'événement" : "Nouvel événement"}
</h2>
<button
onClick={onClose}
className="text-white hover:text-gray-200 transition-colors"
@ -99,12 +121,19 @@ export const CreateEventModal = () => {
</button>
</div>
<div className="mt-2 text-blue-100 text-sm">
{date.toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
{mode === "edit" && event
? new Date(event.start_date).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
: date.toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</div>
</div>
@ -114,10 +143,10 @@ export const CreateEventModal = () => {
<div className="space-y-2">
<input
type="text"
value={createdEvent?.title}
value={formEvent?.title}
onChange={(e) =>
setCreatedEvent({
...createdEvent,
setFormEvent({
...formEvent,
title: e.target.value,
} as Event)
}
@ -136,10 +165,10 @@ export const CreateEventModal = () => {
</label>
<Select
placeholder="Sélectionner un tablo"
selectedKey={createdEvent?.tablo_id}
selectedKey={formEvent?.tablo_id}
onSelectionChange={(key) =>
setCreatedEvent({
...createdEvent,
setFormEvent({
...formEvent,
tablo_id: key as string,
} as Event)
}
@ -168,8 +197,8 @@ export const CreateEventModal = () => {
<DatePicker
aria-label="Date de l'événement"
value={
createdEvent?.start_date
? parseDate(createdEvent?.start_date)
formEvent?.start_date
? parseDate(formEvent?.start_date)
: null
}
minValue={today(getLocalTimeZone())}
@ -177,8 +206,8 @@ export const CreateEventModal = () => {
if (value === null) {
return;
}
setCreatedEvent({
...createdEvent,
setFormEvent({
...formEvent,
start_date: value.toString(),
});
}}
@ -194,14 +223,14 @@ export const CreateEventModal = () => {
<Select
aria-label="Heure de début"
className="min-w-[110px]"
selectedKey={createdEvent?.start_time}
selectedKey={formEvent?.start_time}
onSelectionChange={(value) => {
const option = timeOptions.find(
(option) => option.id === value
);
if (option && value) {
setCreatedEvent({
...createdEvent,
setFormEvent({
...formEvent,
start_time: value.toString(),
});
}
@ -225,14 +254,14 @@ export const CreateEventModal = () => {
<Select
aria-label="Heure de fin"
className="min-w-[110px]"
selectedKey={createdEvent?.end_time}
selectedKey={formEvent?.end_time}
onSelectionChange={(value) => {
const option = timeOptions.find(
(option) => option.id === value
);
if (option && value) {
setCreatedEvent({
...createdEvent,
setFormEvent({
...formEvent,
end_time: value.toString(),
});
}
@ -256,10 +285,10 @@ export const CreateEventModal = () => {
Description
</label>
<textarea
value={createdEvent?.description ?? ""}
value={formEvent?.description ?? ""}
onChange={(e) =>
setCreatedEvent({
...createdEvent,
setFormEvent({
...formEvent,
description: e.target.value,
} as Event)
}
@ -277,7 +306,11 @@ export const CreateEventModal = () => {
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
onClick={onClose}
aria-label="Annuler la création d'événement"
aria-label={
mode === "edit"
? "Annuler la modification"
: "Annuler la création d'événement"
}
>
Annuler
</button>
@ -285,16 +318,27 @@ export const CreateEventModal = () => {
type="button"
className="px-6 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm hover:shadow-md"
onClick={() => {
const eventName = createdEvent?.title.trim() || "(Sans titre)";
createEvents(
{ ...createdEvent, title: eventName },
{ onSuccess: () => onClose() }
);
const eventName = formEvent?.title.trim() || "(Sans titre)";
if (mode === "edit" && event) {
updateEvent.mutate(
{ id: event.id, ...formEvent, title: eventName },
{ onSuccess: () => onClose() }
);
} else {
createEvents(
{ ...formEvent, title: eventName },
{ onSuccess: () => onClose() }
);
}
}}
disabled={!createdEvent?.tablo_id}
aria-label="Enregistrer l'événement"
disabled={!formEvent?.tablo_id}
aria-label={
mode === "edit"
? "Modifier l'événement"
: "Enregistrer l'événement"
}
>
Enregistrer
{mode === "edit" ? "Modifier" : "Enregistrer"}
</button>
</div>
</div>

View file

@ -237,3 +237,18 @@ export const useDeleteTablo = () => {
},
});
};
export const useGetAllTabloAccess = () => {
const user = useUser();
const { data, isLoading, error } = useQuery({
queryKey: ["tablo-access", user.id],
queryFn: async () => {
const { data } = await supabase
.from("tablo_access")
.select("*")
.eq("user_id", user.id);
return data;
},
});
return { data, isLoading, error };
};

View file

@ -14,7 +14,7 @@ import { SignUpPage } from "@ui/pages/signup";
import { ResetPasswordPage } from "@ui/pages/reset-password";
import { AuthenticationGateway } from "@ui/components/AuthenticationGateway";
import ChatProvider from "@ui/providers/ChatProvider";
import { CreateEventModal } from "@ui/components/CreateEventModal";
import { EventModal } from "@ui/components/EventModal";
import { ChantiersPage } from "@ui/pages/chantiers";
import { ChatPage } from "@ui/pages/chat";
import { FeedbackPage } from "@ui/pages/feedback";
@ -55,8 +55,11 @@ export const routes: RouteObject[] = [
element: <PlanningPage />,
children: [
{ index: true },
{ path: ":tablo_id" },
{ path: "create", element: <CreateEventModal /> },
{
path: ":tablo_id/events/:event_id/edit",
element: <EventModal mode="edit" />,
},
{ path: "create", element: <EventModal mode="create" /> },
],
},
{

File diff suppressed because it is too large Load diff

View file

@ -405,7 +405,7 @@ export function PublicBookingPage() {
)}
{eventType?.location && (
<div className="flex items-center gap-3">
<MapPinIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
<MapPinIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
<div>
<Text className="text-gray-600 dark:text-gray-400 text-sm">
{eventType.location}
@ -485,11 +485,11 @@ export function PublicBookingPage() {
? "text-gray-300 dark:text-gray-600 cursor-not-allowed"
: selectedDate?.toDateString() ===
date.toDateString()
? "bg-blue-600 text-white"
? "bg-purple-600 text-white"
: isToday(date)
? "bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 font-semibold"
: hasAvailableSlots(date)
? "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border-2 border-green-200 dark:border-green-800"
? "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 border-2 border-purple-200 dark:border-purple-800"
: "text-gray-400 dark:text-gray-500 cursor-not-allowed"
}`}
>

View file

@ -146,9 +146,10 @@ export function AvailabilitiesPage() {
variant="solid"
size="lg"
onPress={() => setExceptionModalOpen(true)}
className="[--btn-bg:var(--color-blue-800)]"
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
>
<PlusIcon /> Ajouter une exception
<PlusIcon className="text-[#1a1a1a] dark:text-white" /> Ajouter
une exception
</Button>
<Button
size="lg"
@ -226,7 +227,7 @@ export function AvailabilitiesPage() {
{DAYS_OF_WEEK.map((day) => (
<div
key={day}
className="bg-white dark:bg-gray-800 rounded-lg shadow p-2"
className="bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 p-2 dark:border dark:border-gray-600/30"
>
<div className="space-y-2">
<AvailabilityCard
@ -269,7 +270,7 @@ export function AvailabilitiesPage() {
</Text>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 p-4 dark:border dark:border-gray-600/30">
<Strong className="block mb-2">Votre fuseau horaire</Strong>
<Text className="text-gray-500">
{Intl.DateTimeFormat().resolvedOptions().timeZone}
@ -283,7 +284,7 @@ export function AvailabilitiesPage() {
</Text>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 p-4 dark:border dark:border-gray-600/30">
<Strong className="block mb-2">Information</Strong>
<Text className="text-gray-500 text-sm">
Les créneaux horaires seront automatiquement convertis dans
@ -317,15 +318,16 @@ export function AvailabilitiesPage() {
variant="solid"
size="lg"
onPress={() => setExceptionModalOpen(true)}
className="[--btn-bg:var(--color-blue-800)]"
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
>
<PlusIcon /> Ajouter une exception
<PlusIcon className="text-[#1a1a1a] dark:text-white" /> Ajouter
une exception
</Button>
</div>
{exceptions.length === 0 ? (
<div className="text-center py-12">
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-8">
<div className="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-8 dark:border dark:border-gray-600/20">
<Text className="text-gray-500 dark:text-gray-400 text-lg mb-4">
Aucune exception définie
</Text>
@ -340,7 +342,7 @@ export function AvailabilitiesPage() {
{exceptions.map((exception, index) => (
<div
key={`${exception.date}-${index}`}
className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border border-gray-200 dark:border-gray-700"
className="bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 p-4 border border-gray-200 dark:border-gray-600/50"
>
<div className="flex justify-between items-start">
<div className="flex-1">
@ -598,7 +600,7 @@ export function AvailabilitiesPage() {
]);
}}
>
<PlusIcon className="w-4 h-4 mr-1" />
<PlusIcon className="w-4 h-4 mr-1 text-[#1a1a1a] dark:text-white" />
Ajouter un créneau
</Button>
</div>

View file

@ -16,7 +16,7 @@ import { SearchIcon } from "lucide-react";
import { CalendarIcon } from "@ui/ui-library/icons/outline/calendar";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useEventsByTablo } from "@ui/hooks/events";
import { useTablosList } from "@ui/hooks/tablos";
import { useTablosList, useGetAllTabloAccess } from "@ui/hooks/tablos";
import { EventAndTablo } from "@ui/types/events.types";
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
import { EventDetailsModal } from "@ui/components/EventDetailsModal";
@ -57,6 +57,8 @@ export const BookingsPage = () => {
const { data: events = [], isLoading: eventsLoading } = useEventsByTablo(
selectedTabloId !== "all" ? selectedTabloId : null
);
// Fetch all tablo accesses for permissions
const { data: tabloAccess } = useGetAllTabloAccess();
// Filter and search events
const filteredEvents = useMemo(() => {
@ -157,19 +159,19 @@ export const BookingsPage = () => {
if (eventDate.getTime() === today.getTime()) {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/60 dark:text-blue-200">
Aujourd&apos;hui
</span>
);
} else if (eventDate > today) {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/60 dark:text-purple-200">
À venir
</span>
);
} else {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-800/60 dark:text-gray-200">
Passé
</span>
);
@ -184,8 +186,17 @@ export const BookingsPage = () => {
navigate(`/planning/create?date=${dateParam}${tabloParam}`);
};
// Check if an event can be edited (admin access required)
const canEditEvent = (event: EventAndTablo) => {
return tabloAccess?.find(
(access) => access.tablo_id === event.tablo_id && access.is_admin
)
? true
: false;
};
const handleEditEvent = (event: EventAndTablo) => {
if (event.event_id && event.tablo_id) {
if (event.event_id && event.tablo_id && canEditEvent(event)) {
navigate(`/planning/${event.tablo_id}/events/${event.event_id}/edit`);
}
};
@ -198,7 +209,7 @@ export const BookingsPage = () => {
return (
<div className="min-h-screen">
{/* Header */}
<header className="bg-white dark:bg-gray-800 shadow">
<header className="bg-white dark:bg-gray-700/40 shadow-sm dark:shadow-gray-900/20 dark:border-b dark:border-gray-600/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div>
@ -211,10 +222,10 @@ export const BookingsPage = () => {
</div>
<div className="flex items-center space-x-3">
<Button
className="bg-emerald-700 text-white hover:bg-emerald-600"
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
onPress={handleCreateEvent}
>
<CalendarIcon className="w-4 h-4 mr-2" />
<CalendarIcon className="w-4 h-4 mr-2 text-[#1a1a1a] dark:text-white" />
Nouvel événement
</Button>
</div>
@ -225,12 +236,12 @@ export const BookingsPage = () => {
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<div className="bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 dark:border dark:border-gray-600/30 p-6 mb-6">
<div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center">
{/* Search */}
<div className="flex-1 w-full">
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-300 w-4 h-4" />
<Input
type="text"
placeholder="Rechercher un événement..."
@ -301,7 +312,7 @@ export const BookingsPage = () => {
</div>
{/* Events List */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 dark:border dark:border-gray-600/30">
{tablosLoading || eventsLoading ? (
<div className="flex items-center justify-center h-screen">
<LoadingSpinner />
@ -323,7 +334,7 @@ export const BookingsPage = () => {
{paginatedEvents.map((event) => (
<div
key={event.event_id}
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer"
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-600/40 transition-colors cursor-pointer"
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
@ -377,7 +388,7 @@ export const BookingsPage = () => {
{/* Pagination Controls */}
{totalItems > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow mt-4 px-6 py-4">
<div className="bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 dark:border dark:border-gray-600/30 mt-4 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
<span>
@ -475,7 +486,7 @@ export const BookingsPage = () => {
{/* Stats Summary */}
{filteredEvents.length > 0 && (
<div className="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="mt-6 bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 dark:border dark:border-gray-600/30 p-6">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
@ -529,6 +540,7 @@ export const BookingsPage = () => {
setSelectedEvent(null);
}}
onEdit={() => selectedEvent && handleEditEvent(selectedEvent)}
canEdit={selectedEvent ? canEditEvent(selectedEvent) : false}
/>
</main>
</div>

View file

@ -36,9 +36,9 @@ export function ChatPage() {
}, [channelFromUrl]);
return (
<div className="flex h-screen bg-gray-50 dark:bg-gray-900">
<div className="flex h-screen bg-gray-50 dark:bg-background">
<div
className={`border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 transition-all duration-300 ease-in-out overflow-hidden ${
className={`border-r border-gray-200 dark:border-gray-600/50 bg-white dark:bg-gray-700/40 transition-all duration-300 ease-in-out overflow-hidden ${
isChannelListExpanded ? "w-80" : "w-0"
}`}
>
@ -65,7 +65,7 @@ export function ChatPage() {
)}
/>
</div>
<div className="flex-1 bg-white dark:bg-gray-800">
<div className="flex-1 bg-white dark:bg-gray-700/40">
<Channel channel={channel}>
<Window>
<CustomChannelHeader

View file

@ -109,10 +109,10 @@ export function EventTypesPage() {
<Button
size="lg"
variant="solid"
className="[--btn-bg:var(--color-blue-800)]"
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
onPress={handleCreateEventType}
>
<PlusIcon /> Nouveau type
<PlusIcon className="text-[#1a1a1a] dark:text-white" /> Nouveau type
</Button>
</div>
@ -121,7 +121,7 @@ export function EventTypesPage() {
{eventTypesData?.map((eventType) => (
<div
key={eventType.id}
className={`bg-white dark:bg-gray-800 rounded-lg shadow p-6 border ${
className={`bg-white dark:bg-gray-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 dark:border-gray-600/50 p-6 border ${
eventType.isActive ? "opacity-100" : "opacity-60"
}`}
>
@ -139,7 +139,7 @@ export function EventTypesPage() {
"_blank"
)
}
className="text-gray-500 hover:text-blue-600"
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
tooltip="Aperçu"
>
<ExternalLinkIcon className="w-4 h-4" />
@ -148,7 +148,7 @@ export function EventTypesPage() {
copyValue={getPublicLink(eventType.standardName)}
label="Copier le lien"
labelAfterCopied="Lien copié"
className="text-gray-500 hover:text-blue-600"
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
></CopyButton>
<Button
variant="plain"
@ -159,7 +159,7 @@ export function EventTypesPage() {
eventType as EventTypeConfig
)
}
className="text-gray-500 hover:text-blue-600"
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
>
<EditIcon className="w-4 h-4" />
</Button>
@ -167,7 +167,7 @@ export function EventTypesPage() {
variant="plain"
isIconOnly
onPress={() => deleteEventType({ id: eventType.id })}
className="text-gray-500 hover:text-red-600"
className="text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400"
>
<TrashIcon className="w-4 h-4" />
</Button>
@ -180,12 +180,16 @@ export function EventTypesPage() {
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">Durée:</span>
<span className="text-gray-500 dark:text-gray-400">
Durée:
</span>
<span className="font-medium">{eventType.duration} min</span>
</div>
{eventType.bufferTime && (
<div className="flex justify-between">
<span className="text-gray-500">Temps de battement:</span>
<span className="text-gray-500 dark:text-gray-400">
Temps de battement:
</span>
<span className="font-medium">
{eventType.bufferTime} min
</span>
@ -193,7 +197,9 @@ export function EventTypesPage() {
)}
{eventType.maxBookingsPerDay && (
<div className="flex justify-between">
<span className="text-gray-500">Max par jour:</span>
<span className="text-gray-500 dark:text-gray-400">
Max par jour:
</span>
<span className="font-medium">
{eventType.maxBookingsPerDay}
</span>
@ -201,7 +207,7 @@ export function EventTypesPage() {
)}
{eventType.minAdvanceBooking && (
<div className="flex justify-between">
<span className="text-gray-500">
<span className="text-gray-500 dark:text-gray-400">
Réservation à l&apos;avance:
</span>
<span className="font-medium">
@ -215,7 +221,9 @@ export function EventTypesPage() {
</div>
)}
<div className="flex justify-between items-center pt-2 border-t border-gray-200 dark:border-gray-700">
<span className="text-gray-500">Statut:</span>
<span className="text-gray-500 dark:text-gray-400">
Statut:
</span>
<ToggleButton
isSelected={eventType.isActive}
onChange={() =>
@ -237,15 +245,16 @@ export function EventTypesPage() {
{eventTypesData?.length === 0 && (
<div className="text-center py-12">
<Text className="text-gray-500 mb-4">
<Text className="text-gray-500 dark:text-gray-400 mb-4">
Aucun type d&apos;événement configuré
</Text>
<Button
variant="solid"
onPress={handleCreateEventType}
className="[--btn-bg:var(--color-blue-800)]"
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
>
<PlusIcon /> Créer votre premier type
<PlusIcon className="text-[#1a1a1a] dark:text-white" /> Créer
votre premier type
</Button>
</div>
)}

View file

@ -6,6 +6,9 @@ import { useLoginEmail } from "@ui/hooks/auth";
import { Form } from "@ui/ui-library/form";
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { Link } from "react-router-dom";
import { useTheme } from "@ui/contexts/ThemeContext";
import { SunIcon, MoonIcon, MonitorIcon } from "lucide-react";
import { AnimatedBackground } from "@ui/components/AnimatedBackground";
export function LoginPage() {
const redirectUrl = localStorage.getItem("redirectUrl");
@ -21,6 +24,32 @@ export function LoginPage() {
password: "",
});
// Theme
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
if (theme === "light") {
setTheme("dark");
} else if (theme === "dark") {
setTheme("system");
} else {
setTheme("light");
}
};
const getThemeIcon = () => {
switch (theme) {
case "light":
return <SunIcon className="w-5 h-5" />;
case "dark":
return <MoonIcon className="w-5 h-5" />;
case "system":
return <MonitorIcon className="w-5 h-5" />;
default:
return <SunIcon className="w-5 h-5" />;
}
};
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
login({
@ -30,128 +59,155 @@ export function LoginPage() {
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-100 via-blue-50 to-white dark:bg-gradient-to-br dark:from-slate-900 dark:via-blue-950 dark:via-blue-900 dark:to-blue-800">
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-100 via-purple-50 to-white dark:bg-gradient-to-br dark:from-gray-900 dark:via-slate-900 dark:via-gray-800 dark:to-slate-800 animate-gradient-x bg-[length:400%_400%] relative overflow-hidden">
<AnimatedBackground />
<div
className={twMerge(
"w-full max-w-lg p-8 bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl",
"border border-blue-200 dark:border-blue-900/30",
"shadow-xl"
"w-full max-w-lg rounded-2xl animate-border-light",
"shadow-2xl shadow-purple-500/10 dark:shadow-black/30"
)}
onClick={(e) => e.stopPropagation()}
>
<div className="mb-6">
<Link
to="/landing"
className="inline-flex items-center text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<div className="relative w-full h-full p-8 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md rounded-2xl border border-purple-200 dark:border-purple-400/30 z-10">
<div className="mb-6 flex items-center justify-between">
<a
href="https://www.xtablo.com"
className="inline-flex items-center text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Retour à l&apos;accueil
</Link>
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-8 text-center">
Se connecter
</h1>
<div className="space-y-4 flex flex-col items-center">
<Form
className="space-y-4 w-95 max-w-md mx-auto"
onSubmit={onSubmit}
validationErrors={errors}
>
<TextField isRequired name="email">
<Label>
Email <span className="text-red-500">*</span>
</Label>
<Input
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
/>
<FieldError />
</TextField>
<TextField isRequired name="password">
<Label>
Mot de passe <span className="text-red-500">*</span>
</Label>
<Input
type="password"
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
required
/>
<FieldError />
</TextField>
<div className="flex items-center justify-between">
<Link to="/reset-password">
<a className="text-sm text-blue-600 hover:text-blue-500">
Mot de passe oublié ?
</a>
</Link>
</div>
<Button
className={twMerge(
"w-full bg-blue-700 text-white",
"hover:bg-blue-600"
)}
type="submit"
pendingLabel="Connexion..."
>
{isPending ? "Connexion..." : "Se connecter"}
</Button>
</Form>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200 dark:border-slate-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span
className={twMerge(
"px-4 py-1 bg-white dark:bg-slate-800",
"text-slate-500 dark:text-slate-400",
"text-sm font-medium",
"rounded-full",
"relative z-10",
"before:absolute before:w-[100px] before:h-[1px] before:bg-slate-300 dark:before:bg-slate-600 before:left-[-110px] before:top-1/2",
"after:absolute after:w-[100px] after:h-[1px] after:bg-slate-300 dark:after:bg-slate-600 after:right-[-110px] after:top-1/2"
)}
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
Ou continuer avec
</span>
</div>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Retour à l&apos;accueil
</a>
{/* Theme Toggle */}
<Button
variant="plain"
isIconOnly
onPress={toggleTheme}
className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 p-2"
aria-label={`Changer le thème (actuellement: ${theme})`}
>
{getThemeIcon()}
</Button>
</div>
<LoginWithGoogle />
{/* Xtablo Icon */}
<div className="flex justify-center mb-6">
<img
src="/icon.png"
alt="Xtablo"
className="w-16 h-16 object-contain block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="w-16 h-16 object-contain hidden dark:block"
/>
</div>
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
Pas encore de compte ?{" "}
<Link to="/signup">
<a className="text-blue-600 hover:text-blue-500 font-medium">
S&apos;inscrire
</a>
</Link>
</p>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-8 text-center">
Se connecter à Xtablo
</h1>
<div className="space-y-4 flex flex-col items-center">
<Form
className="space-y-4 w-95 max-w-md mx-auto"
onSubmit={onSubmit}
validationErrors={errors}
>
<TextField isRequired name="email">
<Label>
Email <span className="text-red-500">*</span>
</Label>
<Input
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
/>
<FieldError />
</TextField>
<TextField isRequired name="password">
<Label>
Mot de passe <span className="text-red-500">*</span>
</Label>
<Input
type="password"
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
required
/>
<FieldError />
</TextField>
{/* <div className="flex items-center justify-between">
<Link to="/reset-password">
<a className="text-sm text-purple-600 hover:text-purple-500 dark:text-purple-400 dark:hover:text-purple-300">
Mot de passe oublié ?
</a>
</Link>
</div> */}
<Button
className={twMerge(
"w-full bg-black border-black text-white dark:bg-black dark:border-black dark:text-white",
"hover:bg-gray-800 dark:hover:bg-gray-800 transition-colors"
)}
type="submit"
pendingLabel="Connexion..."
>
{isPending ? "Connexion..." : "Se connecter"}
</Button>
</Form>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200 dark:border-slate-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span
className={twMerge(
"px-4 py-1 bg-white dark:bg-slate-800",
"text-slate-500 dark:text-slate-400",
"text-sm font-medium",
"rounded-full",
"relative z-10",
"before:absolute before:w-[100px] before:h-[1px] before:bg-slate-300 dark:before:bg-slate-600 before:left-[-110px] before:top-1/2",
"after:absolute after:w-[100px] after:h-[1px] after:bg-slate-300 dark:after:bg-slate-600 after:right-[-110px] after:top-1/2"
)}
>
Ou continuer avec
</span>
</div>
</div>
<LoginWithGoogle />
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
Pas encore de compte ?{" "}
<Link to="/signup">
<a className="text-black hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300 font-medium text-sm px-2 py-1 rounded border-gray-300 dark:border-slate-600 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors">
S&apos;inscrire
</a>
</Link>
</p>
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { useTablosList } from "@ui/hooks/tablos";
import { useGetAllTabloAccess, useTablosList } from "@ui/hooks/tablos";
import { useEventsByTablo, useDeleteEvent } from "@ui/hooks/events";
import {
Select,
@ -13,6 +13,7 @@ import { generateICSFromEvents, downloadICSFile } from "@ui/utils/helpers";
import { ImportICSModal } from "@ui/components/ImportICSModal";
import { WebcalModal } from "@ui/components/WebcalModal";
import { FolderInputIcon, PlusIcon } from "lucide-react";
import { EventAndTablo } from "@ui/types/events.types";
type ViewType = "month" | "week" | "day";
@ -34,9 +35,28 @@ export const PlanningPage = () => {
// Fetch events for selected tablo or all tablos
const { data: tabloEvents = [], isLoading: tabloEventsLoading } =
useEventsByTablo(selectedTabloId !== "all" ? selectedTabloId : null);
// Fetch all tablo accesses
const { data: tabloAccess } = useGetAllTabloAccess();
const deleteEvent = useDeleteEvent();
// Check if an event can be deleted (e.g., based on permissions, event status, etc.)
const canDeleteEvent = (event: EventAndTablo) => {
if (
tabloAccess?.find(
(access) => access.tablo_id === event.tablo_id && access.is_admin
)
) {
return true;
}
return false;
};
// Check if an event can be edited (same logic as delete - admin access required)
const canEditEvent = (event: EventAndTablo) => {
return canDeleteEvent(event);
};
// Keyboard shortcuts for view switching
useEffect(() => {
const handleKeyPress = (event: KeyboardEvent) => {
@ -251,12 +271,13 @@ export const PlanningPage = () => {
} ${startOfWeek.getFullYear()}`;
}
} else {
return currentDate.toLocaleDateString("fr-FR", {
const dateString = currentDate.toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
return dateString.charAt(0).toUpperCase() + dateString.slice(1);
}
};
@ -303,7 +324,7 @@ export const PlanningPage = () => {
);
const renderMonthView = () => (
<div className="flex-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
<div className="flex-1 bg-white dark:bg-gray-700/40 border border-gray-200 dark:border-gray-600/50">
{/* Days header */}
<div className="grid grid-cols-7 border-b border-gray-200 dark:border-gray-700">
{dayNamesShort.map((day) => (
@ -327,8 +348,8 @@ export const PlanningPage = () => {
: ""
} ${
day
? "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
: "bg-gray-50 dark:bg-gray-900"
? "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-600/40"
: "bg-gray-50 dark:bg-gray-800/60"
} ${
day && formatDate(day) === formatDate(new Date())
? "bg-blue-50 dark:bg-blue-900/20"
@ -359,25 +380,30 @@ export const PlanningPage = () => {
</div>
<div className="space-y-1">
{getEventsForDate(day)
.sort((a, b) => a.start_time.localeCompare(b.start_time))
.slice(0, 3)
.map((event) => (
<div
key={event.event_id}
className={`text-[10px] px-1.5 py-0.5 rounded text-white ${event.tablo_color} truncate cursor-pointer hover:opacity-80 group relative leading-tight`}
className={`text-[10px] px-1.5 py-1 rounded text-white ${
event.tablo_color
} truncate group relative leading-tight ${
canEditEvent(event)
? "cursor-pointer hover:opacity-80"
: "cursor-default opacity-75"
}`}
title={`${formatTime(event.start_time)} ${event.title}${
selectedTabloId === "all" && event.tablo_name
? ` - ${event.tablo_name}`
: ""
}`}
}${!canEditEvent(event) ? " (Lecture seule)" : ""}`}
onClick={(e) => {
e.stopPropagation();
navigate({
pathname: "/planning/create",
search:
selectedTabloId === "all"
? `?date=${day.toISOString()}`
: `?date=${day.toISOString()}&tablo_id=${selectedTabloId}`,
});
if (canEditEvent(event)) {
navigate(
`/planning/${event.tablo_id}/events/${event.event_id}/edit`
);
}
}}
>
<div className="truncate">
@ -388,16 +414,18 @@ export const PlanningPage = () => {
</span>
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
deleteEvent.mutate(event.event_id);
}}
className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-sm flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm"
title="Supprimer l'événement"
>
×
</button>
{canDeleteEvent(event) && (
<button
onClick={(e) => {
e.stopPropagation();
deleteEvent.mutate(event.event_id);
}}
className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-sm flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm"
title="Supprimer l'événement"
>
×
</button>
)}
</div>
))}
{getEventsForDate(day).length > 3 && (
@ -415,7 +443,7 @@ export const PlanningPage = () => {
);
const renderWeekView = () => (
<div className="flex-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex flex-col">
<div className="flex-1 bg-white dark:bg-gray-700/40 border border-gray-200 dark:border-gray-600/50 flex flex-col">
{/* Week header */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
<div className="w-20 p-4 border-r border-gray-200 dark:border-gray-700 flex-shrink-0"></div>
@ -455,7 +483,7 @@ export const PlanningPage = () => {
{getWeekDays().map((day, index) => (
<div
key={`${day.toISOString()}-${time}`}
className={`flex-1 min-h-[60px] border-r border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer relative ${
className={`flex-1 min-h-[60px] border-r border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600/40 cursor-pointer relative ${
index === 6 ? "border-r-0" : ""
}`}
onClick={() => {
@ -493,17 +521,35 @@ export const PlanningPage = () => {
return (
<div
key={event.event_id}
className={`absolute left-1 right-1 p-0.5 rounded text-white ${event.tablo_color} text-xs overflow-hidden z-10 group cursor-pointer hover:opacity-90`}
className={`absolute left-1 right-1 p-0.5 rounded text-white ${
event.tablo_color
} text-xs overflow-hidden z-10 group ${
canEditEvent(event)
? "cursor-pointer hover:opacity-90"
: "cursor-default opacity-75"
}`}
style={{
top: `${eventOffset}px`,
height: `${eventHeight}px`,
minHeight: "30px",
}}
title={`${formatTime(event.start_time)} - ${formatTime(
event.end_time
)} ${event.title}${
selectedTabloId === "all" && event.tablo_name
? ` - ${event.tablo_name}`
: ""
}${!canEditEvent(event) ? " (Lecture seule)" : ""}`}
onClick={(e) => {
e.stopPropagation();
if (canEditEvent(event)) {
navigate(
`/planning/${event.tablo_id}/events/${event.event_id}/edit`
);
}
}}
>
<div className="text-[10px] font-medium truncate leading-tight">
<div className="text-[10px] font-medium leading-tight">
{event.title}
{selectedTabloId === "all" && event.tablo_name && (
<span className="opacity-75 ml-1">
@ -517,16 +563,18 @@ export const PlanningPage = () => {
{formatTime(event.end_time)}
</div>
)}
<button
onClick={(e) => {
e.stopPropagation();
deleteEvent.mutate(event.event_id);
}}
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-xs flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm z-30"
title="Supprimer l'événement"
>
×
</button>
{canDeleteEvent(event) && (
<button
onClick={(e) => {
e.stopPropagation();
deleteEvent.mutate(event.event_id);
}}
className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-sm flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm"
title="Supprimer l'événement"
>
×
</button>
)}
</div>
);
})}
@ -539,7 +587,7 @@ export const PlanningPage = () => {
);
const renderDayView = () => (
<div className="flex-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
<div className="flex-1 bg-white dark:bg-gray-700/40 border border-gray-200 dark:border-gray-600/50">
{/* Day header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700 text-center">
<div className="text-sm text-gray-500 dark:text-gray-400 uppercase">
@ -555,7 +603,7 @@ export const PlanningPage = () => {
{timeSlots.map((time) => (
<div
key={time}
className="flex border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer relative min-h-[60px]"
className="flex border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600/40 cursor-pointer relative min-h-[60px]"
onClick={() => {
const [hour] = time.split(":").map(Number);
const dateWithTime = new Date(currentDate);
@ -595,14 +643,32 @@ export const PlanningPage = () => {
return (
<div
key={event.event_id}
className={`absolute left-2 right-2 p-1 rounded text-white ${event.tablo_color} overflow-hidden z-10 group cursor-pointer hover:opacity-90`}
className={`absolute left-2 right-2 p-1 rounded text-white ${
event.tablo_color
} overflow-hidden z-10 group ${
canEditEvent(event)
? "cursor-pointer hover:opacity-90"
: "cursor-default opacity-75"
}`}
style={{
top: `${eventOffset}px`,
height: `${eventHeight}px`,
minHeight: "30px",
}}
title={`${formatTime(event.start_time)} - ${formatTime(
event.end_time
)} ${event.title}${
selectedTabloId === "all" && event.tablo_name
? ` - ${event.tablo_name}`
: ""
}${!canEditEvent(event) ? " (Lecture seule)" : ""}`}
onClick={(e) => {
e.stopPropagation();
if (canEditEvent(event)) {
navigate(
`/planning/${event.tablo_id}/events/${event.event_id}/edit`
);
}
}}
>
<div className="text-[10px] font-medium truncate leading-tight">
@ -624,16 +690,18 @@ export const PlanningPage = () => {
{event.description}
</div>
)}
<button
onClick={(e) => {
e.stopPropagation();
deleteEvent.mutate(event.event_id);
}}
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-xs flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm z-30"
title="Supprimer l'événement"
>
×
</button>
{canDeleteEvent(event) && (
<button
onClick={(e) => {
e.stopPropagation();
deleteEvent.mutate(event.event_id);
}}
className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-all text-sm flex items-center justify-center hover:bg-red-600 hover:scale-110 shadow-sm"
title="Supprimer l'événement"
>
×
</button>
)}
</div>
);
})}
@ -645,16 +713,13 @@ export const PlanningPage = () => {
);
return (
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
<div className="min-h-screen bg-gray-100 dark:bg-background">
<div className="flex">
{/* Sidebar */}
<div className="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 min-h-screen">
<div className="w-64 bg-white dark:bg-gray-700/40 border-r border-gray-200 dark:border-gray-600/50 min-h-screen">
<div className="p-4">
{/* Tablo Selector */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Tablo
</label>
<Select
placeholder={
tablosLoading ? "Chargement..." : "Sélectionner un tablo"
@ -693,15 +758,15 @@ export const PlanningPage = () => {
);
}
}}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium shadow-sm disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
className="w-full px-4 py-2 bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity font-medium shadow-sm disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 rounded-lg"
>
<PlusIcon className="w-5 h-5 mr-2" />
<PlusIcon className="w-5 h-5 mr-2 text-[#1a1a1a] dark:text-white" />
<span className="text-sm">Créer un événement</span>
</button>
<button
onClick={() => setIsImportModalOpen(true)}
className="w-full px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium shadow-sm mt-2 flex items-center justify-center space-x-2"
className="w-full px-4 py-2 bg-green-600 dark:bg-purple-600 text-white rounded-lg hover:bg-green-700 dark:hover:bg-purple-700 transition-colors font-medium shadow-sm mt-2 flex items-center justify-center space-x-2"
>
<FolderInputIcon className="w-5 h-5 mr-2" />
<span className="text-sm">Importer un planning</span>
@ -726,7 +791,7 @@ export const PlanningPage = () => {
<div
key={index}
className={`text-center p-1 cursor-pointer rounded ${
day ? "hover:bg-gray-100 dark:hover:bg-gray-700" : ""
day ? "hover:bg-gray-100 dark:hover:bg-gray-600/40" : ""
} ${
day && formatDate(day) === formatDate(new Date())
? "bg-blue-600 text-white"
@ -750,7 +815,7 @@ export const PlanningPage = () => {
{/* Main Content */}
<div className="flex-1 flex flex-col">
{/* Header */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4">
<div className="bg-white dark:bg-gray-700/40 border-b border-gray-200 dark:border-gray-600/50 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
@ -800,9 +865,9 @@ export const PlanningPage = () => {
</svg>
</button>
</div>
<h2 className="text-xl font-medium text-gray-900 dark:text-white">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
{getViewTitle()}
</h2>
</h3>
</div>
<div className="flex items-center space-x-2">
@ -914,13 +979,6 @@ export const PlanningPage = () => {
</div>
</div>
{/* {isEventModalOpen && (
<CreateEventModal
date={selectedDate}
close={() => setIsEventModalOpen(false)}
/>
)} */}
<Outlet />
{isImportModalOpen && (

View file

@ -7,6 +7,9 @@ import { useSignUp } from "@ui/hooks/auth";
import { Form } from "@ui/ui-library/form";
import { Text } from "@ui/ui-library/text";
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { AnimatedBackground } from "@ui/components/AnimatedBackground";
import { useTheme } from "@ui/contexts/ThemeContext";
import { SunIcon, MoonIcon, MonitorIcon } from "lucide-react";
export function SignUpPage() {
const navigate = useNavigate();
@ -26,14 +29,40 @@ export function SignUpPage() {
business_name: "",
});
// Theme
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
if (theme === "light") {
setTheme("dark");
} else if (theme === "dark") {
setTheme("system");
} else {
setTheme("light");
}
};
const getThemeIcon = () => {
switch (theme) {
case "light":
return <SunIcon className="w-5 h-5" />;
case "dark":
return <MoonIcon className="w-5 h-5" />;
case "system":
return <MonitorIcon className="w-5 h-5" />;
default:
return <SunIcon className="w-5 h-5" />;
}
};
const validateForm = () => {
const errors: Record<string, string> = {};
// Business name validation
if (formData.business_name.length < 3) {
errors.business_name =
"Le nom de l'entreprise doit contenir au moins 3 caractères";
}
// // Business name validation
// if (formData.business_name.length < 3) {
// errors.business_name =
// "Le nom de l'entreprise doit contenir au moins 3 caractères";
// }
// Password length validation
if (formData.password.length < 8) {
@ -75,212 +104,249 @@ export function SignUpPage() {
return (
<div
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-100 via-blue-50 to-white dark:bg-gradient-to-br dark:from-slate-900 dark:via-blue-950 dark:via-blue-900 dark:to-blue-800"
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-100 via-purple-50 to-white dark:bg-gradient-to-br dark:from-gray-900 dark:via-slate-900 dark:via-gray-800 dark:to-slate-800 animate-gradient-x bg-[length:400%_400%] relative overflow-hidden"
onClick={() => navigate("/")}
>
<AnimatedBackground />
<div
className={twMerge(
"w-full max-w-xl p-8 bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl",
"border border-blue-200 dark:border-blue-900/30",
"shadow-xl"
"w-full max-w-xl rounded-2xl animate-border-light",
"shadow-2xl shadow-purple-500/10 dark:shadow-black/30"
)}
onClick={(e) => e.stopPropagation()}
>
<div className="mb-6">
<Link
to="/landing"
className="inline-flex items-center text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<div className="relative w-full h-full p-6 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md rounded-2xl border border-purple-200 dark:border-purple-400/30 z-10">
<div className="mb-4 flex items-center justify-between">
<a
href="https://www.xtablo.com"
className="inline-flex items-center text-sm text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Retour à l&apos;accueil
</Link>
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-8 text-center">
Créer un compte
</h1>
<div className="space-y-4 flex flex-col items-center">
<Form
className="space-y-4 w-full"
onSubmit={onSubmit}
validationErrors={errors}
>
<div className="grid grid-cols-2 gap-4">
<TextField isRequired name="first_name">
<Label>
Prénom <span className="text-red-500">*</span>
</Label>
<Input
type="text"
value={formData.first_name}
onChange={(e) =>
setFormData({ ...formData, first_name: e.target.value })
}
required
/>
<FieldError />
</TextField>
<TextField isRequired name="last_name">
<Label>
Nom <span className="text-red-500">*</span>
</Label>
<Input
type="text"
value={formData.last_name}
onChange={(e) =>
setFormData({ ...formData, last_name: e.target.value })
}
required
/>
<FieldError />
</TextField>
</div>
<TextField isRequired name="business_name">
<Label>
Nom de l&apos;entreprise <span className="text-red-500">*</span>
</Label>
<Input
type="text"
value={formData.business_name}
onChange={(e) =>
setFormData({ ...formData, business_name: e.target.value })
}
required
/>
<FieldError />
</TextField>
<TextField isRequired name="email">
<Label>
Email professionnel <span className="text-red-500">*</span>
</Label>
<Input
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
/>
<FieldError />
</TextField>
<TextField isRequired name="password">
<Label>
Mot de passe <span className="text-red-500">*</span>
</Label>
<Input
type="password"
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
required
/>
<FieldError />
{!errors.password && (
<Text slot="description" className="text-red-500">
{errors.password}
</Text>
)}
</TextField>
<TextField isRequired name="confirmPassword">
<Label>
Confirmer le mot de passe{" "}
<span className="text-red-500">*</span>
</Label>
<Input
type="password"
value={formData.confirmPassword}
onChange={(e) =>
setFormData({ ...formData, confirmPassword: e.target.value })
}
required
/>
<FieldError />
</TextField>
<TextField className="flex items-start">
<Input
type="checkbox"
id="terms"
className="mt-1 mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-slate-300 rounded"
required
/>
<Label
htmlFor="terms"
className="text-sm text-slate-700 dark:text-slate-300"
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
J&apos;accepte les{" "}
<a href="#" className="text-blue-600 hover:text-blue-500">
conditions d&apos;utilisation
</a>{" "}
et la{" "}
<a href="#" className="text-blue-600 hover:text-blue-500">
politique de confidentialité
</a>
</Label>
</TextField>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Retour à l&apos;accueil
</a>
{/* Theme Toggle */}
<Button
className={twMerge(
"w-full bg-blue-700 text-white",
"hover:bg-blue-600"
)}
type="submit"
isPending={isPending}
pendingLabel="Création du compte..."
variant="plain"
isIconOnly
onPress={toggleTheme}
className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 p-2"
aria-label={`Changer le thème (actuellement: ${theme})`}
>
{isPending ? "Création du compte..." : "Créer mon compte"}
{getThemeIcon()}
</Button>
</Form>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200 dark:border-slate-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span
className={twMerge(
"px-4 py-1 bg-white dark:bg-slate-800",
"text-slate-500 dark:text-slate-400",
"text-sm font-medium",
"rounded-full",
"relative z-10",
"before:absolute before:w-[100px] before:h-[1px] before:bg-slate-300 dark:before:bg-slate-600 before:left-[-110px] before:top-1/2",
"after:absolute after:w-[100px] after:h-[1px] after:bg-slate-300 dark:after:bg-slate-600 after:right-[-110px] after:top-1/2"
)}
>
Ou continuer avec
</span>
</div>
</div>
<LoginWithGoogle />
{/* Xtablo Icon */}
<div className="flex justify-center mb-4">
<img
src="/icon.png"
alt="Xtablo"
className="w-12 h-12 object-contain block dark:hidden"
/>
<img
src="/logo_white.png"
alt="Xtablo"
className="w-12 h-12 object-contain hidden dark:block"
/>
</div>
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
Déjà un compte ?{" "}
<Link to="/login">
<a className="text-blue-600 hover:text-blue-500 font-medium">
Se connecter
</a>
</Link>
</p>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white mb-6 text-center">
Créer un compte Xtablo
</h1>
<div className="space-y-3 flex flex-col items-center">
<Form
className="space-y-3 w-full"
onSubmit={onSubmit}
validationErrors={errors}
>
<div className="grid grid-cols-2 gap-3">
<TextField isRequired name="first_name">
<Label className="text-sm">
Prénom <span className="text-red-500">*</span>
</Label>
<Input
type="text"
value={formData.first_name}
onChange={(e) =>
setFormData({ ...formData, first_name: e.target.value })
}
required
/>
<FieldError />
</TextField>
<TextField isRequired name="last_name">
<Label className="text-sm">
Nom <span className="text-red-500">*</span>
</Label>
<Input
type="text"
value={formData.last_name}
onChange={(e) =>
setFormData({ ...formData, last_name: e.target.value })
}
required
/>
<FieldError />
</TextField>
</div>
{/* <TextField isRequired name="business_name">
<Label>
Nom de l&apos;entreprise{" "}
<span className="text-red-500">*</span>
</Label>
<Input
type="text"
value={formData.business_name}
onChange={(e) =>
setFormData({ ...formData, business_name: e.target.value })
}
required
/>
<FieldError />
</TextField> */}
<TextField isRequired name="email">
<Label className="text-sm">
Email professionnel <span className="text-red-500">*</span>
</Label>
<Input
type="email"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
required
/>
<FieldError />
</TextField>
<TextField isRequired name="password">
<Label className="text-sm">
Mot de passe <span className="text-red-500">*</span>
</Label>
<Input
type="password"
value={formData.password}
onChange={(e) =>
setFormData({ ...formData, password: e.target.value })
}
required
/>
<FieldError />
{!errors.password && (
<Text slot="description" className="text-red-500">
{errors.password}
</Text>
)}
</TextField>
<TextField isRequired name="confirmPassword">
<Label className="text-sm">
Confirmer le mot de passe{" "}
<span className="text-red-500">*</span>
</Label>
<Input
type="password"
value={formData.confirmPassword}
onChange={(e) =>
setFormData({
...formData,
confirmPassword: e.target.value,
})
}
required
/>
<FieldError />
</TextField>
<TextField className="flex items-start">
<Input
type="checkbox"
id="terms"
className="mt-1 mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-slate-300 rounded"
required
/>
<Label
htmlFor="terms"
className="text-xs text-slate-600 dark:text-slate-300"
>
J&apos;accepte les{" "}
<a
href="#"
className="text-black hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
>
conditions d&apos;utilisation
</a>{" "}
et la{" "}
<a
href="#"
className="text-black hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
>
politique de confidentialité
</a>
</Label>
</TextField>
<Button
className={twMerge(
"w-full bg-black border-black text-white dark:bg-black dark:border-black dark:text-white",
"hover:bg-gray-800 dark:hover:bg-gray-800 transition-colors"
)}
type="submit"
isPending={isPending}
pendingLabel="Création du compte..."
>
{isPending ? "Création du compte..." : "Créer mon compte"}
</Button>
</Form>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-slate-200 dark:border-slate-700"></div>
</div>
<div className="relative flex justify-center text-sm">
<span
className={twMerge(
"px-3 py-1 bg-white dark:bg-slate-800",
"text-slate-500 dark:text-slate-400",
"text-xs font-medium",
"rounded-full",
"relative z-10",
"before:absolute before:w-[100px] before:h-[1px] before:bg-slate-300 dark:before:bg-slate-600 before:left-[-110px] before:top-1/2",
"after:absolute after:w-[100px] after:h-[1px] after:bg-slate-300 dark:after:bg-slate-600 after:right-[-110px] after:top-1/2"
)}
>
Ou continuer avec
</span>
</div>
</div>
<LoginWithGoogle />
<p className="text-center text-xs text-slate-600 dark:text-slate-400">
Déjà un compte ?{" "}
<Link to="/login">
<a className="text-black hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300 font-medium text-sm px-2 py-1 rounded border-gray-300 dark:border-slate-600 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors">
Se connecter
</a>
</Link>
</p>
</div>
</div>
</div>
</div>

View file

@ -1,4 +1,3 @@
import { SignOutButton } from "@ui/components/SignOutButton";
import { CreateTabloModal } from "@ui/components/CreateTabloModal";
import { TabloModal } from "@ui/components/TabloModal";
import { DeleteTabloModal } from "@ui/components/DeleteTabloModal";
@ -239,11 +238,11 @@ export const TabloPage = () => {
<div className="flex items-center gap-3">
<button
type="button"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 hover:shadow-lg hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-md"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity rounded-md shadow-md"
onClick={openCreateModal}
>
<svg
className="w-4 h-4"
className="w-4 h-4 text-[#1a1a1a] dark:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -258,7 +257,6 @@ export const TabloPage = () => {
</svg>
<span>Nouveau tablo</span>
</button>
<SignOutButton />
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
@ -281,11 +279,11 @@ export const TabloPage = () => {
<div className="flex items-center gap-3">
<button
type="button"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 hover:shadow-lg hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-md"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity rounded-md shadow-md"
onClick={openCreateModal}
>
<svg
className="w-4 h-4"
className="w-4 h-4 text-[#1a1a1a] dark:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -300,7 +298,6 @@ export const TabloPage = () => {
</svg>
<span>Nouveau tablo</span>
</button>
<SignOutButton />
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
@ -645,12 +642,12 @@ export const TabloPage = () => {
<button
id="create-tablo-button"
type="button"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 hover:shadow-lg hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-md"
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity rounded-md shadow-md disabled:opacity-50 disabled:cursor-not-allowed"
onClick={openCreateModal}
disabled={createTabloMutation.isPending}
>
<svg
className="w-4 h-4"
className="w-4 h-4 text-[#1a1a1a] dark:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -667,7 +664,6 @@ export const TabloPage = () => {
{createTabloMutation.isPending ? "Création..." : "Nouveau tablo"}
</span>
</button>
<SignOutButton />
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
@ -693,11 +689,11 @@ export const TabloPage = () => {
<div className="flex justify-center">
<button
type="button"
className="flex items-center gap-1.5 px-4 py-2 text-sm text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"
className="flex items-center gap-1.5 px-4 py-2 text-sm bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity rounded-md shadow-md"
onClick={openCreateModal}
>
<svg
className="w-4 h-4"
className="w-4 h-4 text-[#1a1a1a] dark:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"

View file

@ -6,7 +6,7 @@ import { useSession } from "@ui/contexts/SessionContext";
import { api } from "@ui/lib/api";
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
type User = Tables<"profiles"> & {
export type User = Tables<"profiles"> & {
streamToken: string | null;
};

View file

@ -40,7 +40,7 @@
@layer base {
:root {
--background: oklch(1 0 0);
--background: #f4f4f4;
--foreground: oklch(0.141 0.005 285.823);
--accent: oklch(0.21 0.006 285.885);
--input: oklch(0.871 0.006 286.286);
@ -53,7 +53,7 @@
}
.dark {
--background: oklch(0.141 0.005 285.823);
--background: #1c1b1f;
--foreground: oklch(1 0 0);
--accent: oklch(1 0 0);
--input: oklch(0.37 0.013 285.805);