@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user