Merge pull request #4 from artslidd/develop
feat: Implement purple theme with animated backgrounds for login/signup pages
This commit is contained in:
commit
85dfe7bae8
24 changed files with 2188 additions and 507 deletions
|
|
@ -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
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
BIN
ui/public/logo_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -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
BIN
ui/src/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
282
ui/src/components/AnimatedBackground.tsx
Normal file
282
ui/src/components/AnimatedBackground.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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" /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
1148
ui/src/main.css
1148
ui/src/main.css
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'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'é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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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'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'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'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'inscrire
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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'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'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'accepte les{" "}
|
||||
<a href="#" className="text-blue-600 hover:text-blue-500">
|
||||
conditions d'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'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'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'accepte les{" "}
|
||||
<a
|
||||
href="#"
|
||||
className="text-black hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
conditions d'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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue