Adopts shadcn/ui components (Dialog, Button, Input, Select, Popover, Command, Checkbox, Switch, Toggle, Calendar, etc.) across the app, replacing hand-rolled modals, dropdowns, and form controls. Adds a reusable cmdk-backed MultiSelect for the Type, Tags, and Flag filters so all multi-value filter popovers share one component and layout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
167 lines
4.6 KiB
TypeScript
167 lines
4.6 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import clsx from 'clsx'
|
|
import { Trash2, Archive } from 'lucide-react'
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
|
import { Label } from '@/components/ui/label'
|
|
|
|
interface DeleteFolderDialogProps {
|
|
isOpen: boolean
|
|
folderName: string
|
|
/** Number of photos under this folder, including descendants. Surfaced
|
|
* in the dialog copy so the user understands the blast radius. */
|
|
photoCount?: number
|
|
onClose: () => void
|
|
/** Called with the chosen mode when the user confirms. */
|
|
onConfirm: (mode: 'discard' | 'permanent') => void
|
|
}
|
|
|
|
type Mode = 'discard' | 'permanent'
|
|
|
|
/**
|
|
* Two-mode folder delete dialog built on the shadcn Dialog + RadioGroup
|
|
* primitives (instead of the old custom fixed-inset backdrop). Esc
|
|
* closes, overlay click closes, focus is trapped by Radix.
|
|
*/
|
|
export function DeleteFolderDialog({
|
|
isOpen,
|
|
folderName,
|
|
photoCount,
|
|
onClose,
|
|
onConfirm,
|
|
}: DeleteFolderDialogProps) {
|
|
const [mode, setMode] = useState<Mode>('discard')
|
|
|
|
// Reset mode when re-opening so the safe option is always the default.
|
|
useEffect(() => {
|
|
if (isOpen) setMode('discard')
|
|
}, [isOpen])
|
|
|
|
const photoBlurb =
|
|
photoCount === undefined
|
|
? 'photos in this folder'
|
|
: photoCount === 0
|
|
? 'this empty folder'
|
|
: `${photoCount} photo${photoCount === 1 ? '' : 's'} in this folder`
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={(o) => !o && onClose()}>
|
|
<DialogContent className="max-w-[420px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Delete folder "{folderName}"?</DialogTitle>
|
|
<DialogDescription>
|
|
What should happen to {photoBlurb}?
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<RadioGroup
|
|
value={mode}
|
|
onValueChange={(v) => setMode(v as Mode)}
|
|
className="gap-2"
|
|
>
|
|
<ModeCard
|
|
id="folder-delete-discard"
|
|
value="discard"
|
|
icon={<Archive className="h-4 w-4" />}
|
|
title="Move photos to discard pile"
|
|
description="Photos can be restored later from Discarded. The folder and files stay on disk."
|
|
selected={mode === 'discard'}
|
|
/>
|
|
<ModeCard
|
|
id="folder-delete-permanent"
|
|
value="permanent"
|
|
icon={<Trash2 className="h-4 w-4" />}
|
|
title="Permanently delete folder and photos"
|
|
description="Removes the folder, every photo inside it, and the directory from disk. This cannot be undone."
|
|
selected={mode === 'permanent'}
|
|
destructive
|
|
/>
|
|
</RadioGroup>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant={mode === 'permanent' ? 'destructive' : 'default'}
|
|
onClick={() => onConfirm(mode)}
|
|
>
|
|
{mode === 'permanent' ? 'Delete forever' : 'Move to discard pile'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
/** Radio-group item rendered as a full-width descriptive card. */
|
|
function ModeCard({
|
|
id,
|
|
value,
|
|
icon,
|
|
title,
|
|
description,
|
|
selected,
|
|
destructive = false,
|
|
}: {
|
|
id: string
|
|
value: string
|
|
icon: React.ReactNode
|
|
title: string
|
|
description: string
|
|
selected: boolean
|
|
destructive?: boolean
|
|
}) {
|
|
return (
|
|
<Label
|
|
htmlFor={id}
|
|
className={clsx(
|
|
'flex w-full cursor-pointer gap-3 rounded-lg border p-3 text-left transition-colors',
|
|
selected
|
|
? destructive
|
|
? 'border-reject/60 bg-reject/10'
|
|
: 'border-primary/60 bg-primary/10'
|
|
: 'border-border bg-surface-2 hover:bg-surface-offset'
|
|
)}
|
|
>
|
|
<RadioGroupItem id={id} value={value} className="mt-0.5" />
|
|
<div
|
|
className={clsx(
|
|
'mt-0.5 flex-shrink-0',
|
|
selected
|
|
? destructive
|
|
? 'text-reject'
|
|
: 'text-primary'
|
|
: 'text-text-muted'
|
|
)}
|
|
>
|
|
{icon}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div
|
|
className={clsx(
|
|
'text-sm font-medium',
|
|
selected
|
|
? destructive
|
|
? 'text-reject'
|
|
: 'text-primary'
|
|
: 'text-text'
|
|
)}
|
|
>
|
|
{title}
|
|
</div>
|
|
<div className="mt-0.5 text-xs text-text-muted">{description}</div>
|
|
</div>
|
|
</Label>
|
|
)
|
|
}
|