Improve ui

This commit is contained in:
Arthur Belleville 2025-10-15 22:57:57 +02:00
parent c8856a35a0
commit affa408f3a
No known key found for this signature in database
24 changed files with 1774 additions and 1744 deletions

View file

@ -79,6 +79,7 @@
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-stately/calendar": "^3.7.1",
"@supabase/supabase-js": "^2.49.3",
"@tailwindcss/vite": "^4.0.14",

View file

@ -51,6 +51,9 @@ importers:
'@radix-ui/react-switch':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-tooltip':
specifier: ^1.2.8
version: 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@react-stately/calendar':
specifier: ^3.7.1
version: 3.7.1(react@19.0.0)
@ -1552,6 +1555,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-tooltip@1.2.8':
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
@ -7238,6 +7254,26 @@ snapshots:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4(@types/react@19.0.10)
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4(@types/react@19.0.10)
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.0.10)(react@19.0.0)':
dependencies:
react: 19.0.0

View file

@ -1,4 +1,5 @@
import { Button } from "@ui/components/ui/button";
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@ui/components/ui/card";
import {
Select,
SelectContent,
@ -140,138 +141,140 @@ export function AvailabilityCard({
};
return (
<div className="flex flex-col gap-2 w-full">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">{dayDisplay}</h3>
<Card className="w-full bg-muted/30">
<CardHeader>
<CardTitle className="text-lg">{dayDisplay}</CardTitle>
{onCopyToOtherDays && enabled && timeRanges.length > 0 && (
<Button
size="sm"
variant="outline"
onClick={() => onCopyToOtherDays(day, enabled, timeRanges)}
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
</Button>
<CardAction>
<Button
size="sm"
variant="outline"
onClick={() => onCopyToOtherDays(day, enabled, timeRanges)}
className="h-6 px-2 text-xs"
>
<CopyIcon className="size-3 mr-1" />
Copier
</Button>
</CardAction>
)}
</div>
<div className="flex items-center gap-2">
<Switch checked={enabled} onCheckedChange={onEnabledChange} />
<label
className={`font-medium text-sm ${
enabled ? "text-gray-900 dark:text-gray-100" : "text-gray-500 dark:text-gray-400"
}`}
>
{enabled ? "Disponible" : "Indisponible"}
</label>
</div>
{/* Time Ranges */}
<div className="flex gap-1 flex-wrap items-center">
{timeRanges.map((range, index) => (
<div
key={index}
onClick={() => setSelectedRangeIndex(index)}
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-800/60 hover:bg-gray-100 dark:hover:bg-gray-700/60"
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center gap-2">
<Switch checked={enabled} onCheckedChange={onEnabledChange} />
<label
className={`font-medium text-sm ${
enabled ? "text-foreground" : "text-muted-foreground"
}`}
>
<div className="flex items-center text-xs w-fit">
{selectedRangeIndex === index ? (
<>
<Select
value={range.start}
disabled={!enabled}
onValueChange={(value) => {
const newRanges = [...timeRanges];
newRanges[index] = {
...range,
start: value,
};
if (validateTimeRange(newRanges, index)) {
onTimeRangesChange(newRanges);
}
}}
>
<SelectTrigger
aria-label="Heure de début"
className="min-h-0 h-6 px-1 py-0 text-xs bg-transparent hover:bg-white/50 dark:hover:bg-gray-700/50 focus:bg-white/50 dark:focus:bg-gray-700/50 shadow-none outline-none ring-0 focus:ring-0 focus:outline-none min-w-[3rem] w-auto rounded-sm border-0"
{enabled ? "Disponible" : "Indisponible"}
</label>
</div>
{/* Time Ranges */}
<div className="flex gap-1 flex-wrap items-center">
{timeRanges.map((range, index) => (
<div
key={index}
onClick={() => setSelectedRangeIndex(index)}
className={`flex items-center gap-1 rounded-md px-1.5 py-1 cursor-pointer transition-all duration-200 ${
selectedRangeIndex === index ? "bg-primary/10" : "bg-muted/80 hover:bg-muted"
}`}
>
<div className="flex items-center text-xs w-fit">
{selectedRangeIndex === index ? (
<>
<Select
value={range.start}
disabled={!enabled}
onValueChange={(value) => {
const newRanges = [...timeRanges];
newRanges[index] = {
...range,
start: value,
};
if (validateTimeRange(newRanges, index)) {
onTimeRangesChange(newRanges);
}
}}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{timeOptions.map((item) => (
<SelectItem key={item.id} value={item.value} className="text-xs py-0.5">
{item.value}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-gray-500 dark:text-gray-400 text-[10px] mx-2">-</span>
<Select
value={range.end}
disabled={!enabled}
onValueChange={(value) => {
const newRanges = [...timeRanges];
newRanges[index] = {
...range,
end: value,
};
if (validateTimeRange(newRanges, index)) {
onTimeRangesChange(newRanges);
}
}}
>
<SelectTrigger
aria-label="Heure de fin"
className="min-h-0 h-6 px-1 py-0 text-xs bg-transparent hover:bg-white/50 dark:hover:bg-gray-700/50 focus:bg-white/50 dark:focus:bg-gray-700/50 shadow-none outline-none ring-0 focus:ring-0 focus:outline-none min-w-[3rem] w-auto rounded-sm border-0"
<SelectTrigger
aria-label="Heure de début"
className="min-h-0 h-6 px-1 py-0 text-xs bg-transparent hover:bg-accent focus:bg-accent shadow-none outline-none ring-0 focus:ring-0 focus:outline-none min-w-[3rem] w-auto rounded-sm border-0"
>
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{timeOptions.map((item) => (
<SelectItem key={item.id} value={item.value} className="text-xs py-0.5">
{item.value}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-muted-foreground text-[10px] mx-2">-</span>
<Select
value={range.end}
disabled={!enabled}
onValueChange={(value) => {
const newRanges = [...timeRanges];
newRanges[index] = {
...range,
end: value,
};
if (validateTimeRange(newRanges, index)) {
onTimeRangesChange(newRanges);
}
}}
>
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{timeOptions.map((item) => (
<SelectItem key={item.id} value={item.value} className="text-xs py-0.5">
{item.value}
</SelectItem>
))}
</SelectContent>
</Select>
</>
) : (
<>
<span className="font-medium text-xs px-1">{range.start}</span>
<span className="text-gray-500 dark:text-gray-400 text-[10px]"></span>
<span className="font-medium text-xs px-1">{range.end}</span>
</>
<SelectTrigger
aria-label="Heure de fin"
className="min-h-0 h-6 px-1 py-0 text-xs bg-transparent hover:bg-accent focus:bg-accent shadow-none outline-none ring-0 focus:ring-0 focus:outline-none min-w-[3rem] w-auto rounded-sm border-0"
>
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{timeOptions.map((item) => (
<SelectItem key={item.id} value={item.value} className="text-xs py-0.5">
{item.value}
</SelectItem>
))}
</SelectContent>
</Select>
</>
) : (
<>
<span className="font-medium text-xs px-1">{range.start}</span>
<span className="text-muted-foreground text-[10px]"></span>
<span className="font-medium text-xs px-1">{range.end}</span>
</>
)}
</div>
{timeRanges.length > 1 && (
<Button
onClick={() => handleDeleteRange(index)}
disabled={!enabled}
variant="destructive"
size="sm"
className="h-4 w-4 p-0 border-0"
>
<MinusIcon className="size-2" />
</Button>
)}
</div>
{timeRanges.length > 1 && (
<Button
onClick={() => handleDeleteRange(index)}
disabled={!enabled}
variant="outline"
size="sm"
className="h-4 w-4 p-0 border-0 bg-transparent hover:bg-rose-100 dark:hover:bg-rose-950/30 text-rose-500 hover:text-rose-600 dark:text-rose-400 dark:hover:text-rose-300"
>
<MinusIcon className="size-2" />
</Button>
)}
</div>
))}
{timeRanges.length < 3 && (
<Button
onClick={() => handleAddRange()}
disabled={!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-700/50 hover:bg-gray-200/50 dark:hover:bg-gray-600/50"
>
<PlusIcon className="size-2.5" />
</Button>
)}
</div>
</div>
))}
{timeRanges.length < 3 && (
<Button
onClick={() => handleAddRange()}
disabled={!enabled}
variant="outline"
size="sm"
className="h-5 px-1.5 flex items-center text-xs border-0 bg-muted/50 hover:bg-muted"
>
<PlusIcon className="size-2.5" />
</Button>
)}
</div>
</CardContent>
</Card>
);
}

View file

@ -0,0 +1,131 @@
import { EventType, EventTypeConfig, useEventTypes } from "@ui/hooks/event-types";
import { Button } from "@ui/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@ui/components/ui/card";
import { CopyButton } from "@ui/components/ui/clipboard";
import { Text } from "@ui/components/ui/typography";
import { ExternalLinkIcon, EditIcon, TrashIcon, CheckIcon, XIcon } from "lucide-react";
import { useUser } from "src/providers/UserStoreProvider";
export function EventTypeCard({
eventType,
handleEditEventType,
}: {
eventType: EventType;
handleEditEventType: (id: string, eventType: EventTypeConfig) => void;
}) {
const { toggleEventType, deleteEventType } = useEventTypes();
const user = useUser();
const getPublicLink = (standardName: string | null) => {
// Sanitize user name for URL (replace spaces with hyphens, lowercase, remove special chars)
const sanitizedUserName = user.name
?.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
const shortUserId = user.id.substring(0, 6);
// Construct the public booking URL
const baseUrl = window.location.origin;
const publicUrl = `${baseUrl}/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
return publicUrl;
};
return (
<Card key={eventType.id} className={eventType.isActive ? "opacity-100" : "opacity-60"}>
<CardHeader>
<CardTitle className="text-lg">{eventType.name}</CardTitle>
<CardAction>
<div className="flex gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => window.open(getPublicLink(eventType.standardName ?? null), "_blank")}
aria-label="Aperçu"
>
<ExternalLinkIcon className="w-4 h-4" />
</Button>
<CopyButton
copyValue={getPublicLink(eventType.standardName ?? null)}
label="Copier le lien"
labelAfterCopied="Lien copié"
></CopyButton>
<Button
variant="ghost"
size="icon"
onClick={() => handleEditEventType(eventType.id, eventType as EventTypeConfig)}
>
<EditIcon className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => deleteEventType({ id: eventType.id })}
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
</CardAction>
</CardHeader>
<CardContent className="min-h-[200px]">
<Text className="text-muted-foreground">{eventType.description}</Text>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Durée:</span>
<span className="font-medium">{eventType.duration} min</span>
</div>
{eventType.bufferTime && (
<div className="flex justify-between">
<span className="text-muted-foreground">Temps de battement:</span>
<span className="font-medium">{eventType.bufferTime} min</span>
</div>
)}
{eventType.maxBookingsPerDay && (
<div className="flex justify-between">
<span className="text-muted-foreground">Max par jour:</span>
<span className="font-medium">{eventType.maxBookingsPerDay}</span>
</div>
)}
{eventType.minAdvanceBooking && (
<div className="flex justify-between">
<span className="text-muted-foreground">Réservation à l&apos;avance:</span>
<span className="font-medium">
{eventType.minAdvanceBooking.value}{" "}
{eventType.minAdvanceBooking.unit === "minutes"
? "min"
: eventType.minAdvanceBooking.unit === "hours"
? "h"
: "j"}
</span>
</div>
)}
</div>
</CardContent>
<CardFooter className="justify-between border-t">
<span className="text-muted-foreground">Statut:</span>
<Button
variant={eventType.isActive ? "default" : "outline"}
size="sm"
onClick={() =>
toggleEventType({
id: eventType.id,
isActive: !eventType.isActive,
})
}
className="text-sm"
>
{eventType.isActive ? <CheckIcon /> : <XIcon />}
{eventType.isActive ? "Actif" : "Inactif"}
</Button>
</CardFooter>
</Card>
);
}

View file

@ -1,5 +1,12 @@
import { EventTypeConfig } from "@ui/hooks/event-types";
import { Button } from "@ui/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@ui/components/ui/dialog";
import {
Select,
SelectContent,
@ -9,7 +16,6 @@ import {
} from "@ui/components/ui/select";
import { Description, Input, Label, TextArea, TextField } from "@ui/ui-library/field";
import { NumberField, NumberInput } from "@ui/ui-library/number-field";
import { CustomModal } from "./CustomModal";
export function EventTypeModal({
isModalOpen,
@ -27,132 +33,136 @@ export function EventTypeModal({
handleSaveEventType: () => void;
}) {
return (
<CustomModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title={editingEventType ? "Modifier le type d'événement" : "Nouveau type d'événement"}
width="xl"
>
<div className="space-y-6">
{/* Basic Information Section */}
<div className="space-y-2">
<TextField
value={formData.name || ""}
onChange={(value) => setFormData({ ...formData, name: value })}
isRequired
>
<Label requiredHint>Nom du type d&apos;événement</Label>
<Input type="text" />
</TextField>
<TextField>
<Label>Description</Label>
<TextArea
value={formData.description || ""}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={2}
required
placeholder="Décrivez ce type d'événement et son objectif..."
/>
</TextField>
</div>
{/* Timing Configuration Section */}
<div className="space-y-2">
<h4 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Configuration des horaires
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<NumberField
value={formData.duration || 60}
onChange={(value) => setFormData({ ...formData, duration: value })}
minValue={15}
maxValue={480}
step={15}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editingEventType ? "Modifier le type d'événement" : "Nouveau type d'événement"}
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* Basic Information Section */}
<div className="space-y-2">
<TextField
value={formData.name || ""}
onChange={(value) => setFormData({ ...formData, name: value })}
isRequired
>
<Label requiredHint>Durée (minutes)</Label>
<NumberInput />
</NumberField>
<Label requiredHint>Nom du type d&apos;événement</Label>
<Input type="text" />
</TextField>
<NumberField
value={formData.bufferTime || 0}
onChange={(value) => setFormData({ ...formData, bufferTime: value })}
minValue={0}
maxValue={60}
step={5}
>
<Label>Temps de battement (minutes)</Label>
<NumberInput />
<Description>Temps de battement avant et après l&apos;événement</Description>
</NumberField>
<TextField>
<Label>Description</Label>
<TextArea
value={formData.description || ""}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={2}
required
placeholder="Décrivez ce type d'événement et son objectif..."
/>
</TextField>
</div>
</div>
{/* Booking Limits Section */}
<div className="space-y-2">
<h4 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Limites de réservation
</h4>
{/* Timing Configuration Section */}
<div className="space-y-2">
<h4 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Configuration des horaires
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<NumberField
value={formData.maxBookingsPerDay || 8}
onChange={(value) => setFormData({ ...formData, maxBookingsPerDay: value })}
minValue={1}
maxValue={50}
>
<Label>Maximum par jour</Label>
<NumberInput />
</NumberField>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<NumberField
value={formData.duration || 60}
onChange={(value) => setFormData({ ...formData, duration: value })}
minValue={15}
maxValue={480}
step={15}
>
<Label requiredHint>Durée (minutes)</Label>
<NumberInput />
</NumberField>
<div className="flex flex-col gap-2">
<Label>Réservation à l&apos;avance (heures)</Label>
<div className="flex flex-row gap-2">
<NumberField
value={formData.minAdvanceBooking?.value || 0}
onChange={(value) =>
setFormData({
...formData,
minAdvanceBooking: {
value,
unit: formData.minAdvanceBooking?.unit || "minutes",
},
})
}
minValue={0}
maxValue={168}
>
<NumberInput />
</NumberField>
<Select
value={String(formData.minAdvanceBooking?.unit || "minutes")}
onValueChange={(value) => {
setFormData({
...formData,
minAdvanceBooking: {
value: formData.minAdvanceBooking?.value || 0,
unit: value as "minutes" | "hours" | "days",
},
});
}}
>
<SelectTrigger className="min-w-[110px]" aria-label="Délai minimum pour réserver">
<SelectValue placeholder="..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="minutes">minutes</SelectItem>
<SelectItem value="hours">heures</SelectItem>
<SelectItem value="days">jours</SelectItem>
</SelectContent>
</Select>
</div>
<Description>Délai minimum pour réserver</Description>
<NumberField
value={formData.bufferTime || 0}
onChange={(value) => setFormData({ ...formData, bufferTime: value })}
minValue={0}
maxValue={60}
step={5}
>
<Label>Temps de battement (minutes)</Label>
<NumberInput />
<Description>Temps de battement avant et après l&apos;événement</Description>
</NumberField>
</div>
</div>
</div>
{/* Pricing Section
{/* Booking Limits Section */}
<div className="space-y-2">
<h4 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Limites de réservation
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<NumberField
value={formData.maxBookingsPerDay || 8}
onChange={(value) => setFormData({ ...formData, maxBookingsPerDay: value })}
minValue={1}
maxValue={50}
>
<Label>Maximum par jour</Label>
<NumberInput />
</NumberField>
<div className="flex flex-col gap-2">
<Label>Réservation à l&apos;avance (heures)</Label>
<div className="flex flex-row gap-2">
<NumberField
value={formData.minAdvanceBooking?.value || 0}
onChange={(value) =>
setFormData({
...formData,
minAdvanceBooking: {
value,
unit: formData.minAdvanceBooking?.unit || "minutes",
},
})
}
minValue={0}
maxValue={168}
>
<NumberInput />
</NumberField>
<Select
value={String(formData.minAdvanceBooking?.unit || "minutes")}
onValueChange={(value) => {
setFormData({
...formData,
minAdvanceBooking: {
value: formData.minAdvanceBooking?.value || 0,
unit: value as "minutes" | "hours" | "days",
},
});
}}
>
<SelectTrigger
className="min-w-[110px]"
aria-label="Délai minimum pour réserver"
>
<SelectValue placeholder="..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="minutes">minutes</SelectItem>
<SelectItem value="hours">heures</SelectItem>
<SelectItem value="days">jours</SelectItem>
</SelectContent>
</Select>
</div>
<Description>Délai minimum pour réserver</Description>
</div>
</div>
</div>
{/* Pricing Section
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Tarification (optionnel)
@ -176,8 +186,8 @@ export function EventTypeModal({
</NumberField>
</div> */}
{/* Settings Section */}
{/* <div className="space-y-4">
{/* Settings Section */}
{/* <div className="space-y-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Paramètres
</h3>
@ -216,9 +226,10 @@ export function EventTypeModal({
</div>
</div>
</div> */}
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<DialogFooter>
<Button variant="outline" onClick={() => setIsModalOpen(false)}>
Annuler
</Button>
@ -230,8 +241,8 @@ export function EventTypeModal({
>
{editingEventType ? "Modifier" : "Créer"}
</Button>
</div>
</div>
</CustomModal>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@ui/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -0,0 +1,86 @@
import React from "react";
import { cn } from "@ui/lib/utils";
import { Button, ButtonProps } from "./button";
import { useCopyToClipboard } from "@ui/ui-library/hooks/use-clipboard";
import { Check, Copy } from "lucide-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
export type ClipboardProps = {
timeout?: number;
children: (payload: { copied: boolean; copy: (value: string) => void }) => React.ReactNode;
};
export function Clipboard({ timeout, children }: ClipboardProps) {
const { copied, copy } = useCopyToClipboard({ timeout });
return children({ copied, copy }) as React.ReactElement;
}
export function CopyButton({
copyValue,
label = "Copy",
labelAfterCopied = "Copied to clipboard",
icon,
variant = "ghost",
children,
className,
...props
}: {
copyValue: string;
label?: string;
labelAfterCopied?: string;
icon?: React.ReactElement<{ className?: string }>;
} & Omit<ButtonProps, "onClick">) {
return (
<Clipboard>
{({ copied, copy }) => {
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={variant}
size={!children ? "icon" : undefined}
aria-label={label}
className={className}
{...props}
onClick={() => copy(copyValue)}
>
{children ?? (
<div className="relative flex items-center justify-center">
{icon ? (
React.cloneElement(icon, {
className: cn(
icon.props.className,
"transition-all w-4 h-4",
copied ? "absolute scale-0 opacity-0" : "scale-100 opacity-100"
),
})
) : (
<Copy
className={cn(
"transition-all w-4 h-4",
copied ? "absolute scale-0 opacity-0" : "scale-100 opacity-100"
)}
/>
)}
<Check
className={cn(
"text-green-600 dark:text-green-400 transition-all w-4 h-4",
copied ? "scale-100 opacity-100" : "absolute scale-0 opacity-0"
)}
/>
</div>
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{copied ? labelAfterCopied : label}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}}
</Clipboard>
);
}

View file

@ -20,10 +20,7 @@ const DialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-foreground/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
className={cn("fixed inset-0 z-50 bg-foreground/80", className)}
{...props}
/>
));
@ -38,7 +35,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg",
className
)}
{...props}

View file

@ -0,0 +1,242 @@
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@ui/lib/utils"
import { Label } from "@ui/components/ui/label"
import { Separator } from "@ui/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
if (errors?.length == 1) {
return errors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{errors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}

View file

@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@ui/lib/utils";
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = "Textarea";
export { Textarea };

View file

@ -0,0 +1,30 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@ui/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View file

@ -9,7 +9,7 @@ export function TypographyH1({
return (
<h1
className={cn(
"scroll-m-20 text-center text-4xl font-extrabold tracking-tight text-balance",
"scroll-m-20 text-center text-4xl font-extrabold tracking-tight text-balance text-foreground",
className
)}
{...props}
@ -27,7 +27,7 @@ export function TypographyH2({
return (
<h2
className={cn(
"scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0",
"scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0 text-foreground",
className
)}
{...props}
@ -43,7 +43,10 @@ export function TypographyH3({
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h3 className={cn("scroll-m-20 text-2xl font-semibold tracking-tight", className)} {...props}>
<h3
className={cn("scroll-m-20 text-2xl font-semibold tracking-tight text-foreground", className)}
{...props}
>
{children}
</h3>
);
@ -55,7 +58,10 @@ export function TypographyH4({
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h4 className={cn("scroll-m-20 text-xl font-semibold tracking-tight", className)} {...props}>
<h4
className={cn("scroll-m-20 text-xl font-semibold tracking-tight text-foreground", className)}
{...props}
>
{children}
</h4>
);
@ -67,7 +73,7 @@ export function TypographyP({
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p className={cn("leading-7 [&:not(:first-child)]:mt-6", className)} {...props}>
<p className={cn("leading-7 [&:not(:first-child)]:mt-6 text-foreground", className)} {...props}>
{children}
</p>
);
@ -79,7 +85,7 @@ export function TypographyLarge({
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn("text-lg font-semibold", className)} {...props}>
<div className={cn("text-lg font-semibold text-foreground", className)} {...props}>
{children}
</div>
);
@ -91,7 +97,7 @@ export function TypographySmall({
...props
}: React.HTMLAttributes<HTMLElement>) {
return (
<small className={cn("text-sm leading-none font-medium", className)} {...props}>
<small className={cn("text-sm leading-none font-medium text-foreground", className)} {...props}>
{children}
</small>
);

View file

@ -25,6 +25,12 @@ export type EventTypeConfig = {
}; // minimum hours in advance
};
export type EventType = EventTypeConfig & {
id: string;
standardName?: string;
isActive: boolean;
};
export type EventTypePayload = {
name: string;
description: string;
@ -89,7 +95,13 @@ export function useEventTypes() {
eventType: EventTypePayload;
}
>({
mutationFn: async ({ id, eventType }: { id: string; eventType: EventTypePayload }) => {
mutationFn: async ({
id,
eventType,
}: {
id: string;
eventType: EventTypePayload;
}) => {
const { error } = await supabase
.from("event_types")
.update({
@ -160,7 +172,7 @@ export function useEventTypes() {
return {
isLoading,
eventTypes,
eventTypes: eventTypes as EventType[],
addEventType,
updateEventType,
toggleEventType,

View file

@ -17,7 +17,6 @@ import { PublicBookingPage } from "@ui/pages/PublicBookingPage";
import { PlanningPage } from "@ui/pages/planning";
import { ResetPasswordPage } from "@ui/pages/reset-password";
import { SignUpPage } from "@ui/pages/signup";
import { SupportPage } from "@ui/pages/support";
import { TabloPage } from "@ui/pages/tablo";
import ChatProvider from "@ui/providers/ChatProvider";
import { RouteObject } from "react-router-dom";
@ -87,10 +86,6 @@ export const routes: RouteObject[] = [
path: "feedback",
element: <FeedbackPage />,
},
{
path: "support",
element: <SupportPage />,
},
],
},
],

View file

@ -1,7 +1,6 @@
import { getLocalTimeZone, today } from "@internationalized/date";
import { AvailabilityCard } from "@ui/components/AvailabilityCard";
import { AvailabilityVisualization } from "@ui/components/AvailabilityVisualization";
import { CustomModal } from "@ui/components/CustomModal";
import {
DEFAULT_AVAILABILITIES,
Exception,
@ -12,12 +11,28 @@ import { Button } from "@ui/components/ui/button";
import { ButtonGroup } from "@ui/components/ui/button-group";
import { Checkbox } from "@ui/ui-library/checkbox";
import { DatePicker } from "@ui/components/ui/date-picker";
import { Label } from "@ui/ui-library/field";
import { Plus as PlusIcon } from "lucide-react";
import { Strong, Text } from "@ui/components/ui/typography";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@ui/components/ui/dialog";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@ui/components/ui/empty";
import { Label } from "@ui/components/ui/label";
import { Plus as PlusIcon, SaveIcon } from "lucide-react";
import { Strong, Text, TypographyH3, TypographyMuted } from "@ui/components/ui/typography";
import { toast } from "@ui/lib/toast";
import { SaveIcon } from "lucide-react";
import { useState } from "react";
import { Card } from "@ui/components/ui/card";
import { CardContent } from "src/components/ui/card";
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
const DAYS_OF_WEEK_DISPLAY = [
@ -89,340 +104,336 @@ export function AvailabilitiesPage() {
};
return (
<div className="h-full flex flex-col p-4">
<div className="flex justify-between items-start mb-3">
<div>
<h2 className="text-2xl font-bold">Disponibilités</h2>
<Strong className="text-gray-500 mt-2 text-xl">
{activeTab === "availabilities"
? "Définissez vos horaires de disponibilité pour chaque jour de la semaine"
: activeTab === "visualisation"
? "Visualisez votre planning hebdomadaire"
: "Gérez vos exceptions de disponibilité"}
</Strong>
</div>
{activeTab === "availabilities" && (
<div className="flex gap-2">
<Button
size="lg"
variant="default"
className="[--btn-bg:var(--color-green-800)]"
onClick={() => {
updateAvailabilities(
{
updatedAvailabilities: draftAvailabilities,
newException: null,
},
{
onSuccess: () => {
toast.add({
title: "Succès",
description: "Disponibilités enregistrées avec succès",
type: "success",
});
},
onError: (err) => {
console.error(err);
toast.add({
title: "Erreur",
description: "Erreur lors de l'enregistrement des disponibilités",
type: "error",
});
},
}
);
}}
>
<SaveIcon /> Enregistrer
</Button>
<Button
variant="default"
size="lg"
onClick={() => setExceptionModalOpen(true)}
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
>
<PlusIcon className="text-[#1a1a1a] dark:text-white" /> Ajouter une exception
</Button>
<Button
size="lg"
onClick={() => {
updateAvailabilities({
updatedAvailabilities: DEFAULT_AVAILABILITIES,
});
}}
className="py-1"
>
Horaires de bureau (9h-17h)
</Button>
<Button
size="lg"
variant="outline"
onClick={() => {
const newAvailabilities: WeeklyAvailability = {};
DAYS_OF_WEEK.forEach((day) => {
newAvailabilities[day] = {
enabled: false,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
});
updateAvailabilities({
updatedAvailabilities: newAvailabilities,
});
}}
className="py-1"
>
Tout désactiver
</Button>
</div>
)}
</div>
{/* Tab Navigation */}
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-6">
<button
onClick={() => setActiveTab("availabilities")}
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
activeTab === "availabilities"
? "border-primary text-primary"
: "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
}`}
>
Disponibilités
</button>
<button
onClick={() => setActiveTab("exceptions")}
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
activeTab === "exceptions"
? "border-primary text-primary"
: "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
}`}
>
Exceptions
</button>
<button
onClick={() => setActiveTab("visualisation")}
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
activeTab === "visualisation"
? "border-primary text-primary"
: "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
}`}
>
Visualisation
</button>
</div>
<div className="flex-1 overflow-auto">
{activeTab === "availabilities" && (
<div className="flex items-start">
<div className="flex-1 pr-6 border-r border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-2 gap-4">
{DAYS_OF_WEEK.map((day) => (
<div
key={day}
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
day={day}
enabled={draftAvailabilities[day].enabled}
onEnabledChange={(enabled) => {
setDraftAvailabilities({
...draftAvailabilities,
[day]: {
...draftAvailabilities[day],
enabled,
},
<div className="min-h-screen">
<header className="bg-card shadow-sm border-b border-border">
<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>
<TypographyH3>Disponibilités</TypographyH3>
<TypographyMuted>
{activeTab === "availabilities"
? "Définissez vos horaires de disponibilité pour chaque jour de la semaine"
: activeTab === "visualisation"
? "Visualisez votre planning hebdomadaire"
: "Gérez vos exceptions de disponibilité"}
</TypographyMuted>
</div>
{activeTab === "availabilities" && (
<div className="flex gap-2">
<Button
size="lg"
variant="default"
className="[--btn-bg:var(--color-green-800)]"
onClick={() => {
updateAvailabilities(
{
updatedAvailabilities: draftAvailabilities,
newException: null,
},
{
onSuccess: () => {
toast.add({
title: "Succès",
description: "Disponibilités enregistrées avec succès",
type: "success",
});
}}
timeRanges={draftAvailabilities[day].timeRanges}
onTimeRangesChange={(ranges) => {
setDraftAvailabilities({
...draftAvailabilities,
[day]: {
...draftAvailabilities[day],
timeRanges: ranges,
},
},
onError: (err) => {
console.error(err);
toast.add({
title: "Erreur",
description: "Erreur lors de l'enregistrement des disponibilités",
type: "error",
});
}}
onCopyToOtherDays={handleCopyToOtherDays}
/>
</div>
</div>
))}
</div>
</div>
<div className="w-80 pl-6">
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold mb-2">Fuseau horaire</h3>
<Text className="text-gray-500">
Vos disponibilités sont affichées dans votre fuseau horaire local.
</Text>
</div>
<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}
</Text>
<Text className="text-sm text-gray-400 mt-2">
{new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}{" "}
- Heure locale
</Text>
</div>
<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 le fuseau horaire de
vos clients lorsqu&apos;ils consulteront vos disponibilités.
</Text>
</div>
</div>
</div>
</div>
)}
{activeTab === "visualisation" && (
<AvailabilityVisualization
draftAvailabilities={draftAvailabilities}
slotDurationMinutes={60}
/>
)}
{activeTab === "exceptions" && (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h3 className="text-xl font-semibold">Mes exceptions</h3>
<Text className="text-gray-500 mt-1">
Gérez vos exceptions de disponibilité pour des dates spécifiques
</Text>
</div>
<Button
variant="default"
size="lg"
onClick={() => setExceptionModalOpen(true)}
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
>
<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-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>
<Text className="text-gray-400 dark:text-gray-500 text-sm">
Les exceptions vous permettent de modifier vos disponibilités pour des dates
spécifiques.
</Text>
</div>
</div>
) : (
<div className="grid gap-4">
{exceptions.map((exception, index) => (
<div
key={`${exception.date}-${index}`}
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">
<div className="flex items-center gap-3 mb-2">
<Strong className="text-lg">
{new Date(exception.date).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</Strong>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
exception.type === "day"
? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
}`}
>
{exception.type === "day" ? "Indisponible" : "Horaires personnalisés"}
</span>
</div>
{exception.type === "hours" && "hours" in exception && (
<div className="space-y-1">
<Text className="text-sm text-gray-600 dark:text-gray-400 font-medium">
Créneaux disponibles :
</Text>
<div className="flex flex-wrap gap-2">
{exception.hours.map((timeRange, timeIndex) => (
<span
key={timeIndex}
className="px-2 py-1 bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 rounded text-sm"
>
{timeRange.start} - {timeRange.end}
</span>
))}
</div>
</div>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
deleteException(
{ exceptionIndex: index },
{
onSuccess: () => {
toast.add({
title: "Succès",
description: "Exception supprimée avec succès",
type: "success",
});
},
onError: (err) => {
console.error(err);
toast.add({
title: "Erreur",
description: "Erreur lors de la suppression de l'exception",
type: "error",
});
},
}
);
}}
className="text-red-600 hover:text-red-700 border-red-300 hover:border-red-400"
>
Supprimer
</Button>
</div>
</div>
))}
},
}
);
}}
>
<SaveIcon /> Enregistrer
</Button>
<Button variant="default" size="lg" onClick={() => setExceptionModalOpen(true)}>
<PlusIcon /> Ajouter une exception
</Button>
<Button
size="lg"
onClick={() => {
updateAvailabilities({
updatedAvailabilities: DEFAULT_AVAILABILITIES,
});
}}
className="py-1"
>
Horaires de bureau (9h-17h)
</Button>
<Button
size="lg"
variant="outline"
onClick={() => {
const newAvailabilities: WeeklyAvailability = {};
DAYS_OF_WEEK.forEach((day) => {
newAvailabilities[day] = {
enabled: false,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
});
updateAvailabilities({
updatedAvailabilities: newAvailabilities,
});
}}
className="py-1"
>
Tout désactiver
</Button>
</div>
)}
</div>
)}
</div>
</div>
</header>
{/* Copy Modal */}
<CustomModal
isOpen={copyModalOpen}
onClose={() => setCopyModalOpen(false)}
title={`Copier les horaires de ${
sourceDayData ? DAYS_OF_WEEK_DISPLAY[sourceDayData.day] : ""
}`}
>
<div className="space-y-4">
<Text className="text-gray-600 dark:text-gray-400">
Sélectionnez les jours vers lesquels vous souhaitez copier ces horaires :
</Text>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* Tab Navigation */}
<div className="flex border-b border-border mb-6">
<button
onClick={() => setActiveTab("availabilities")}
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
activeTab === "availabilities"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
Disponibilités
</button>
<button
onClick={() => setActiveTab("exceptions")}
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
activeTab === "exceptions"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
Exceptions
</button>
<button
onClick={() => setActiveTab("visualisation")}
className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
activeTab === "visualisation"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
}`}
>
Visualisation
</button>
</div>
<div className="flex-1 overflow-auto">
{activeTab === "availabilities" && (
<div className="flex items-start">
<div className="flex-1 pr-6 border-r border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-2 gap-4">
{DAYS_OF_WEEK.map((day) => (
<AvailabilityCard
key={day}
day={day}
enabled={draftAvailabilities[day].enabled}
onEnabledChange={(enabled) => {
setDraftAvailabilities({
...draftAvailabilities,
[day]: {
...draftAvailabilities[day],
enabled,
},
});
}}
timeRanges={draftAvailabilities[day].timeRanges}
onTimeRangesChange={(ranges) => {
setDraftAvailabilities({
...draftAvailabilities,
[day]: {
...draftAvailabilities[day],
timeRanges: ranges,
},
});
}}
onCopyToOtherDays={handleCopyToOtherDays}
/>
))}
</div>
</div>
<div className="w-80 pl-6">
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold mb-2">Fuseau horaire</h3>
<Text className="text-gray-500">
Vos disponibilités sont affichées dans votre fuseau horaire local.
</Text>
</div>
<Card className="bg-muted/30">
<CardContent className="px-4">
<Strong className="block mb-2">Votre fuseau horaire</Strong>
<Text className="text-gray-500">
{Intl.DateTimeFormat().resolvedOptions().timeZone}
</Text>
<Text className="text-sm text-gray-400 mt-2">
{new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}{" "}
- Heure locale
</Text>
</CardContent>
</Card>
<Card className="bg-muted/30">
<CardContent className="px-4">
<Strong className="block mb-2">Information</Strong>
<Text className="text-gray-500 text-sm">
Les créneaux horaires seront automatiquement convertis dans le fuseau
horaire de vos clients lorsqu&apos;ils consulteront vos disponibilités.
</Text>
</CardContent>
</Card>
</div>
</div>
</div>
)}
{activeTab === "visualisation" && (
<AvailabilityVisualization
draftAvailabilities={draftAvailabilities}
slotDurationMinutes={60}
/>
)}
{activeTab === "exceptions" && (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h3 className="text-xl font-semibold">Mes exceptions</h3>
<Text className="text-muted-foreground mt-1">
Gérez vos exceptions de disponibilité pour des dates spécifiques
</Text>
</div>
<Button variant="default" size="lg" onClick={() => setExceptionModalOpen(true)}>
<PlusIcon /> Ajouter une exception
</Button>
</div>
{exceptions.length === 0 ? (
<Empty>
<EmptyHeader>
<EmptyTitle>Aucune exception définie</EmptyTitle>
<EmptyDescription>
Les exceptions vous permettent de modifier vos disponibilités pour des dates
spécifiques.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="default" size="lg" onClick={() => setExceptionModalOpen(true)}>
<PlusIcon /> Ajouter une exception
</Button>
</EmptyContent>
</Empty>
) : (
<div className="grid gap-4">
{exceptions.map((exception, index) => (
<div
key={`${exception.date}-${index}`}
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">
<div className="flex items-center gap-3 mb-2">
<Strong className="text-lg">
{new Date(exception.date).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</Strong>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
exception.type === "day"
? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
}`}
>
{exception.type === "day" ? "Indisponible" : "Horaires personnalisés"}
</span>
</div>
{exception.type === "hours" && "hours" in exception && (
<div className="space-y-1">
<Text className="text-sm text-gray-600 dark:text-gray-400 font-medium">
Créneaux disponibles :
</Text>
<div className="flex flex-wrap gap-2">
{exception.hours.map((timeRange, timeIndex) => (
<span
key={timeIndex}
className="px-2 py-1 bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 rounded text-sm"
>
{timeRange.start} - {timeRange.end}
</span>
))}
</div>
</div>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
deleteException(
{ exceptionIndex: index },
{
onSuccess: () => {
toast.add({
title: "Succès",
description: "Exception supprimée avec succès",
type: "success",
});
},
onError: (err) => {
console.error(err);
toast.add({
title: "Erreur",
description: "Erreur lors de la suppression de l'exception",
type: "error",
});
},
}
);
}}
className="text-destructive hover:text-destructive/90 border-destructive/50 hover:border-destructive"
>
Supprimer
</Button>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
</main>
{/* Copy Dialog */}
<Dialog open={copyModalOpen} onOpenChange={setCopyModalOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Copier les horaires de {sourceDayData ? DAYS_OF_WEEK_DISPLAY[sourceDayData.day] : ""}
</DialogTitle>
<DialogDescription>
Sélectionnez les jours vers lesquels vous souhaitez copier ces horaires
</DialogDescription>
</DialogHeader>
<div className="space-y-3 max-h-60 overflow-y-auto">
{DAYS_OF_WEEK.filter((day) => day !== sourceDayData?.day).map((day) => (
@ -442,7 +453,7 @@ export function AvailabilitiesPage() {
))}
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<DialogFooter>
<Button variant="outline" onClick={() => setCopyModalOpen(false)}>
Annuler
</Button>
@ -453,21 +464,20 @@ export function AvailabilitiesPage() {
>
Copier vers {selectedDays.length} jour(s)
</Button>
</div>
</div>
</CustomModal>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Exception Modal */}
<CustomModal
isOpen={exceptionModalOpen}
onClose={() => setExceptionModalOpen(false)}
title="Ajouter une exception"
>
<div className="space-y-4">
<Text className="text-gray-600 dark:text-gray-400">
Définissez une exception pour une date spécifique qui remplacera vos disponibilités
habituelles.
</Text>
{/* Exception Dialog */}
<Dialog open={exceptionModalOpen} onOpenChange={setExceptionModalOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Ajouter une exception</DialogTitle>
<DialogDescription>
Définissez une exception pour une date spécifique qui remplacera vos disponibilités
habituelles
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
@ -505,15 +515,13 @@ export function AvailabilitiesPage() {
{/* Custom Time Ranges (shown when custom is selected) */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Créneaux horaires (optionnel)
</label>
<Label>Créneaux horaires (optionnel)</Label>
<div className="space-y-2">
{(exceptionHours || [{ start: "09:00", end: "17:00" }]).map((timeRange, index) => (
<div key={index} className="flex items-center gap-2">
<input
type="time"
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-primary focus:border-primary dark:bg-gray-700 dark:text-white"
className="px-3 py-2 border border-input rounded-md shadow-sm focus:ring-ring focus:border-ring bg-background"
value={timeRange.start}
onChange={(e) => {
const updatedRanges = [
@ -526,10 +534,10 @@ export function AvailabilitiesPage() {
setExceptionHours(updatedRanges);
}}
/>
<Text className="text-gray-500">à</Text>
<Text className="text-muted-foreground">à</Text>
<input
type="time"
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-primary focus:border-primary dark:bg-gray-700 dark:text-white"
className="px-3 py-2 border border-input rounded-md shadow-sm focus:ring-ring focus:border-ring bg-background"
value={timeRange.end}
onChange={(e) => {
const updatedRanges = [
@ -566,14 +574,14 @@ export function AvailabilitiesPage() {
setExceptionHours([...currentRanges, { start: "09:00", end: "17:00" }]);
}}
>
<PlusIcon className="w-4 h-4 mr-1 text-[#1a1a1a] dark:text-white" />
<PlusIcon className="w-4 h-4 mr-1" />
Ajouter un créneau
</Button>
</div>
</div>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<DialogFooter>
<Button variant="outline" onClick={() => setExceptionModalOpen(false)}>
Annuler
</Button>
@ -612,9 +620,9 @@ export function AvailabilitiesPage() {
>
Ajouter l&apos;exception
</Button>
</div>
</div>
</CustomModal>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View file

@ -12,11 +12,10 @@ import {
SelectTrigger,
SelectValue,
} from "@ui/components/ui/select";
import { Input } from "@ui/ui-library/field";
import { Calendar as CalendarIcon } from "lucide-react";
import { Strong, Text } from "@ui/components/ui/typography";
import { Input } from "@ui/components/ui/input";
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, SearchIcon } from "lucide-react";
import { Strong, Text, TypographyMuted, TypographyH3 } from "@ui/components/ui/typography";
import { getTextColorFromTabloColor } from "@ui/utils/helpers";
import { ChevronLeft, ChevronRight, SearchIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { twMerge } from "tailwind-merge";
@ -149,19 +148,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/60 dark:text-blue-200">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/10 text-primary">
Aujourd&apos;hui
</span>
);
} else if (eventDate > today) {
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/60 dark:text-purple-200">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-secondary text-secondary-foreground">
À 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-800/60 dark:text-gray-200">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground">
Passé
</span>
);
@ -196,21 +195,16 @@ export const BookingsPage = () => {
return (
<div className="min-h-screen">
{/* Header */}
<header className="bg-white dark:bg-gray-700/40 shadow-sm dark:shadow-gray-900/20 dark:border-b dark:border-gray-600/30">
<header className="bg-card shadow-sm border-b border-border">
<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>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Réservations</h1>
<Text className="text-gray-600 dark:text-gray-400">
Gérez vos événements et réservations
</Text>
<TypographyH3>Réservations</TypographyH3>
<TypographyMuted>Gérez vos événements et réservations</TypographyMuted>
</div>
<div className="flex items-center space-x-3">
<Button
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
onClick={handleCreateEvent}
>
<CalendarIcon className="w-4 h-4 mr-2 text-[#1a1a1a] dark:text-white" />
<Button onClick={handleCreateEvent}>
<CalendarIcon className="w-4 h-4 mr-2" />
Nouvel événement
</Button>
</div>
@ -221,12 +215,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-700/40 rounded-lg shadow-sm dark:shadow-gray-900/20 dark:border dark:border-gray-600/30 p-6 mb-6">
<div className="bg-card rounded-lg shadow-sm border border-border 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 dark:text-gray-300 w-4 h-4" />
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
type="text"
placeholder="Rechercher un événement..."
@ -249,7 +243,10 @@ export const BookingsPage = () => {
<SelectItem key={tablo.id} value={tablo.id}>
<div className="flex items-center gap-2">
<div
className={twMerge("w-2 h-2 rounded-full", tablo.color || "bg-gray-400")}
className={twMerge(
"w-2 h-2 rounded-full",
tablo.color || "bg-muted-foreground"
)}
/>
{tablo.name}
</div>
@ -277,40 +274,38 @@ export const BookingsPage = () => {
</div>
{/* Events List */}
<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">
<div className="bg-card rounded-lg shadow-sm border border-border">
{tablosLoading || eventsLoading ? (
<div className="flex items-center justify-center h-screen">
<LoadingSpinner />
</div>
) : paginatedEvents.length === 0 ? (
<div className="p-12 text-center">
<CalendarIcon className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
Aucun événement trouvé
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
<CalendarIcon className="mx-auto h-12 w-12 text-muted-foreground" />
<h3 className="mt-2 text-sm font-medium text-foreground">Aucun événement trouvé</h3>
<p className="mt-1 text-sm text-muted-foreground">
{searchTerm || statusFilter !== "all"
? "Essayez de modifier vos filtres de recherche."
: "Commencez par créer votre premier événement."}
</p>
</div>
) : (
<div className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="divide-y divide-border">
{paginatedEvents.map((event) => (
<div
key={event.event_id}
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-600/40 transition-colors cursor-pointer"
className="p-6 hover:bg-muted transition-colors cursor-pointer"
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-3 mb-2">
<Strong className="text-lg text-gray-900 dark:text-gray-100 truncate">
<Strong className="text-lg text-foreground truncate">
{event.title || "Événement sans titre"}
</Strong>
{getEventStatusBadge(event)}
</div>
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400 mb-2">
<div className="flex items-center space-x-4 text-sm text-muted-foreground mb-2">
<span className="flex items-center">
<CalendarIcon className="w-4 h-4 mr-1" />
{formatEventDateTime(event)}
@ -329,7 +324,7 @@ export const BookingsPage = () => {
</div>
{event.description && (
<Text className="text-gray-600 dark:text-gray-300 line-clamp-2">
<Text className="text-muted-foreground line-clamp-2">
{event.description}
</Text>
)}
@ -349,9 +344,9 @@ export const BookingsPage = () => {
{/* Pagination Controls */}
{totalItems > 0 && (
<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="bg-card rounded-lg shadow-sm border border-border 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">
<div className="flex items-center space-x-4 text-sm text-muted-foreground">
<span>
Affichage de {startIndex + 1} à {Math.min(endIndex, totalItems)} sur {totalItems}{" "}
événements
@ -402,7 +397,9 @@ export const BookingsPage = () => {
return (
<div key={page} className="flex items-center">
{showEllipsis && <span className="px-2 text-gray-400">...</span>}
{showEllipsis && (
<span className="px-2 text-muted-foreground">...</span>
)}
<Button
variant={currentPage === page ? "default" : "outline"}
size="sm"
@ -437,16 +434,14 @@ export const BookingsPage = () => {
{/* Stats Summary */}
{filteredEvents.length > 0 && (
<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="mt-6 bg-card rounded-lg shadow-sm border border-border 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">
{filteredEvents.length}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">Événements trouvés</div>
<div className="text-2xl font-bold text-foreground">{filteredEvents.length}</div>
<div className="text-sm text-muted-foreground">Événements trouvés</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-emerald-600">
<div className="text-2xl font-bold text-foreground">
{
filteredEvents.filter((e) => {
if (!e.start_date) return false;
@ -455,10 +450,10 @@ export const BookingsPage = () => {
}).length
}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">À venir</div>
<div className="text-sm text-muted-foreground">À venir</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">
<div className="text-2xl font-bold text-primary">
{
filteredEvents.filter((e) => {
if (!e.start_date) return false;
@ -470,7 +465,7 @@ export const BookingsPage = () => {
}).length
}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">Aujourd&apos;hui</div>
<div className="text-sm text-muted-foreground">Aujourd&apos;hui</div>
</div>
</div>
</div>

View file

@ -1,15 +1,13 @@
import { EventTypeModal } from "@ui/components/EventTypeModal";
import { EventTypeConfig, useEventTypes } from "@ui/hooks/event-types";
import { useUser } from "@ui/providers/UserStoreProvider";
import { Button } from "@ui/components/ui/button";
import { CopyButton } from "@ui/ui-library/clipboard";
import { Strong, Text } from "@ui/components/ui/typography";
import { Text, TypographyH3 } from "@ui/components/ui/typography";
import { toast } from "@ui/lib/toast";
import { CheckIcon, EditIcon, ExternalLinkIcon, PlusIcon, TrashIcon, XIcon } from "lucide-react";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { EventTypeCard } from "src/components/EventTypeCard";
export function EventTypesPage() {
const user = useUser();
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingEventType, setEditingEventType] = useState<
(EventTypeConfig & { id: string }) | null
@ -22,13 +20,7 @@ export function EventTypesPage() {
maxBookingsPerDay: 8,
requiresApproval: false,
});
const {
eventTypes: eventTypesData,
addEventType,
updateEventType,
toggleEventType,
deleteEventType,
} = useEventTypes();
const { eventTypes: eventTypesData, addEventType, updateEventType } = useEventTypes();
const handleCreateEventType = () => {
setEditingEventType(null);
@ -73,159 +65,46 @@ export function EventTypesPage() {
setEditingEventType(null);
};
const getPublicLink = (standardName: string | null) => {
// Sanitize user name for URL (replace spaces with hyphens, lowercase, remove special chars)
const sanitizedUserName = user.name
?.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
const shortUserId = user.id.substring(0, 6);
// Construct the public booking URL
const baseUrl = window.location.origin;
const publicUrl = `${baseUrl}/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
return publicUrl;
};
return (
<div className="h-full flex flex-col p-4">
<div className="flex justify-between items-start mb-6">
<div>
<h2 className="text-2xl font-bold">Types d&apos;événements</h2>
<Strong className="text-gray-500 mt-2 text-xl">
Configurez les différents types d&apos;événements que vous proposez
</Strong>
<div className="min-h-screen">
<header className="bg-card shadow-sm border-b border-border">
<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>
<TypographyH3>Types d&apos;événements</TypographyH3>
<Text className="text-muted-foreground">
Configurez les différents types d&apos;événements que vous proposez
</Text>
</div>
<Button size="lg" variant="default" onClick={handleCreateEventType}>
<PlusIcon className="w-4 h-4 mr-2" /> Nouveau type
</Button>
</div>
</div>
<Button
size="lg"
variant="default"
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
onClick={handleCreateEventType}
>
<PlusIcon className="text-[#1a1a1a] dark:text-white" /> Nouveau type
</Button>
</div>
</header>
<div className="flex-1 overflow-auto">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{eventTypesData?.map((eventType) => (
<div
<EventTypeCard
key={eventType.id}
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"
}`}
>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3">
<h3 className="text-lg font-semibold">{eventType.name}</h3>
</div>
<div className="flex gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => window.open(getPublicLink(eventType.standardName), "_blank")}
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
aria-label="Aperçu"
>
<ExternalLinkIcon className="w-4 h-4" />
</Button>
<CopyButton
copyValue={getPublicLink(eventType.standardName)}
label="Copier le lien"
labelAfterCopied="Lien copié"
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
></CopyButton>
<Button
variant="ghost"
size="icon"
onClick={() => handleEditEventType(eventType.id, eventType as EventTypeConfig)}
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
>
<EditIcon className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => deleteEventType({ id: eventType.id })}
className="text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400"
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
</div>
<Text className="text-gray-600 dark:text-gray-400 mb-4">{eventType.description}</Text>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<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 dark:text-gray-400">Temps de battement:</span>
<span className="font-medium">{eventType.bufferTime} min</span>
</div>
)}
{eventType.maxBookingsPerDay && (
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">Max par jour:</span>
<span className="font-medium">{eventType.maxBookingsPerDay}</span>
</div>
)}
{eventType.minAdvanceBooking && (
<div className="flex justify-between">
<span className="text-gray-500 dark:text-gray-400">
Réservation à l&apos;avance:
</span>
<span className="font-medium">
{eventType.minAdvanceBooking.value}{" "}
{eventType.minAdvanceBooking.unit === "minutes"
? "min"
: eventType.minAdvanceBooking.unit === "hours"
? "h"
: "j"}
</span>
</div>
)}
<div className="flex justify-between items-center pt-2 border-t border-gray-200 dark:border-gray-700">
<span className="text-gray-500 dark:text-gray-400">Statut:</span>
<Button
variant={eventType.isActive ? "default" : "outline"}
size="sm"
onClick={() =>
toggleEventType({
id: eventType.id,
isActive: !eventType.isActive,
})
}
className="text-sm"
>
{eventType.isActive ? <CheckIcon /> : <XIcon />}
{eventType.isActive ? "Actif" : "Inactif"}
</Button>
</div>
</div>
</div>
eventType={eventType}
handleEditEventType={handleEditEventType}
/>
))}
</div>
{eventTypesData?.length === 0 && (
<div className="text-center py-12">
<Text className="text-gray-500 dark:text-gray-400 mb-4">
<Text className="text-muted-foreground mb-4">
Aucun type d&apos;événement configuré
</Text>
<Button
variant="default"
onClick={handleCreateEventType}
className="bg-[#dabdff] border-[#dabdff] text-[#1a1a1a] dark:bg-[#6911d9] dark:border-[#6911d9] dark:text-white hover:opacity-90 transition-opacity"
>
<PlusIcon className="text-[#1a1a1a] dark:text-white" /> Créer votre premier type
<Button variant="default" size="lg" onClick={handleCreateEventType}>
<PlusIcon className="w-4 h-4 mr-2" /> Créer votre premier type
</Button>
</div>
)}
</div>
</main>
<EventTypeModal
isModalOpen={isModalOpen}

View file

@ -3,7 +3,9 @@ import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { useTheme } from "@ui/contexts/ThemeContext";
import { useLoginEmail } from "@ui/hooks/auth";
import { Button } from "@ui/components/ui/button";
import { FieldError, Input, Label, TextField } from "@ui/ui-library/field";
import { Input } from "@ui/components/ui/input";
import { Label } from "@ui/components/ui/label";
import { FieldError } from "@ui/ui-library/field";
import { Form } from "@ui/ui-library/form";
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import { useState } from "react";
@ -59,20 +61,20 @@ export function LoginPage() {
};
return (
<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">
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-background to-secondary/5 animate-gradient-x bg-[length:400%_400%] relative overflow-hidden">
<AnimatedBackground />
<div
className={twMerge(
"w-full max-w-lg rounded-2xl animate-border-light",
"shadow-2xl shadow-purple-500/10 dark:shadow-black/30"
"shadow-2xl shadow-primary/10"
)}
onClick={(e) => e.stopPropagation()}
>
<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="relative w-full h-full p-8 bg-card/80 backdrop-blur-md rounded-2xl border border-border 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"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@ -90,7 +92,7 @@ export function LoginPage() {
variant="ghost"
size="icon"
onClick={toggleTheme}
className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 p-2"
className="text-muted-foreground hover:text-foreground p-2"
aria-label={`Changer le thème (actuellement: ${theme})`}
>
{getThemeIcon()}
@ -111,7 +113,7 @@ export function LoginPage() {
/>
</div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-8 text-center">
<h1 className="text-3xl font-bold text-foreground mb-8 text-center">
Se connecter à Xtablo
</h1>
@ -121,31 +123,35 @@ export function LoginPage() {
onSubmit={onSubmit}
validationErrors={errors}
>
<TextField isRequired name="email">
<Label>
<div className="space-y-2">
<Label htmlFor="email">
Email <span className="text-red-500">*</span>
</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
<FieldError />
</TextField>
</div>
<TextField isRequired name="password">
<Label>
<div className="space-y-2">
<Label htmlFor="password">
Mot de passe <span className="text-red-500">*</span>
</Label>
<Input
id="password"
name="password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
required
/>
<FieldError />
</TextField>
</div>
{/* <div className="flex items-center justify-between">
<Link to="/reset-password">
@ -155,32 +161,25 @@ export function LoginPage() {
</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..."
>
<Button className="w-full" type="submit">
{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 className="w-full border-t border-border"></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",
"px-4 py-1 bg-background",
"text-muted-foreground",
"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"
"before:absolute before:w-[100px] before:h-[1px] before:bg-border before:left-[-110px] before:top-1/2",
"after:absolute after:w-[100px] after:h-[1px] after:bg-border after:right-[-110px] after:top-1/2"
)}
>
Ou continuer avec
@ -190,10 +189,10 @@ export function LoginPage() {
<LoginWithGoogle />
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
<p className="text-center text-sm text-muted-foreground">
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">
<a className="text-foreground hover:text-foreground/80 font-medium text-sm px-2 py-1 rounded hover:bg-muted transition-colors">
S&apos;inscrire
</a>
</Link>

View file

@ -7,13 +7,15 @@ import {
SelectTrigger,
SelectValue,
} from "@ui/components/ui/select";
import { Button } from "@ui/components/ui/button";
import { useDeleteEvent, useEventsByTablo } from "@ui/hooks/events";
import { useGetAllTabloAccess, useTablosList } from "@ui/hooks/tablos";
import { EventAndTablo } from "@ui/types/events.types";
import { downloadICSFile, generateICSFromEvents } from "@ui/utils/helpers";
import { FolderInputIcon, PlusIcon } from "lucide-react";
import { Download, FolderInputIcon, PlusIcon, RefreshCcw } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet, useNavigate, useParams } from "react-router-dom";
import { TypographyH3 } from "src/components/ui/typography";
type ViewType = "month" | "week" | "day";
@ -298,13 +300,13 @@ export const PlanningPage = () => {
const timeSlots = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, "0")}:00`);
const renderMonthView = () => (
<div className="flex-1 bg-white dark:bg-gray-700/40 border border-gray-200 dark:border-gray-600/50">
<div className="flex-1 bg-card border border-border">
{/* Days header */}
<div className="grid grid-cols-7 border-b border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-7 border-b border-border">
{dayNamesShort.map((day) => (
<div
key={day}
className="p-4 text-center text-sm font-medium text-gray-500 dark:text-gray-400 border-r border-gray-200 dark:border-gray-700 last:border-r-0"
className="p-4 text-center text-sm font-medium text-muted-foreground border-r border-border last:border-r-0"
>
{day}
</div>
@ -316,16 +318,10 @@ export const PlanningPage = () => {
{getDaysInMonth(currentDate).map((day, index) => (
<div
key={index}
className={`min-h-[120px] border-b border-gray-200 dark:border-gray-700 ${
(index + 1) % 7 !== 0 ? "border-r border-gray-200 dark:border-gray-700" : ""
} ${
day
? "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"
: ""
className={`min-h-[120px] border-b border-border ${
(index + 1) % 7 !== 0 ? "border-r border-border" : ""
} ${day ? "cursor-pointer hover:bg-muted" : "bg-muted"} ${
day && formatDate(day) === formatDate(new Date()) ? "bg-primary/10" : ""
}`}
onClick={() => {
if (day) {
@ -343,9 +339,7 @@ export const PlanningPage = () => {
<div className="p-2">
<div
className={`text-sm font-medium mb-1 ${
formatDate(day) === formatDate(new Date())
? "text-blue-600 dark:text-blue-400"
: "text-gray-900 dark:text-white"
formatDate(day) === formatDate(new Date()) ? "text-primary" : "text-foreground"
}`}
>
{day.getDate()}
@ -388,7 +382,7 @@ export const PlanningPage = () => {
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"
className="absolute -top-1 -right-1 w-5 h-5 bg-destructive text-destructive-foreground rounded-full opacity-0 group-hover:opacity-100 transition-all text-sm flex items-center justify-center hover:scale-110 shadow-sm"
title="Supprimer l'événement"
>
×
@ -397,7 +391,7 @@ export const PlanningPage = () => {
</div>
))}
{getEventsForDate(day).length > 3 && (
<div className="text-xs text-gray-500 dark:text-gray-400 px-2">
<div className="text-xs text-muted-foreground px-2">
+{getEventsForDate(day).length - 3} autres
</div>
)}
@ -411,25 +405,23 @@ export const PlanningPage = () => {
);
const renderWeekView = () => (
<div className="flex-1 bg-white dark:bg-gray-700/40 border border-gray-200 dark:border-gray-600/50 flex flex-col">
<div className="flex-1 bg-card border border-border 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>
<div className="flex border-b border-border">
<div className="w-20 p-4 border-r border-border flex-shrink-0"></div>
{getWeekDays().map((day, index) => (
<div
key={day.toISOString()}
className={`flex-1 p-4 text-center border-r border-gray-200 dark:border-gray-700 ${
className={`flex-1 p-4 text-center border-r border-border ${
index === 6 ? "border-r-0" : ""
}`}
>
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase">
<div className="text-xs text-muted-foreground uppercase">
{dayNamesShort[day.getDay() === 0 ? 6 : day.getDay() - 1]}
</div>
<div
className={`text-lg font-medium mt-1 ${
formatDate(day) === formatDate(new Date())
? "text-blue-600 dark:text-blue-400"
: "text-gray-900 dark:text-white"
formatDate(day) === formatDate(new Date()) ? "text-primary" : "text-foreground"
}`}
>
{day.getDate()}
@ -441,14 +433,14 @@ export const PlanningPage = () => {
{/* Time slots */}
<div className="flex-1 overflow-y-auto">
{timeSlots.map((time) => (
<div key={time} className="flex border-b border-gray-200 dark:border-gray-700">
<div className="w-20 p-2 text-xs text-gray-500 dark:text-gray-400 text-right border-r border-gray-200 dark:border-gray-700 flex-shrink-0">
<div key={time} className="flex border-b border-border">
<div className="w-20 p-2 text-xs text-muted-foreground text-right border-r border-border flex-shrink-0">
{time}
</div>
{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-600/40 cursor-pointer relative ${
className={`flex-1 min-h-[60px] border-r border-border hover:bg-muted cursor-pointer relative ${
index === 6 ? "border-r-0" : ""
}`}
onClick={() => {
@ -461,12 +453,12 @@ export const PlanningPage = () => {
{/* Current time indicator for today */}
{isWithinCurrentHour(day, time) && (
<div
className="absolute left-0 right-0 h-0.5 bg-red-500 z-20 pointer-events-none"
className="absolute left-0 right-0 h-0.5 bg-destructive z-20 pointer-events-none"
style={{
top: `${getCurrentTimePosition()}px`,
}}
>
<div className="absolute -left-1 -top-1 w-2 h-2 bg-red-500 rounded-full"></div>
<div className="absolute -left-1 -top-1 w-2 h-2 bg-destructive rounded-full"></div>
</div>
)}
@ -521,7 +513,7 @@ export const PlanningPage = () => {
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"
className="absolute -top-1 -right-1 w-5 h-5 bg-destructive text-destructive-foreground rounded-full opacity-0 group-hover:opacity-100 transition-all text-sm flex items-center justify-center hover:scale-110 shadow-sm"
title="Supprimer l'événement"
>
×
@ -539,15 +531,13 @@ export const PlanningPage = () => {
);
const renderDayView = () => (
<div className="flex-1 bg-white dark:bg-gray-700/40 border border-gray-200 dark:border-gray-600/50">
<div className="flex-1 bg-card border border-border">
{/* 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">
<div className="p-4 border-b border-border text-center">
<div className="text-sm text-muted-foreground uppercase">
{dayNames[currentDate.getDay()]}
</div>
<div className="text-2xl font-medium text-gray-900 dark:text-white mt-1">
{currentDate.getDate()}
</div>
<div className="text-2xl font-medium text-foreground mt-1">{currentDate.getDate()}</div>
</div>
{/* Time slots */}
@ -555,7 +545,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-600/40 cursor-pointer relative min-h-[60px]"
className="flex border-b border-border hover:bg-muted cursor-pointer relative min-h-[60px]"
onClick={() => {
const [hour] = time.split(":").map(Number);
const dateWithTime = new Date(currentDate);
@ -563,19 +553,19 @@ export const PlanningPage = () => {
navigateToCreateEvent(dateWithTime, selectedTabloId);
}}
>
<div className="w-20 p-2 text-xs text-gray-500 dark:text-gray-400 text-right border-r border-gray-200 dark:border-gray-700">
<div className="w-20 p-2 text-xs text-muted-foreground text-right border-r border-border">
{time}
</div>
<div className="flex-1 p-2 relative">
{/* Current time indicator for today */}
{isWithinCurrentHour(new Date(currentDate), time) && (
<div
className="absolute left-0 right-0 h-0.5 bg-red-500 z-20 pointer-events-none"
className="absolute left-0 right-0 h-0.5 bg-destructive z-20 pointer-events-none"
style={{
top: `${getCurrentTimePosition()}px`,
}}
>
<div className="absolute -left-1 -top-1 w-2 h-2 bg-red-500 rounded-full"></div>
<div className="absolute -left-1 -top-1 w-2 h-2 bg-destructive rounded-full"></div>
</div>
)}
@ -635,7 +625,7 @@ export const PlanningPage = () => {
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"
className="absolute -top-1 -right-1 w-5 h-5 bg-destructive text-destructive-foreground rounded-full opacity-0 group-hover:opacity-100 transition-all text-sm flex items-center justify-center hover:scale-110 shadow-sm"
title="Supprimer l'événement"
>
×
@ -652,10 +642,10 @@ export const PlanningPage = () => {
);
return (
<div className="min-h-screen bg-gray-100 dark:bg-background">
<div className="min-h-screen bg-background">
<div className="flex">
{/* Sidebar */}
<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="w-64 bg-card border-r border-border min-h-screen">
<div className="p-4">
{/* Tablo Selector */}
<div className="mb-4">
@ -680,7 +670,7 @@ export const PlanningPage = () => {
</Select>
</div>
<button
<Button
onClick={() => {
if (selectedTabloId === "all") {
navigate(`/planning/create?date=${currentDate.toISOString()}`);
@ -690,29 +680,30 @@ export const PlanningPage = () => {
);
}
}}
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"
className="w-full"
>
<PlusIcon className="w-5 h-5 mr-2 text-[#1a1a1a] dark:text-white" />
<span className="text-sm">Créer un événement</span>
</button>
<PlusIcon className="w-5 h-5 mr-2" />
Créer un événement
</Button>
<button
<Button
onClick={() => setIsImportModalOpen(true)}
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"
variant="secondary"
className="w-full mt-2"
>
<FolderInputIcon className="w-5 h-5 mr-2" />
<span className="text-sm">Importer un planning</span>
</button>
Importer un planning
</Button>
</div>
{/* Mini Calendar */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<div className="text-sm font-medium text-gray-900 dark:text-white mb-3">
<div className="p-4 border-t border-border">
<div className="text-sm font-medium text-foreground mb-3">
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
</div>
<div className="grid grid-cols-7 gap-1 text-xs">
{dayNamesShort.map((day) => (
<div key={day} className="text-center text-gray-500 dark:text-gray-400 p-1">
<div key={day} className="text-center text-muted-foreground p-1">
{day.slice(0, 1)}
</div>
))}
@ -720,12 +711,12 @@ export const PlanningPage = () => {
<div
key={index}
className={`text-center p-1 cursor-pointer rounded ${
day ? "hover:bg-gray-100 dark:hover:bg-gray-600/40" : ""
day ? "hover:bg-muted" : ""
} ${
day && formatDate(day) === formatDate(new Date())
? "bg-blue-600 text-white"
? "bg-primary text-primary-foreground"
: day
? "text-gray-900 dark:text-white"
? "text-foreground"
: ""
}`}
onClick={() => {
@ -744,21 +735,15 @@ export const PlanningPage = () => {
{/* Main Content */}
<div className="flex-1 flex flex-col">
{/* Header */}
<div className="bg-white dark:bg-gray-700/40 border-b border-gray-200 dark:border-gray-600/50 p-4">
<div className="bg-card border-b border-border 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">Planning</h1>
<button
onClick={goToToday}
className="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
>
<TypographyH3>Planning</TypographyH3>
<Button onClick={goToToday} variant="outline" size="sm">
Aujourd&apos;hui
</button>
</Button>
<div className="flex items-center space-x-2">
<button
onClick={() => navigateDate(-1)}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-700 dark:text-gray-300"
>
<button onClick={() => navigateDate(-1)} className="p-2 hover:bg-muted rounded">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
@ -768,10 +753,7 @@ export const PlanningPage = () => {
/>
</svg>
</button>
<button
onClick={() => navigateDate(1)}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-700 dark:text-gray-300"
>
<button onClick={() => navigateDate(1)} className="p-2 hover:bg-muted rounded">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
@ -782,44 +764,30 @@ export const PlanningPage = () => {
</svg>
</button>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
{getViewTitle()}
</h3>
<h3 className="text-lg font-medium text-foreground">{getViewTitle()}</h3>
</div>
<div className="flex items-center space-x-2">
<button
<Button
onClick={handleExportICS}
disabled={!tabloEvents || tabloEvents.length === 0}
className="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
variant="outline"
size="sm"
title="Exporter en format ICS"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2z"
/>
</svg>
<span>Exporter</span>
</button>
<button
<Download className="w-4 h-4 mr-1" />
Exporter
</Button>
<Button
onClick={() => setIsWebcalModalOpen(true)}
className="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 flex items-center space-x-1"
variant="outline"
size="sm"
title="Synchroniser avec votre calendrier"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<span>Synchroniser</span>
</button>
<div className="flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
<RefreshCcw className="w-4 h-4 mr-1" />
Synchroniser
</Button>
<div className="flex bg-muted rounded-lg p-1">
{(["month", "week", "day"] as ViewType[]).map((view) => (
<button
key={view}
@ -829,8 +797,8 @@ export const PlanningPage = () => {
} (${view === "month" ? "M ou 1" : view === "week" ? "W ou 2" : "D ou 3"})`}
className={`px-3 py-1.5 text-sm rounded-md transition-colors capitalize ${
currentView === view
? "bg-white dark:bg-gray-800 text-gray-900 dark:text-white shadow-sm"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{view === "month" ? "Mois" : view === "week" ? "Semaine" : "Jour"}
@ -853,9 +821,7 @@ export const PlanningPage = () => {
alt="Loading..."
className="animate-spin rounded-full h-8 w-8 object-cover"
/>
<span className="ml-2 text-gray-600 dark:text-gray-300">
Chargement des événements...
</span>
<span className="ml-2 text-muted-foreground">Chargement des événements...</span>
</div>
) : (
<>

View file

@ -1,5 +1,7 @@
import { Button } from "@ui/components/ui/button";
import { FieldError, Input, Label, TextField } from "@ui/ui-library/field";
import { Input } from "@ui/components/ui/input";
import { Label } from "@ui/components/ui/label";
import { FieldError } from "@ui/ui-library/field";
import { Form } from "@ui/ui-library/form";
import { Text } from "@ui/components/ui/typography";
import { useState } from "react";
@ -26,27 +28,25 @@ export function ResetPasswordPage() {
if (isSubmitted) {
return (
<div
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-emerald-100 via-green-100 to-white dark:bg-gradient-to-br dark:from-[#0a1f0a] dark:via-[#051505] dark:to-black"
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-background to-secondary/5"
onClick={() => navigate("/login")}
>
<div
className={twMerge(
"w-full max-w-lg p-8 bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl",
"border border-emerald-200 dark:border-emerald-900/30",
"w-full max-w-lg p-8 bg-card/80 backdrop-blur-lg rounded-2xl",
"border border-border",
"shadow-xl"
)}
onClick={(e) => e.stopPropagation()}
>
<div className="text-center space-y-4">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white">Email envoyé</h1>
<Text className="text-slate-600 dark:text-slate-400">
<h1 className="text-3xl font-bold text-foreground">Email envoyé</h1>
<Text className="text-muted-foreground">
Si un compte existe avec l&apos;adresse {email}, vous recevrez un email avec les
instructions pour réinitialiser votre mot de passe.
</Text>
<Link to="/login">
<Button className={twMerge("mt-4 bg-emerald-700 text-white", "hover:bg-emerald-600")}>
Retour à la connexion
</Button>
<Button className="mt-4">Retour à la connexion</Button>
</Link>
</div>
</div>
@ -56,52 +56,47 @@ export function ResetPasswordPage() {
return (
<div
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-emerald-100 via-green-100 to-white dark:bg-gradient-to-br dark:from-[#0a1f0a] dark:via-[#051505] dark:to-black"
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-background to-secondary/5"
onClick={() => navigate("/login")}
>
<div
className={twMerge(
"w-full max-w-lg p-8 bg-white dark:bg-slate-800/50 backdrop-blur-lg rounded-2xl",
"border border-emerald-200 dark:border-emerald-900/30",
"w-full max-w-lg p-8 bg-card/80 backdrop-blur-lg rounded-2xl",
"border border-border",
"shadow-xl"
)}
onClick={(e) => e.stopPropagation()}
>
<div className="space-y-4">
<div className="text-center">
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Mot de passe oublié ?
</h1>
<Text className="text-slate-600 dark:text-slate-400">
<h1 className="text-3xl font-bold text-foreground mb-2">Mot de passe oublié ?</h1>
<Text className="text-muted-foreground">
Entrez votre adresse email et nous vous enverrons un lien pour réinitialiser votre mot
de passe.
</Text>
</div>
<Form className="space-y-4" onSubmit={onSubmit}>
<TextField isRequired name="email">
<Label>Email</Label>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<FieldError />
</TextField>
</div>
<Button
className={twMerge("w-full bg-emerald-700 text-white", "hover:bg-emerald-600")}
type="submit"
// isPending={isPending}
// pendingLabel="Envoi en cours..."
>
<Button className="w-full" type="submit">
Réinitialiser le mot de passe
</Button>
</Form>
<p className="text-center text-sm text-slate-600 dark:text-slate-400">
<a href="/login" className="text-emerald-600 hover:text-emerald-500 font-medium">
<p className="text-center text-sm text-muted-foreground">
<a href="/login" className="text-foreground hover:text-foreground/80 font-medium">
Retour à la connexion
</a>
</p>

View file

@ -3,7 +3,9 @@ import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { useTheme } from "@ui/contexts/ThemeContext";
import { useSignUp } from "@ui/hooks/auth";
import { Button } from "@ui/components/ui/button";
import { FieldError, Input, Label, TextField } from "@ui/ui-library/field";
import { Input } from "@ui/components/ui/input";
import { Label } from "@ui/components/ui/label";
import { FieldError } from "@ui/ui-library/field";
import { Form } from "@ui/ui-library/form";
import { Text } from "@ui/components/ui/typography";
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
@ -104,22 +106,22 @@ export function SignUpPage() {
return (
<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"
className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary/10 via-background to-secondary/5 animate-gradient-x bg-[length:400%_400%] relative overflow-hidden"
onClick={() => navigate("/")}
>
<AnimatedBackground />
<div
className={twMerge(
"w-full max-w-xl rounded-2xl animate-border-light",
"shadow-2xl shadow-purple-500/10 dark:shadow-black/30"
"shadow-2xl shadow-primary/10"
)}
onClick={(e) => e.stopPropagation()}
>
<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="relative w-full h-full p-6 bg-card/80 backdrop-blur-md rounded-2xl border border-border 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"
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
@ -137,7 +139,7 @@ export function SignUpPage() {
variant="ghost"
size="icon"
onClick={toggleTheme}
className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 p-2"
className="text-muted-foreground hover:text-foreground p-2"
aria-label={`Changer le thème (actuellement: ${theme})`}
>
{getThemeIcon()}
@ -158,45 +160,51 @@ export function SignUpPage() {
/>
</div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white mb-6 text-center">
<h1 className="text-2xl font-bold text-foreground 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">
<div className="space-y-2">
<Label htmlFor="first_name" className="text-sm">
Prénom <span className="text-red-500">*</span>
</Label>
<Input
id="first_name"
name="first_name"
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">
</div>
<div className="space-y-2">
<Label htmlFor="last_name" className="text-sm">
Nom <span className="text-red-500">*</span>
</Label>
<Input
id="last_name"
name="last_name"
type="text"
value={formData.last_name}
onChange={(e) => setFormData({ ...formData, last_name: e.target.value })}
required
/>
<FieldError />
</TextField>
</div>
</div>
{/* <TextField isRequired name="business_name">
<Label>
{/* <div className="space-y-2">
<Label htmlFor="business_name">
Nom de l&apos;entreprise{" "}
<span className="text-red-500">*</span>
</Label>
<Input
id="business_name"
name="business_name"
type="text"
value={formData.business_name}
onChange={(e) =>
@ -205,44 +213,46 @@ export function SignUpPage() {
required
/>
<FieldError />
</TextField> */}
</div> */}
<TextField isRequired name="email">
<Label className="text-sm">
<div className="space-y-2">
<Label htmlFor="email" className="text-sm">
Email professionnel <span className="text-red-500">*</span>
</Label>
<Input
id="email"
name="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
/>
<FieldError />
</TextField>
</div>
<TextField isRequired name="password">
<Label className="text-sm">
<div className="space-y-2">
<Label htmlFor="password" className="text-sm">
Mot de passe <span className="text-red-500">*</span>
</Label>
<Input
id="password"
name="password"
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>
{!errors.password && <Text className="text-red-500">{errors.password}</Text>}
</div>
<TextField isRequired name="confirmPassword">
<Label className="text-sm">
<div className="space-y-2">
<Label htmlFor="confirmPassword" className="text-sm">
Confirmer le mot de passe <span className="text-red-500">*</span>
</Label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={(e) =>
@ -254,60 +264,47 @@ export function SignUpPage() {
required
/>
<FieldError />
</TextField>
</div>
<TextField className="flex items-start">
<div 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"
name="terms"
className="mt-1 mr-2 h-4 w-4 rounded"
required
/>
<Label htmlFor="terms" className="text-xs text-slate-600 dark:text-slate-300">
<Label htmlFor="terms" className="text-xs text-muted-foreground">
J&apos;accepte les{" "}
<a
href="#"
className="text-black hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
>
<a href="#" className="text-foreground hover:text-foreground/80">
conditions d&apos;utilisation
</a>{" "}
et la{" "}
<a
href="#"
className="text-black hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
>
<a href="#" className="text-foreground hover:text-foreground/80">
politique de confidentialité
</a>
</Label>
</TextField>
</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"
// isPending={isPending}
// pendingLabel="Création du compte..."
>
<Button className="w-full" type="submit">
{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 className="w-full border-t border-border"></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",
"px-3 py-1 bg-background",
"text-muted-foreground",
"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"
"before:absolute before:w-[100px] before:h-[1px] before:bg-border before:left-[-110px] before:top-1/2",
"after:absolute after:w-[100px] after:h-[1px] after:bg-border after:right-[-110px] after:top-1/2"
)}
>
Ou continuer avec
@ -317,10 +314,10 @@ export function SignUpPage() {
<LoginWithGoogle />
<p className="text-center text-xs text-slate-600 dark:text-slate-400">
<p className="text-center text-xs text-muted-foreground">
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">
<a className="text-foreground hover:text-foreground/80 font-medium text-sm px-2 py-1 rounded hover:bg-muted transition-colors">
Se connecter
</a>
</Link>

View file

@ -1,191 +0,0 @@
import { SupportTicketData, useCreateSupportTicket } from "@ui/hooks/support";
import { Button } from "@ui/components/ui/button";
import { Description, Label, TextArea, TextField } from "@ui/ui-library/field";
import { Form } from "@ui/ui-library/form";
import { Text } from "@ui/components/ui/typography";
import { ArrowLeftIcon, SendIcon } from "lucide-react";
import React, { useState } from "react";
import { Separator } from "react-aria-components";
import { useNavigate } from "react-router-dom";
import { twMerge } from "tailwind-merge";
export function SupportPage() {
const navigate = useNavigate();
const [formData, setFormData] = useState<SupportTicketData>({
issue_type: "bug",
severity: "medium",
title: "",
description: "",
});
const { createSupportTicket, isSuccess, isPending } = useCreateSupportTicket();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
createSupportTicket(formData);
};
const handleInputChange = (field: keyof SupportTicketData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
return (
<div className="max-w-2xl mx-auto p-6">
{/* Header */}
<div className="mb-6">
<div className="flex items-center gap-4 mb-4">
<Button
variant="outline"
size="icon"
onClick={() => navigate(-1)}
aria-label="Retour"
className="shrink-0"
>
<ArrowLeftIcon className="w-4 h-4" />
</Button>
<div>
<Text className="text-2xl font-bold">Support technique</Text>
<Text className="text-gray-600 dark:text-gray-400 mt-1">
Signalez un problème ou demandez de l&apos;aide
</Text>
</div>
</div>
</div>
<Separator className="mb-6" />
{isSuccess ? (
<div className="text-center py-12">
<div className="text-green-600 mb-4">
<SendIcon className="w-12 h-12 mx-auto" />
</div>
<Text className="text-xl font-medium text-green-600 mb-2">
Votre ticket de support a é créé !
</Text>
<Text className="text-gray-600 dark:text-gray-400 mb-6">
Votre demande a é envoyée avec succès. Notre équipe de support vous répondra dans les
plus brefs délais.
</Text>
<Button variant="outline" onClick={() => navigate(-1)}>
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Retour
</Button>
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-lg border p-6">
<Form onSubmit={handleSubmit} className="space-y-6">
{/* Issue Type */}
<TextField>
<Label>Type de problème</Label>
<select
value={formData.issue_type}
onChange={(e) =>
handleInputChange("issue_type", e.target.value as SupportTicketData["issue_type"])
}
className={twMerge(
"w-full rounded-md border border-gray-300 dark:border-gray-600",
"px-3 py-2 bg-white dark:bg-gray-700",
"text-gray-900 dark:text-gray-100",
"focus:border-blue-500 focus:ring-1 focus:ring-blue-500",
"outline-none"
)}
required
>
<option value="bug">Bug / Erreur</option>
<option value="performance">Problème de performance</option>
<option value="security">Problème de sécurité</option>
<option value="account">Problème de compte</option>
<option value="other">Autre</option>
</select>
</TextField>
{/* Severity */}
<TextField>
<Label>Niveau de priorité</Label>
<select
value={formData.severity}
onChange={(e) =>
handleInputChange("severity", e.target.value as SupportTicketData["severity"])
}
className={twMerge(
"w-full rounded-md border border-gray-300 dark:border-gray-600",
"px-3 py-2 bg-white dark:bg-gray-700",
"text-gray-900 dark:text-gray-100",
"focus:border-blue-500 focus:ring-1 focus:ring-blue-500",
"outline-none"
)}
required
>
<option value="low">Faible</option>
<option value="medium">Moyen</option>
<option value="high">Élevé</option>
<option value="critical">Critique</option>
</select>
</TextField>
{/* Title Field */}
<TextField>
<Label>Titre du problème</Label>
<input
type="text"
value={formData.title}
onChange={(e) => handleInputChange("title", e.target.value)}
placeholder="Titre du problème"
className={twMerge(
"w-full rounded-md border border-gray-300 dark:border-gray-600",
"px-3 py-2 bg-white dark:bg-gray-700",
"text-gray-900 dark:text-gray-100",
"placeholder-gray-400 dark:placeholder-gray-500",
"focus:border-blue-500 focus:ring-1 focus:ring-blue-500",
"outline-none"
)}
required
/>
</TextField>
{/* Description Field */}
<TextField>
<Label>Description détaillée</Label>
<TextArea
value={formData.description}
onChange={(e) => handleInputChange("description", e.target.value)}
placeholder="Veuillez décrire votre problème en détail..."
rows={8}
required
className="resize-none"
/>
<Description>
Plus vous donnez de détails, plus nous pourrons vous aider rapidement
</Description>
</TextField>
{/* Submit Button */}
<div className="flex justify-end gap-3 pt-4">
<Button variant="outline" onClick={() => navigate(-1)} type="button">
Annuler
</Button>
<Button
variant="default"
type="submit"
disabled={isPending || !formData.title || !formData.description}
className="min-w-32"
>
{isPending ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Envoi en cours...
</span>
) : (
<span className="flex items-center gap-2">
<SendIcon className="w-4 h-4" />
Envoyer le ticket
</span>
)}
</Button>
</div>
</Form>
</div>
)}
</div>
);
}

View file

@ -13,6 +13,15 @@ import {
SelectTrigger,
SelectValue,
} from "@ui/components/ui/select";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@ui/components/ui/empty";
import { Button } from "@ui/components/ui/button";
import { Text, TypographyH3, TypographyMuted } from "@ui/components/ui/typography";
import {
HelpCircle,
CheckCircle2,
@ -22,6 +31,8 @@ import {
Shield,
LayoutGrid,
List,
Plus,
Trash2,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
@ -158,13 +169,13 @@ export const TabloPage = () => {
const getStatusBadgeColor = (status: string) => {
switch (status) {
case "todo":
return "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300";
return "bg-muted text-muted-foreground";
case "in_progress":
return "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300";
return "bg-primary/10 text-primary";
case "done":
return "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300";
return "bg-secondary text-secondary-foreground";
default:
return "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300";
return "bg-muted text-muted-foreground";
}
};
@ -233,7 +244,7 @@ export const TabloPage = () => {
};
const getRoleColor = (tablo: UserTablo) => {
return tablo.is_admin ? "text-blue-600 dark:text-blue-400" : "text-gray-500 dark:text-gray-400";
return tablo.is_admin ? "text-primary" : "text-muted-foreground";
};
// Calculate KPIs
@ -265,32 +276,21 @@ export const TabloPage = () => {
if (isLoading) {
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Tablos</h1>
<div className="flex items-center gap-3">
<button
type="button"
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 text-[#1a1a1a] dark:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<span>Nouveau tablo</span>
</button>
<header className="bg-card shadow-sm border-b border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold text-foreground">Tablos</h1>
<Text className="text-muted-foreground mt-1">
Gérez vos projets et collaborations
</Text>
</div>
<Button onClick={openCreateModal}>
<Plus /> Nouveau tablo
</Button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex justify-center items-center min-h-64">
<LoadingSpinner />
@ -304,39 +304,26 @@ export const TabloPage = () => {
if (error) {
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Tablos</h1>
<div className="flex items-center gap-3">
<button
type="button"
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 text-[#1a1a1a] dark:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<span>Nouveau tablo</span>
</button>
<header className="bg-card shadow-sm border-b border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold text-foreground">Tablos</h1>
<Text className="text-muted-foreground mt-1">
Gérez vos projets et collaborations
</Text>
</div>
<Button onClick={openCreateModal}>
<Plus /> Nouveau tablo
</Button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex justify-center items-center min-h-64">
<div className="text-center">
<p className="text-red-600 dark:text-red-400 mb-2">
Erreur lors du chargement des tablos
</p>
<p className="text-gray-500 dark:text-gray-400 text-sm">
<p className="text-destructive mb-2">Erreur lors du chargement des tablos</p>
<p className="text-muted-foreground text-sm">
{error instanceof Error ? error.message : "Une erreur inconnue s'est produite"}
</p>
</div>
@ -361,7 +348,7 @@ export const TabloPage = () => {
}}
>
<div
className={`bg-white dark:bg-gray-800 rounded-lg shadow-lg transition-all duration-300 w-56 overflow-hidden border border-gray-200 dark:border-gray-700 ${
className={`bg-card rounded-lg shadow-lg transition-all duration-300 w-56 overflow-hidden border border-border ${
isAdmin ? "hover:shadow-xl cursor-pointer" : "hover:shadow-xl cursor-pointer opacity-75"
}`}
onClick={(e) => {
@ -386,53 +373,21 @@ export const TabloPage = () => {
{/* Trash Icon - Only show for admins */}
{isAdmin && (
<button
className="absolute top-2 right-2 p-1.5 bg-red-500 hover:bg-red-600 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10"
className="absolute top-2 right-2 p-1.5 bg-destructive hover:bg-destructive/90 text-destructive-foreground rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10"
onClick={(e) => {
e.stopPropagation();
handleDeleteTablo(tablo.id);
}}
title="Supprimer le tablo"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
></path>
</svg>
<Trash2 className="w-4 h-4" />
</button>
)}
{/* Read-only indicator for non-admins */}
{!isAdmin && (
<div className="absolute top-2 right-2 p-1.5 bg-gray-500 text-white rounded-full opacity-80">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
<div className="absolute top-2 right-2 p-1.5 bg-muted text-muted-foreground rounded-full opacity-80">
<Shield className="w-4 h-4" />
</div>
)}
</div>
@ -441,9 +396,7 @@ export const TabloPage = () => {
<div className="p-3">
<div className="space-y-1">
<div className="flex items-center gap-1">
<h3 className="text-gray-900 dark:text-white font-semibold text-base truncate">
{tablo.name}
</h3>
<h3 className="text-foreground font-semibold text-base truncate">{tablo.name}</h3>
{/* Status badge */}
<div
className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusBadgeColor(
@ -454,14 +407,7 @@ export const TabloPage = () => {
</div>
</div>
<div className={`flex items-center gap-1 text-xs font-medium ${getRoleColor(tablo)}`}>
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z" />
</svg>
<Shield className="w-3 h-3" />
<span>{getUserRole(tablo)}</span>
</div>
</div>
@ -471,7 +417,7 @@ export const TabloPage = () => {
{/* Contextual Menu */}
{contextMenuTablo === tablo.id && contextMenuPosition && (
<div
className="fixed bg-gray-50 dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-30 min-w-36"
className="fixed bg-card rounded-lg shadow-lg border border-border py-2 z-30 min-w-36"
style={{
left: contextMenuPosition.x,
top: contextMenuPosition.y,
@ -484,7 +430,7 @@ export const TabloPage = () => {
.map((item, index) => (
<button
key={index}
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted"
onClick={(e) => {
e.stopPropagation();
item.action(tablo.id);
@ -494,76 +440,42 @@ export const TabloPage = () => {
</button>
))}
{/* <div className="border-t border-gray-200 dark:border-gray-600 my-1"></div> */}
{/* Tablo actions
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={(e) => {
e.stopPropagation();
moveTabloLeft(tablo.id);
setContextMenuTablo(null);
}}
>
Déplacer vers la gauche
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={(e) => {
e.stopPropagation();
moveTabloRight(tablo.id);
setContextMenuTablo(null);
}}
>
Déplacer vers la droite
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={(e) => {
e.stopPropagation();
openTablo(tablo.id);
setContextMenuTablo(null);
}}
>
<span>Ouvrir le tablo</span>
</button> */}
{/* Status change options - Only for admins */}
{isAdmin && (
<>
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
<div className="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
<div className="border-t border-border my-1"></div>
<div className="px-3 py-1 text-xs font-medium text-muted-foreground">
Changer le statut
</div>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "todo");
}}
>
<span>À faire</span>
{tablo.status === "todo" && <span className="text-blue-500"></span>}
{tablo.status === "todo" && <span className="text-primary"></span>}
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "in_progress");
}}
>
<span>En cours</span>
{tablo.status === "in_progress" && <span className="text-blue-500"></span>}
{tablo.status === "in_progress" && <span className="text-primary"></span>}
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "done");
}}
>
<span>Terminé</span>
{tablo.status === "done" && <span className="text-blue-500"></span>}
{tablo.status === "done" && <span className="text-primary"></span>}
</button>
</>
)}
@ -587,7 +499,7 @@ export const TabloPage = () => {
}}
>
<div
className={`bg-white dark:bg-gray-800 rounded-lg shadow-md transition-all duration-300 overflow-hidden border border-gray-200 dark:border-gray-700 ${
className={`bg-card rounded-lg shadow-md transition-all duration-300 overflow-hidden border border-border ${
isAdmin ? "hover:shadow-lg cursor-pointer" : "hover:shadow-lg cursor-pointer opacity-75"
}`}
onClick={(e) => {
@ -616,9 +528,7 @@ export const TabloPage = () => {
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3">
<h3 className="text-gray-900 dark:text-white font-semibold text-base truncate">
{tablo.name}
</h3>
<h3 className="text-foreground font-semibold text-base truncate">{tablo.name}</h3>
<div
className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusBadgeColor(
tablo.status
@ -630,14 +540,7 @@ export const TabloPage = () => {
<div
className={`flex items-center gap-1 text-xs font-medium ${getRoleColor(tablo)} mt-1`}
>
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z" />
</svg>
<Shield className="w-3 h-3" />
<span>{getUserRole(tablo)}</span>
</div>
</div>
@ -645,99 +548,47 @@ export const TabloPage = () => {
{/* Actions */}
<div className="flex items-center gap-2 flex-shrink-0">
{/* Quick action buttons */}
<button
className="p-2 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors"
<Button
variant="ghost"
size="sm"
className="p-2"
onClick={(e) => {
e.stopPropagation();
navigate(`/chat/${tablo.id}`);
}}
title="Conversations"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
</button>
<button
className="p-2 text-gray-500 hover:text-purple-600 dark:text-gray-400 dark:hover:text-purple-400 transition-colors"
<Users className="w-5 h-5" />
</Button>
<Button
variant="ghost"
size="sm"
className="p-2"
onClick={(e) => {
e.stopPropagation();
navigate(`/planning/${tablo.id}`);
}}
title="Planning"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</button>
<Clock className="w-5 h-5" />
</Button>
{isAdmin && (
<button
className="p-2 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 transition-colors"
<Button
variant="ghost"
size="sm"
className="p-2 text-destructive hover:text-destructive"
onClick={(e) => {
e.stopPropagation();
handleDeleteTablo(tablo.id);
}}
title="Supprimer le tablo"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
></path>
</svg>
</button>
<Trash2 className="w-5 h-5" />
</Button>
)}
{!isAdmin && (
<div className="p-2 text-gray-400 dark:text-gray-500" title="Lecture seule">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
></path>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
></path>
</svg>
<div className="p-2 text-muted-foreground" title="Lecture seule">
<Shield className="w-5 h-5" />
</div>
)}
</div>
@ -747,7 +598,7 @@ export const TabloPage = () => {
{/* Contextual Menu - same as grid view */}
{contextMenuTablo === tablo.id && contextMenuPosition && (
<div
className="fixed bg-gray-50 dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-30 min-w-36"
className="fixed bg-card rounded-lg shadow-lg border border-border py-2 z-30 min-w-36"
style={{
left: contextMenuPosition.x,
top: contextMenuPosition.y,
@ -759,7 +610,7 @@ export const TabloPage = () => {
.map((item, index) => (
<button
key={index}
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted"
onClick={(e) => {
e.stopPropagation();
item.action(tablo.id);
@ -771,39 +622,39 @@ export const TabloPage = () => {
{isAdmin && (
<>
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
<div className="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400">
<div className="border-t border-border my-1"></div>
<div className="px-3 py-1 text-xs font-medium text-muted-foreground">
Changer le statut
</div>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "todo");
}}
>
<span>À faire</span>
{tablo.status === "todo" && <span className="text-blue-500"></span>}
{tablo.status === "todo" && <span className="text-primary"></span>}
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "in_progress");
}}
>
<span>En cours</span>
{tablo.status === "in_progress" && <span className="text-blue-500"></span>}
{tablo.status === "in_progress" && <span className="text-primary"></span>}
</button>
<button
className="w-full px-4 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between"
className="w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted flex items-center justify-between"
onClick={(e) => {
e.stopPropagation();
changeTabloStatus(tablo.id, "done");
}}
>
<span>Terminé</span>
{tablo.status === "done" && <span className="text-blue-500"></span>}
{tablo.status === "done" && <span className="text-primary"></span>}
</button>
</>
)}
@ -821,225 +672,192 @@ export const TabloPage = () => {
setContextMenuPosition(null);
}}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Tablos</h1>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{!hasInteractedWithTutorial && (
<div className="flex items-center gap-1 px-3 py-1 bg-blue-50 dark:bg-blue-950 rounded-full border border-blue-200 dark:border-blue-800 animate-pulse shadow-lg shadow-blue-200/50 dark:shadow-blue-800/50">
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">
Avant de commencer
</span>
<svg
className="w-4 h-4 text-blue-600 dark:text-blue-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<header className="bg-card shadow-sm border-b border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="flex justify-between items-start">
<div>
<TypographyH3>Tablos</TypographyH3>
<TypographyMuted>Gérez vos projets et collaborations</TypographyMuted>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{!hasInteractedWithTutorial && (
<div className="flex items-center gap-1 px-3 py-1 bg-primary/10 rounded-full border border-primary/20 animate-pulse shadow-lg">
<span className="text-sm font-medium text-primary">Avant de commencer</span>
</div>
)}
<Button
variant="ghost"
size="sm"
className={`p-2 ${
!hasInteractedWithTutorial
? "animate-pulse bg-primary/10 rounded-lg shadow-lg ring-2 ring-primary/20"
: ""
}`}
onClick={handleOpenTutorial}
title="Aide - Revoir le guide"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
<HelpCircle
className={`w-5 h-5 ${!hasInteractedWithTutorial ? "text-primary" : ""}`}
/>
</svg>
</Button>
</div>
)}
<button
className={`p-2 text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors ${
!hasInteractedWithTutorial
? "animate-pulse bg-blue-50 dark:bg-blue-950 rounded-lg shadow-lg ring-2 ring-blue-200 dark:ring-blue-800 ring-opacity-50"
: ""
}`}
onClick={handleOpenTutorial}
title="Aide - Revoir le guide"
>
<HelpCircle
className={`w-5 h-5 ${
!hasInteractedWithTutorial ? "text-blue-600 dark:text-blue-400" : ""
}`}
/>
</button>
</div>
{/* Filter Controls */}
<div className="flex items-center gap-2">
<Select
value={filterType}
onValueChange={(value) =>
setFilterType(value as "all" | "todo" | "in_progress" | "done")
}
>
<SelectTrigger className="min-w-36 h-8">
<SelectValue placeholder="Filtrer" />
</SelectTrigger>
<SelectContent>
{filterOptions.map((option) => (
<SelectItem key={option.id} value={option.id}>
{option.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Filter Controls */}
<div className="flex items-center gap-2">
<Select
value={filterType}
onValueChange={(value) =>
setFilterType(value as "all" | "todo" | "in_progress" | "done")
}
>
<SelectTrigger className="min-w-36 h-8">
<SelectValue placeholder="Filtrer" />
</SelectTrigger>
<SelectContent>
{filterOptions.map((option) => (
<SelectItem key={option.id} value={option.id}>
{option.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* View Mode Toggle */}
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-1 border border-gray-200 dark:border-gray-700">
<button
onClick={() => setViewMode("grid")}
className={`p-1.5 rounded transition-colors ${
viewMode === "grid"
? "bg-white dark:bg-gray-700 text-purple-600 dark:text-purple-400 shadow-sm"
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
}`}
title="Vue en grille"
>
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode("list")}
className={`p-1.5 rounded transition-colors ${
viewMode === "list"
? "bg-white dark:bg-gray-700 text-purple-600 dark:text-purple-400 shadow-sm"
: "text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
}`}
title="Vue en liste"
>
<List className="w-4 h-4" />
</button>
</div>
{/* View Mode Toggle */}
<div className="flex items-center gap-1 bg-muted rounded-lg p-1 border border-border">
<button
onClick={() => setViewMode("grid")}
className={`p-1.5 rounded transition-colors ${
viewMode === "grid"
? "bg-background text-primary shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
title="Vue en grille"
>
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode("list")}
className={`p-1.5 rounded transition-colors ${
viewMode === "list"
? "bg-background text-primary shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
title="Vue en liste"
>
<List className="w-4 h-4" />
</button>
</div>
<button
id="create-tablo-button"
type="button"
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 text-[#1a1a1a] dark:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<span>{createTabloMutation.isPending ? "Création..." : "Nouveau tablo"}</span>
</button>
<Button
id="create-tablo-button"
onClick={openCreateModal}
disabled={createTabloMutation.isPending}
>
<Plus />
{createTabloMutation.isPending ? "Création..." : "Nouveau tablo"}
</Button>
</div>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{/* KPI Section */}
{kpis && (
<div className="mb-8">
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4">
{/* Total Tablos */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700">
<div className="bg-card rounded-lg shadow-md p-4 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
{kpis.totalTablos}
</p>
<p className="text-sm font-medium text-muted-foreground">Total</p>
<p className="text-2xl font-bold text-foreground mt-1">{kpis.totalTablos}</p>
</div>
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<Users className="w-5 h-5 text-purple-600 dark:text-purple-400" />
<div className="p-2 bg-primary/10 rounded-lg">
<Users className="w-5 h-5 text-primary" />
</div>
</div>
</div>
{/* Todo Count */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700">
<div className="bg-card rounded-lg shadow-md p-4 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">À faire</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
{kpis.todoCount}
</p>
<p className="text-sm font-medium text-muted-foreground">À faire</p>
<p className="text-2xl font-bold text-foreground mt-1">{kpis.todoCount}</p>
</div>
<div className="p-2 bg-gray-100 dark:bg-gray-700 rounded-lg">
<ListTodo className="w-5 h-5 text-gray-600 dark:text-gray-400" />
<div className="p-2 bg-muted rounded-lg">
<ListTodo className="w-5 h-5 text-muted-foreground" />
</div>
</div>
</div>
{/* In Progress Count */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700">
<div className="bg-card rounded-lg shadow-md p-4 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">En cours</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
<p className="text-sm font-medium text-muted-foreground">En cours</p>
<p className="text-2xl font-bold text-foreground mt-1">
{kpis.inProgressCount}
</p>
</div>
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<Clock className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<div className="p-2 bg-primary/10 rounded-lg">
<Clock className="w-5 h-5 text-primary" />
</div>
</div>
</div>
{/* Done Count */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700">
<div className="bg-card rounded-lg shadow-md p-4 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Terminé</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
{kpis.doneCount}
</p>
<p className="text-sm font-medium text-muted-foreground">Terminé</p>
<p className="text-2xl font-bold text-foreground mt-1">{kpis.doneCount}</p>
</div>
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<CheckCircle2 className="w-5 h-5 text-green-600 dark:text-green-400" />
<div className="p-2 bg-secondary/50 rounded-lg">
<CheckCircle2 className="w-5 h-5 text-secondary-foreground" />
</div>
</div>
</div>
{/* Completion Rate */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700">
<div className="bg-card rounded-lg shadow-md p-4 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Taux</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
<p className="text-sm font-medium text-muted-foreground">Taux</p>
<p className="text-2xl font-bold text-foreground mt-1">
{kpis.completionRate}%
</p>
</div>
<div className="p-2 bg-emerald-100 dark:bg-emerald-900/30 rounded-lg">
<CheckCircle2 className="w-5 h-5 text-emerald-600 dark:text-emerald-400" />
<div className="p-2 bg-secondary/50 rounded-lg">
<CheckCircle2 className="w-5 h-5 text-secondary-foreground" />
</div>
</div>
</div>
{/* Admin Count */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700">
<div className="bg-card rounded-lg shadow-md p-4 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Admin</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
{kpis.adminCount}
</p>
<p className="text-sm font-medium text-muted-foreground">Admin</p>
<p className="text-2xl font-bold text-foreground mt-1">{kpis.adminCount}</p>
</div>
<div className="p-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg">
<Shield className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />
<div className="p-2 bg-primary/10 rounded-lg">
<Shield className="w-5 h-5 text-primary" />
</div>
</div>
</div>
{/* Guest Count */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700">
<div className="bg-card rounded-lg shadow-md p-4 border border-border">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Invité</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white mt-1">
{kpis.guestCount}
</p>
<p className="text-sm font-medium text-muted-foreground">Invité</p>
<p className="text-2xl font-bold text-foreground mt-1">{kpis.guestCount}</p>
</div>
<div className="p-2 bg-slate-100 dark:bg-slate-700 rounded-lg">
<Users className="w-5 h-5 text-slate-600 dark:text-slate-400" />
<div className="p-2 bg-muted rounded-lg">
<Users className="w-5 h-5 text-muted-foreground" />
</div>
</div>
</div>
@ -1061,9 +879,9 @@ export const TabloPage = () => {
</div>
)
) : (
<div className="flex justify-center items-center min-h-64">
<div className="text-center">
<p className="text-gray-500 dark:text-gray-400 mb-4">
<Empty>
<EmptyHeader>
<EmptyTitle>
{filterType === "todo"
? "Aucun tablo 'À faire' trouvé"
: filterType === "in_progress"
@ -1071,34 +889,20 @@ export const TabloPage = () => {
: filterType === "done"
? "Aucun tablo 'Terminé' trouvé"
: "Aucun tablo trouvé"}
</p>
{filterType === "all" && (
<div className="flex justify-center">
<button
type="button"
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 text-[#1a1a1a] dark:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
></path>
</svg>
<span>Créer votre premier tablo</span>
</button>
</div>
)}
</div>
</div>
</EmptyTitle>
<EmptyDescription>
{filterType === "all" &&
"Créez votre premier tablo pour commencer à organiser votre travail"}
</EmptyDescription>
</EmptyHeader>
{filterType === "all" && (
<EmptyContent>
<Button variant="default" size="lg" onClick={openCreateModal}>
<Plus /> Créer votre premier tablo
</Button>
</EmptyContent>
)}
</Empty>
)}
</div>
</main>

View file

@ -1,85 +0,0 @@
import React from "react";
import { twMerge } from "tailwind-merge";
import { Button, ButtonProps } from "./button";
import { useCopyToClipboard } from "./hooks/use-clipboard";
import { CheckIcon, CopyIcon } from "./icons";
import { Tooltip, TooltipTrigger } from "./tooltip";
export type ClipboardProps = {
timeout?: number;
children: (payload: { copied: boolean; copy: (value: string) => void }) => React.ReactNode;
};
export function Clipboard({ timeout, children }: ClipboardProps) {
const { copied, copy } = useCopyToClipboard({ timeout });
return children({ copied, copy });
}
export function CopyButton({
copyValue,
label = "Copy",
labelAfterCopied = "Copied to clipboard",
icon,
variant = "plain",
children,
...props
}: {
copyValue: string;
label?: string;
labelAfterCopied?: string;
icon?: React.JSX.Element;
} & ButtonProps) {
const [showTooltip, setShowTooltip] = React.useState(false);
return (
<Clipboard>
{({ copied, copy }) => {
return (
<TooltipTrigger isOpen={copied || showTooltip}>
<Button
variant={variant}
{...(!children && {
isIconOnly: true,
})}
aria-label={label}
{...props}
onHoverChange={setShowTooltip}
onPress={() => {
copy(copyValue);
setShowTooltip(false);
}}
>
{children ?? (
<>
{icon ? (
React.cloneElement(icon, {
className: twMerge(
"transition-all",
copied ? "absolute scale-0 opacity-0" : "scale-100 opacity-100"
),
})
) : (
<CopyIcon
className={twMerge(
"transition-all",
copied ? "absolute scale-0 opacity-0" : "scale-100 opacity-100"
)}
/>
)}
<CheckIcon
className={twMerge(
"text-success transition-all",
copied ? "scale-100 opacity-100" : "absolute scale-0 opacity-0"
)}
/>
</>
)}
</Button>
<Tooltip>{copied ? labelAfterCopied : label}</Tooltip>
</TooltipTrigger>
);
}}
</Clipboard>
);
}