Improve flow for event types
This commit is contained in:
parent
b7becbc30d
commit
59e196fcb2
8 changed files with 157 additions and 155 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
34
ui/src/components/ui/hooks/use-clipboard.ts
Normal file
34
ui/src/components/ui/hooks/use-clipboard.ts
Normal 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 };
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue