Improve ui
This commit is contained in:
parent
c8856a35a0
commit
affa408f3a
24 changed files with 1774 additions and 1744 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
131
ui/src/components/EventTypeCard.tsx
Normal file
131
ui/src/components/EventTypeCard.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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'é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'é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'é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'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'é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'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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
92
ui/src/components/ui/card.tsx
Normal file
92
ui/src/components/ui/card.tsx
Normal 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,
|
||||
}
|
||||
86
ui/src/components/ui/clipboard.tsx
Normal file
86
ui/src/components/ui/clipboard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
242
ui/src/components/ui/field.tsx
Normal file
242
ui/src/components/ui/field.tsx
Normal 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,
|
||||
}
|
||||
21
ui/src/components/ui/textarea.tsx
Normal file
21
ui/src/components/ui/textarea.tsx
Normal 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 };
|
||||
30
ui/src/components/ui/tooltip.tsx
Normal file
30
ui/src/components/ui/tooltip.tsx
Normal 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 };
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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'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'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'exception
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CustomModal>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'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'hui</div>
|
||||
<div className="text-sm text-muted-foreground">Aujourd'hui</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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'événements</h2>
|
||||
<Strong className="text-gray-500 mt-2 text-xl">
|
||||
Configurez les différents types d'é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'événements</TypographyH3>
|
||||
<Text className="text-muted-foreground">
|
||||
Configurez les différents types d'é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'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'é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}
|
||||
|
|
|
|||
|
|
@ -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'inscrire
|
||||
</a>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
|
|
|
|||
|
|
@ -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'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'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'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>
|
||||
|
|
|
|||
|
|
@ -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'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 été créé !
|
||||
</Text>
|
||||
<Text className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
Votre demande a été 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue