Improve flow for event types

This commit is contained in:
Arthur Belleville 2025-10-19 11:34:51 +02:00
parent b7becbc30d
commit 59e196fcb2
No known key found for this signature in database
8 changed files with 157 additions and 155 deletions

View file

@ -9,15 +9,11 @@ import {
DropdownMenuTrigger,
} from "@ui/components/ui/dropdown-menu";
import { useUser } from "@ui/providers/UserStoreProvider";
// react-aria components (still used)
import { Disclosure, DisclosureControl, DisclosurePanel } from "@ui/ui-library/disclosure";
import { Link } from "@ui/ui-library/link";
import { isProd, isStaging } from "@ui/utils/helpers";
import { getXtabloIcon } from "@ui/utils/iconHelpers";
import {
CalendarCheckIcon,
CalendarIcon,
ChevronRightIcon,
Circle,
ConstructionIcon,
Kanban,
@ -32,7 +28,7 @@ import {
SquareKanban,
} from "lucide-react";
import { useState } from "react";
import { LinkProps, Separator } from "react-aria-components";
import { Separator } from "react-aria-components";
import { Link as RouterLink, useLocation } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { ThemeSwitcher } from "./ThemeSwitcher";
@ -43,35 +39,14 @@ import { useLogout } from "src/hooks/auth";
type NavLinkItem = {
isActive?: boolean;
} & LinkProps;
children: React.ReactNode;
};
type NavLinkProps = NavLinkItem | { title: string; items: NavLinkItem[] };
type NavLinkProps = NavLinkItem;
function NavLink(props: NavLinkProps) {
if ("items" in props) {
return (
<Disclosure defaultExpanded>
<DisclosureControl className="group/control [&:not(:hover)]:text-white/50 mt-3 w-full ps-2.5 text-xs /6 font-semibold">
{props.title}{" "}
<ChevronRightIcon className="ms-auto hidden size-4 transition-all group-hover/control:flex group-aria-expanded:rotate-90" />
</DisclosureControl>
<DisclosurePanel>
<ul className="grid gap-y-1">
{props.items.map((item) => (
<li key={item.href}>
<NavLink {...item} />
</li>
))}
</ul>
</DisclosurePanel>
</Disclosure>
);
}
const { isActive, ...rest } = props;
function NavLink({ isActive, children }: NavLinkProps) {
return (
<Link
{...rest}
<div
className={twMerge(
"group w-full gap-x-3 overflow-hidden px-2.5 py-1.5 text-nowrap hover:bg-navbar-darker hover:no-underline focus-visible:outline-offset-0 [&>[data-ui=icon]:not([class*=size-])]:size-4.5",
"[&>[data-ui=notification-badge]]:bg-navbar-darker",
@ -88,8 +63,8 @@ function NavLink(props: NavLinkProps) {
: ["font-medium", "text-gray-300/90 [&:not(:hover)>[data-ui=icon]]:bg-navbar-darker"]
)}
>
{props.children}
</Link>
{children}
</div>
);
}

View file

@ -10,7 +10,7 @@ import { useTabloMembers } from "@ui/hooks/tablos";
import { toast } from "@ui/lib/toast";
import { useUser } from "@ui/providers/UserStoreProvider";
import { TabloUpdate, UserTablo } from "@ui/types/tablos.types";
import { FileTrigger } from "@ui/ui-library/file-trigger";
import { FileTrigger } from "react-aria-components";
import { DownloadIcon, Trash2Icon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { ClickOutside } from "./ClickOutside";

View file

@ -1,25 +1,29 @@
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { cn } from "@ui/lib/utils";
import { Check } from "lucide-react";
import * as React from "react";
"use client";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@ui/lib/utils";
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View file

@ -1,5 +1,5 @@
import { cn } from "@ui/lib/utils";
import { useCopyToClipboard } from "@ui/ui-library/hooks/use-clipboard";
import { useCopyToClipboard } from "@ui/components/ui/hooks/use-clipboard";
import { Check, Copy } from "lucide-react";
import React from "react";
import { Button, ButtonProps } from "./button";

View file

@ -0,0 +1,34 @@
import React from "react";
export function useCopyToClipboard({ timeout = 2000 } = {}) {
const [error, setError] = React.useState<Error | null>(null);
const [copied, setCopied] = React.useState(false);
const [copyTimeout, setCopyTimeout] = React.useState<number | null>(null);
const handleCopyResult = (value: boolean) => {
window.clearTimeout(copyTimeout!);
setCopyTimeout(window.setTimeout(() => setCopied(false), timeout));
setCopied(value);
};
const copy = (valueToCopy: string) => {
if ("clipboard" in navigator) {
navigator.clipboard
.writeText(valueToCopy)
.then(() => handleCopyResult(true))
.catch((err) => setError(err));
} else {
setError(
new Error("useCopyToClipboard: navigator.clipboard is not supported")
);
}
};
const reset = () => {
setCopied(false);
setError(null);
window.clearTimeout(copyTimeout!);
};
return { copy, reset, error, copied };
}

View file

@ -33,20 +33,23 @@ export type Exception = {
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce((acc, day) => {
if (day === 5 || day === 6) {
acc[day] = {
enabled: false,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
} else {
acc[day] = {
enabled: true,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
}
return acc;
}, {} as WeeklyAvailability);
export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce(
(acc, day) => {
if (day === 5 || day === 6) {
acc[day] = {
enabled: false,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
} else {
acc[day] = {
enabled: true,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
}
return acc;
},
{} as WeeklyAvailability
);
export function useAvailabilities() {
const { session } = useSession();
@ -83,7 +86,8 @@ export function useAvailabilities() {
newException?: Exception | null;
}) => {
const newAvailabilities = updatedAvailabilities;
const newExceptions = (availabilities?.exceptions as Exception[] | null) || [];
const newExceptions =
(availabilities?.exceptions as Exception[] | null) || [];
if (newException) {
newExceptions.push(newException);
}
@ -104,14 +108,22 @@ export function useAvailabilities() {
},
});
const { mutate: deleteException } = useMutation<void, Error, { exceptionIndex: number }>({
const { mutate: deleteException } = useMutation<
void,
Error,
{ exceptionIndex: number }
>({
mutationFn: async ({ exceptionIndex }: { exceptionIndex: number }) => {
const currentExceptions = (availabilities?.exceptions as Exception[] | null) || [];
const updatedExceptions = currentExceptions.filter((_, index) => index !== exceptionIndex);
const currentExceptions =
(availabilities?.exceptions as Exception[] | null) || [];
const updatedExceptions = currentExceptions.filter(
(_, index) => index !== exceptionIndex
);
const { error } = await supabase.from("availabilities").upsert(
{
availability_data: availabilities?.availability_data || DEFAULT_AVAILABILITIES,
availability_data:
availabilities?.availability_data || DEFAULT_AVAILABILITIES,
exceptions: updatedExceptions,
user_id: session?.user.id,
},
@ -126,11 +138,14 @@ export function useAvailabilities() {
},
});
const [draftAvailabilities, setDraftAvailabilities] = useState<WeeklyAvailability | null>(null);
const [draftAvailabilities, setDraftAvailabilities] =
useState<WeeklyAvailability | null>(null);
useEffect(() => {
if (availabilities?.availability_data) {
setDraftAvailabilities(availabilities.availability_data as WeeklyAvailability);
setDraftAvailabilities(
availabilities.availability_data as WeeklyAvailability
);
}
}, [availabilities?.availability_data]);
@ -141,5 +156,6 @@ export function useAvailabilities() {
setDraftAvailabilities,
exceptions: (availabilities?.exceptions as Exception[] | null) || [],
deleteException,
isModified: draftAvailabilities !== availabilities?.availability_data,
};
}

View file

@ -26,11 +26,12 @@ import {
WeeklyAvailability,
} from "@ui/hooks/availabilities";
import { toast } from "@ui/lib/toast";
import { Checkbox } from "@ui/ui-library/checkbox";
import { Checkbox } from "@ui/components/ui/checkbox";
import { Plus as PlusIcon, SaveIcon } from "lucide-react";
import { useState } from "react";
import { ExceptionModal } from "src/components/ExceptionModal";
import { CardContent } from "src/components/ui/card";
import { Label } from "src/components/ui/label";
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
const DAYS_OF_WEEK_DISPLAY = [
@ -55,6 +56,7 @@ export function AvailabilitiesPage() {
setDraftAvailabilities,
exceptions,
deleteException,
isModified,
} = useAvailabilities();
const [copyModalOpen, setCopyModalOpen] = useState(false);
@ -118,38 +120,40 @@ export function AvailabilitiesPage() {
<TabsContent value="availabilities" className="space-y-4">
<div className="flex gap-2 mb-4">
<Button
size="sm"
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",
});
{isModified && (
<Button
size="sm"
variant="default"
className="[--btn-bg:var(--color-green-800)]"
onClick={() => {
updateAvailabilities(
{
updatedAvailabilities: draftAvailabilities,
newException: null,
},
onError: (err) => {
console.error(err);
toast.add({
title: "Erreur",
description: "Erreur lors de l'enregistrement des disponibilités",
type: "error",
});
},
}
);
}}
>
<SaveIcon /> Enregistrer
</Button>
{
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
size="sm"
onClick={() => {
@ -391,19 +395,20 @@ export function AvailabilitiesPage() {
<div className="space-y-3 max-h-60 overflow-y-auto">
{DAYS_OF_WEEK.filter((day) => day !== sourceDayData?.day).map((day) => (
<Checkbox
key={day}
isSelected={selectedDays.includes(day)}
onChange={(isSelected) => {
if (isSelected) {
setSelectedDays([...selectedDays, day]);
} else {
setSelectedDays(selectedDays.filter((d) => d !== day));
}
}}
>
<Text className="font-medium">{DAYS_OF_WEEK_DISPLAY[day]}</Text>
</Checkbox>
<div key={day} className="flex items-center gap-2">
<Checkbox
checked={selectedDays.includes(day)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedDays([...selectedDays, day]);
} else {
setSelectedDays(selectedDays.filter((d) => d !== day));
}
}}
id={`day-${day}`}
/>
<Label htmlFor={`day-${day}`}>{DAYS_OF_WEEK_DISPLAY[day]}</Label>
</div>
))}
</div>

View file

@ -1,35 +1,3 @@
/**
* From https://github.com/mantinedev/mantine/blob/master/packages/%40mantine/hooks/src/use-clipboard/use-clipboard.ts
*/
import React from "react";
export function useCopyToClipboard({ timeout = 2000 } = {}) {
const [error, setError] = React.useState<Error | null>(null);
const [copied, setCopied] = React.useState(false);
const [copyTimeout, setCopyTimeout] = React.useState<number | null>(null);
const handleCopyResult = (value: boolean) => {
window.clearTimeout(copyTimeout!);
setCopyTimeout(window.setTimeout(() => setCopied(false), timeout));
setCopied(value);
};
const copy = (valueToCopy: string) => {
if ("clipboard" in navigator) {
navigator.clipboard
.writeText(valueToCopy)
.then(() => handleCopyResult(true))
.catch((err) => setError(err));
} else {
setError(new Error("useCopyToClipboard: navigator.clipboard is not supported"));
}
};
const reset = () => {
setCopied(false);
setError(null);
window.clearTimeout(copyTimeout!);
};
return { copy, reset, error, copied };
}