Skip to content
Closed
14 changes: 13 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"tailwindcss": "^4.0.0",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.2",
"vaul": "^1.1.2",
"vite": "^6.0"
},
"optionalDependencies": {
Expand Down
58 changes: 34 additions & 24 deletions resources/js/components/delete-user.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
import { useForm } from '@inertiajs/react';
import { FormEventHandler, useRef } from 'react';
import { FormEventHandler, useCallback, useRef, useState } from 'react';

import InputError from '@/components/input-error';
import { ResponsiveModal } from '@/components/responsive-modal';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

import HeadingSmall from '@/components/heading-small';

import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { DialogDescription, DialogFooter, DialogTitle } from '@/components/ui/dialog';

export default function DeleteUser() {
const passwordInput = useRef<HTMLInputElement>(null);

const { data, setData, delete: destroy, processing, reset, errors, clearErrors } = useForm<Required<{ password: string }>>({ password: '' });
const [isOpen, setIsOpen] = useState<boolean>(false);

const handleModalClose = useCallback(() => {
setIsOpen(false);
clearErrors();
reset();
}, [clearErrors, reset]);

const handleOpenChange = useCallback(
(open: boolean) => {
if (!open) handleModalClose();
else setIsOpen(true);
},
[handleModalClose],
);

const deleteUser: FormEventHandler = (e) => {
e.preventDefault();

destroy(route('profile.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onSuccess: () => handleModalClose(),
onError: () => passwordInput.current?.focus(),
onFinish: () => reset(),
});
};

const closeModal = () => {
clearErrors();
reset();
};

return (
<div className="space-y-6">
<HeadingSmall title="Delete account" description="Delete your account and all of its resources" />
Expand All @@ -39,13 +50,14 @@ export default function DeleteUser() {
<p className="text-sm">Please proceed with caution, this cannot be undone.</p>
</div>

<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete account</Button>
</DialogTrigger>
<DialogContent>
<Button variant="destructive" onClick={() => setIsOpen(true)}>
Delete account
</Button>

<ResponsiveModal open={isOpen} onOpenChange={handleOpenChange}>
<div className="p-4 sm:p-6">
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
<DialogDescription>
<DialogDescription className="mt-2 mb-4">
Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password
to confirm you would like to permanently delete your account.
</DialogDescription>
Expand All @@ -70,19 +82,17 @@ export default function DeleteUser() {
</div>

<DialogFooter className="gap-2">
<DialogClose asChild>
<Button variant="secondary" onClick={closeModal}>
Cancel
</Button>
</DialogClose>
<Button variant="secondary" onClick={handleModalClose} type="button">
Cancel
</Button>

<Button variant="destructive" disabled={processing} asChild>
<button type="submit">Delete account</button>
<Button variant="destructive" disabled={processing} type="submit">
Delete account
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</ResponsiveModal>
</div>
</div>
);
Expand Down
31 changes: 31 additions & 0 deletions resources/js/components/responsive-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useIsMobile } from '@/hooks/use-mobile';

import { Dialog, DialogContent } from '@/components/ui/dialog';
import { Drawer, DrawerContent } from '@/components/ui/drawer';
import React from 'react';

interface ResponsiveModalProps {
children: React.ReactNode;
open: boolean;
onOpenChange: (open: boolean) => void;
}

export const ResponsiveModal = ({ children, onOpenChange, open }: ResponsiveModalProps) => {
const isMobile = useIsMobile();

if (isMobile) {
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<DrawerContent>
<div className="hide-scrollbar max-h-[85vh] overflow-y-auto">{children}</div>
</DrawerContent>
</Drawer>
);
}

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="hide-scrollbar max-h-[85vh] w-full overflow-y-auto border-none sm:max-w-lg">{children}</DialogContent>
</Dialog>
);
};
130 changes: 130 additions & 0 deletions resources/js/components/ui/drawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"

import { cn } from "@/lib/utils"

function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}

function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}

function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}

function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}

function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
/>
)
}

function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}

function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}

function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}

function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}

function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}

export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}