Admin Panel
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-09-29 14:57:37 +07:00
parent 04fbddba13
commit 8c6dad2a31
94 changed files with 4864 additions and 1151 deletions
@@ -0,0 +1,216 @@
import { Form } from '@inertiajs/react';
import type { ComponentProps, ReactNode } from 'react';
import { useState } from 'react';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import type { RouteFormDefinition } from '@/wayfinder';
const controlClasses =
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs outline-none focus-visible:ring-[3px] disabled:opacity-50 md:text-sm';
export function PageHeader({
title,
description,
action,
}: {
title: string;
description?: string;
action?: ReactNode;
}) {
return (
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="flex flex-col gap-1">
<h1 className="font-display text-xl font-bold tracking-tight">
{title}
</h1>
{description && (
<p className="text-muted-foreground text-sm">
{description}
</p>
)}
</div>
{action}
</div>
);
}
export function FormField({
id,
label,
error,
className,
children,
}: {
id: string;
label: string;
error?: string;
className?: string;
children: ReactNode;
}) {
return (
<div className={cn('grid gap-2', className)}>
<Label htmlFor={id}>{label}</Label>
{children}
<InputError message={error} />
</div>
);
}
export function NativeSelect({
options,
className,
...props
}: ComponentProps<'select'> & {
options: ReadonlyArray<{ value: string; label: string }>;
}) {
return (
<select
className={cn(controlClasses, 'h-9 cursor-pointer py-1', className)}
{...props}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}
export function Textarea({ className, ...props }: ComponentProps<'textarea'>) {
return (
<textarea
rows={4}
className={cn(controlClasses, className)}
{...props}
/>
);
}
type Errors = Partial<Record<string, string>>;
/**
* A dialog holding a create/edit form; it closes itself once the server
* accepts the submission.
*/
export function FormDialog({
trigger,
title,
description,
form,
submitLabel = 'Simpan',
className,
children,
}: {
trigger: ReactNode;
title: string;
description?: string;
form: RouteFormDefinition<'get' | 'post'>;
submitLabel?: string;
className?: string;
children: (errors: Errors) => ReactNode;
}) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent
className={cn(
'max-h-[90vh] overflow-y-auto sm:max-w-lg',
className,
)}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{description && (
<DialogDescription>{description}</DialogDescription>
)}
</DialogHeader>
<Form
{...form}
options={{ preserveScroll: true }}
onSuccess={() => setOpen(false)}
className="flex flex-col gap-4"
>
{({ processing, errors }) => (
<>
{children(errors)}
<DialogFooter className="gap-2">
<DialogClose asChild>
<Button type="button" variant="secondary">
Batal
</Button>
</DialogClose>
<Button type="submit" disabled={processing}>
{submitLabel}
</Button>
</DialogFooter>
</>
)}
</Form>
</DialogContent>
</Dialog>
);
}
/** Asks for confirmation before sending a delete request. */
export function ConfirmDelete({
trigger,
title,
description,
form,
}: {
trigger: ReactNode;
title: string;
description: string;
form: RouteFormDefinition<'get' | 'post'>;
}) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<Form
{...form}
options={{ preserveScroll: true }}
onSuccess={() => setOpen(false)}
>
{({ processing }) => (
<DialogFooter className="gap-2">
<DialogClose asChild>
<Button type="button" variant="secondary">
Batal
</Button>
</DialogClose>
<Button
type="submit"
variant="destructive"
disabled={processing}
>
Hapus
</Button>
</DialogFooter>
)}
</Form>
</DialogContent>
</Dialog>
);
}