feat: add delete confirmation dialog to DetailPanel
Uses the existing ConfirmDialog component (native <dialog> element styled like the app). Clicking Delete Track now opens a confirmation modal instead of instantly deleting. Also cleaned up ConfirmDialog to use React.Ref<HTMLDialogElement> for React 19 compatibility, and removed unused ConfirmDialog import from App.tsx.
This commit is contained in:
1 parent
fb3c3c7c05
commit
b514e282a1
3 files changed
+111
-3
No files matched your search
@@ -0,0 +1,81 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** The <dialog> element ref — call ref.current?.showModal() to open. */
|
||||||
|
ref?: React.Ref<HTMLDialogElement>
|
||||||
|
/** Title shown in the dialog header. */
|
||||||
|
title: string
|
||||||
|
/** Body text explaining what will happen. */
|
||||||
|
message: string
|
||||||
|
/** Label for the confirm button. Defaults to "Confirm". */
|
||||||
|
confirmLabel?: string
|
||||||
|
/** Called when the user confirms. */
|
||||||
|
onConfirm: () => void
|
||||||
|
/** Optional: called when the user cancels. */
|
||||||
|
onCancel?: () => void
|
||||||
|
/** Danger style — makes confirm button red. Default true. */
|
||||||
|
danger?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConfirmDialog({
|
||||||
|
ref,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmLabel = 'Confirm',
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
danger = true,
|
||||||
|
}: Props) {
|
||||||
|
useEffect(() => {
|
||||||
|
const dialog = ref && 'current' in ref ? ref.current : null
|
||||||
|
if (!dialog) return
|
||||||
|
|
||||||
|
const handleClose = () => onCancel?.()
|
||||||
|
dialog.addEventListener('close', handleClose)
|
||||||
|
return () => dialog.removeEventListener('close', handleClose)
|
||||||
|
}, [ref, onCancel])
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (ref && 'current' in ref && ref.current) ref.current.close('confirm')
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
if (ref && 'current' in ref && ref.current) ref.current.close('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog
|
||||||
|
ref={ref}
|
||||||
|
className="bg-kithara-panel border border-white/10 rounded-lg text-white backdrop:bg-black/60 backdrop:backdrop-blur-sm max-w-sm w-full p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (ref && 'current' in ref && e.target === ref.current) handleCancel()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form method="dialog" className="p-5">
|
||||||
|
<h3 className="text-base font-semibold mb-2">{title}</h3>
|
||||||
|
<p className="text-sm text-kithara-muted mb-5">{message}</p>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="px-3 py-1.5 text-sm rounded bg-white/5 border border-white/10 text-kithara-muted hover:text-white hover:bg-white/10 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
className={`px-3 py-1.5 text-sm rounded font-medium transition-colors ${
|
||||||
|
danger
|
||||||
|
? 'bg-red-500/80 hover:bg-red-500 text-white'
|
||||||
|
: 'bg-kithara-accent hover:bg-kithara-accent/80 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRef, useState, useEffect } from 'react'
|
import { useRef, useState, useEffect } from 'react'
|
||||||
import { getKeyColor, getBPMColor } from '../utils/musicColors'
|
import { getKeyColor, getBPMColor } from '../utils/musicColors'
|
||||||
|
import ConfirmDialog from './ConfirmDialog'
|
||||||
|
|
||||||
interface Clip {
|
interface Clip {
|
||||||
id: string
|
id: string
|
||||||
@@ -47,6 +48,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
|||||||
const artworkInputRef = useRef<HTMLInputElement>(null)
|
const artworkInputRef = useRef<HTMLInputElement>(null)
|
||||||
const [isEditingTitle, setIsEditingTitle] = useState(false)
|
const [isEditingTitle, setIsEditingTitle] = useState(false)
|
||||||
const [editTitle, setEditTitle] = useState(clip.title || '')
|
const [editTitle, setEditTitle] = useState(clip.title || '')
|
||||||
|
const deleteDialogRef = useRef<HTMLDialogElement>(null)
|
||||||
const titleInputRef = useRef<HTMLInputElement>(null)
|
const titleInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
// Focus and select when entering edit mode
|
// Focus and select when entering edit mode
|
||||||
@@ -422,7 +424,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
|||||||
Download MP3
|
Download MP3
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
onClick={() => { onDelete(clip.id); onClose() }}
|
onClick={() => deleteDialogRef.current?.showModal()}
|
||||||
className="w-full py-2 text-sm text-red-400 bg-red-500/10 hover:bg-red-500/20 rounded-md transition-colors"
|
className="w-full py-2 text-sm text-red-400 bg-red-500/10 hover:bg-red-500/20 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
Delete Track
|
Delete Track
|
||||||
@@ -430,6 +432,14 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
ref={deleteDialogRef}
|
||||||
|
title="Delete Track"
|
||||||
|
message="This will permanently delete this track and its audio file. This cannot be undone."
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={() => { onDelete(clip.id); onClose() }}
|
||||||
|
onCancel={() => deleteDialogRef.current?.close()}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useMemo, useRef, useCallback, useEffect } from 'react'
|
import { useState, useMemo, useRef, useCallback, useEffect } from 'react'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
|
import ConfirmDialog from './ConfirmDialog'
|
||||||
import { getKeyColor, getBPMColor } from '../utils/musicColors'
|
import { getKeyColor, getBPMColor } from '../utils/musicColors'
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
@@ -91,6 +92,8 @@ function useDebounce<T>(value: T, delay: number): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, onUploadArtwork, currentClipId }: Props) {
|
export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, onUploadArtwork, currentClipId }: Props) {
|
||||||
|
const deleteDialogRef = useRef<HTMLDialogElement>(null)
|
||||||
|
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null)
|
||||||
const [uploadingId, setUploadingId] = useState<string | null>(null)
|
const [uploadingId, setUploadingId] = useState<string | null>(null)
|
||||||
const artworkInputRef = useRef<HTMLInputElement | null>(null)
|
const artworkInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
@@ -224,6 +227,11 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
|||||||
filters.dateFrom ||
|
filters.dateFrom ||
|
||||||
filters.dateTo
|
filters.dateTo
|
||||||
|
|
||||||
|
const confirmDelete = (id: string) => {
|
||||||
|
onDelete(id)
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
}
|
||||||
|
|
||||||
const startRename = (clip: Clip) => {
|
const startRename = (clip: Clip) => {
|
||||||
setEditingId(clip.id)
|
setEditingId(clip.id)
|
||||||
setEditValue(clip.title || '')
|
setEditValue(clip.title || '')
|
||||||
@@ -678,7 +686,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onDelete(clip.id) }}
|
onClick={(e) => { e.stopPropagation(); setPendingDeleteId(clip.id); deleteDialogRef.current?.showModal() }}
|
||||||
className="text-kithara-muted hover:text-red-400 transition-colors shrink-0"
|
className="text-kithara-muted hover:text-red-400 transition-colors shrink-0"
|
||||||
title="Delete clip"
|
title="Delete clip"
|
||||||
>
|
>
|
||||||
@@ -830,7 +838,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onDelete(clip.id) }}
|
onClick={(e) => { e.stopPropagation(); setPendingDeleteId(clip.id); deleteDialogRef.current?.showModal() }}
|
||||||
className="text-kithara-muted hover:text-red-400 transition-colors text-xs"
|
className="text-kithara-muted hover:text-red-400 transition-colors text-xs"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
>
|
>
|
||||||
@@ -881,6 +889,15 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
|||||||
setUploadingId(null)
|
setUploadingId(null)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* Delete confirmation */}
|
||||||
|
<ConfirmDialog
|
||||||
|
ref={deleteDialogRef}
|
||||||
|
title="Delete Clip"
|
||||||
|
message="This will permanently delete this clip and its audio file. This cannot be undone."
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={() => pendingDeleteId && confirmDelete(pendingDeleteId)}
|
||||||
|
onCancel={() => setPendingDeleteId(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user