@@ -33,6 +33,15 @@ import { Button } from '@/components/ui/button';
3333import { Badge } from '@/components/ui/badge' ;
3434import { Separator } from '@/components/ui/separator' ;
3535import { Input } from '@/components/ui/input' ;
36+ import { Label } from '@/components/ui/label' ;
37+ import {
38+ Dialog ,
39+ DialogContent ,
40+ DialogDescription ,
41+ DialogFooter ,
42+ DialogHeader ,
43+ DialogTitle ,
44+ } from '@/components/ui/dialog' ;
3645import { useProjectDetail , useRetryProvisioning , useUpdateHostname , useDeleteProject } from '@/hooks/useProjects' ;
3746import { useClient } from '@objectstack/client-react' ;
3847import { useProductionGuard } from '@/components/production-guard' ;
@@ -52,6 +61,8 @@ function ProjectOverviewComponent() {
5261 const { remove : deleteProject , deleting } = useDeleteProject ( ) ;
5362 const [ hostnameEditing , setHostnameEditing ] = useState ( false ) ;
5463 const [ hostnameInput , setHostnameInput ] = useState ( '' ) ;
64+ const [ deleteDialogOpen , setDeleteDialogOpen ] = useState ( false ) ;
65+ const [ deleteConfirmText , setDeleteConfirmText ] = useState ( '' ) ;
5566
5667 const project = detail ?. project ;
5768 const provisioningError =
@@ -134,18 +145,16 @@ function ProjectOverviewComponent() {
134145 }
135146 } ;
136147
137- const handleDelete = async ( ) => {
148+ const handleConfirmDelete = async ( ) => {
138149 if ( ! project ) return ;
139- const ok = await guard . confirm ( {
140- title : `Delete project "${ project . display_name } "?` ,
141- description :
142- 'This permanently deletes the project, its credentials, members, package installations, and the underlying physical database. This action cannot be undone.' ,
143- confirmLabel : 'Delete project' ,
144- confirmVariant : 'destructive' ,
145- requireTypedConfirmation : true ,
146- typedConfirmationValue : project . display_name ,
147- } ) ;
148- if ( ! ok ) return ;
150+ if ( deleteConfirmText !== project . display_name ) {
151+ toast ( {
152+ title : 'Confirmation does not match' ,
153+ description : `Type "${ project . display_name } " to confirm deletion.` ,
154+ variant : 'destructive' ,
155+ } ) ;
156+ return ;
157+ }
149158 try {
150159 const result = await deleteProject ( project . id , { force : project . is_default } ) ;
151160 const warnings = ( result as any ) ?. warnings as string [ ] | undefined ;
@@ -156,6 +165,8 @@ function ProjectOverviewComponent() {
156165 : `${ project . display_name } and its database have been removed.` ,
157166 variant : warnings ?. length ? 'destructive' : undefined ,
158167 } ) ;
168+ setDeleteDialogOpen ( false ) ;
169+ setDeleteConfirmText ( '' ) ;
159170 navigate ( { to : '/projects' } ) ;
160171 } catch ( err ) {
161172 toast ( {
@@ -456,26 +467,128 @@ function ProjectOverviewComponent() {
456467
457468 < Separator />
458469
459- < div className = "flex justify-end" >
460- < Button
461- variant = "destructive"
462- size = "sm"
463- className = "gap-2"
464- disabled = { deleting }
465- onClick = { handleDelete }
466- >
467- { deleting ? (
468- < Loader2 className = "h-3.5 w-3.5 animate-spin" />
469- ) : (
470+ { /* Danger zone — GitHub/Vercel-style cascade-delete card. */ }
471+ < Card className = "border-destructive/40 p-5" >
472+ < h2 className = "mb-2 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-destructive" >
473+ < AlertTriangle className = "h-3.5 w-3.5" />
474+ Danger zone
475+ </ h2 >
476+ < div className = "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between" >
477+ < div className = "text-sm" >
478+ < p className = "font-medium" > Delete this project</ p >
479+ < p className = "text-muted-foreground" >
480+ Once deleted, the project, its credentials, members, package
481+ installations, and the underlying database are gone forever.
482+ </ p >
483+ </ div >
484+ < Button
485+ variant = "destructive"
486+ size = "sm"
487+ className = "gap-2 self-start sm:self-auto"
488+ disabled = { deleting }
489+ onClick = { ( ) => setDeleteDialogOpen ( true ) }
490+ >
470491 < Trash className = "h-3.5 w-3.5" />
471- ) }
472- { deleting ? 'Deleting…' : 'Delete project' }
473- </ Button >
474- </ div >
492+ Delete project
493+ </ Button >
494+ </ div >
495+ </ Card >
475496 </ >
476497 ) }
477498 </ div >
478499 </ div >
500+
501+ { /* Delete Project Dialog (GitHub/Vercel-style typed confirmation) */ }
502+ < Dialog
503+ open = { deleteDialogOpen }
504+ onOpenChange = { ( open ) => {
505+ if ( deleting ) return ;
506+ setDeleteDialogOpen ( open ) ;
507+ if ( ! open ) setDeleteConfirmText ( '' ) ;
508+ } }
509+ >
510+ < DialogContent className = "sm:max-w-lg" >
511+ < DialogHeader >
512+ < DialogTitle className = "flex items-center gap-2 text-destructive" >
513+ < AlertTriangle className = "h-5 w-5" />
514+ Delete project
515+ </ DialogTitle >
516+ < DialogDescription >
517+ This action < strong > cannot be undone</ strong > . This will permanently
518+ delete the < strong > { project ?. display_name } </ strong > project, its
519+ credentials, members, package installations, and the underlying
520+ physical database.
521+ </ DialogDescription >
522+ </ DialogHeader >
523+
524+ { project && (
525+ < div className = "my-2 space-y-1.5 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-xs" >
526+ < div className = "flex flex-col gap-0.5" >
527+ < span className = "text-muted-foreground" > Project</ span >
528+ < span className = "font-medium" > { project . display_name } </ span >
529+ </ div >
530+ < div className = "flex flex-col gap-0.5" >
531+ < span className = "text-muted-foreground" > ID</ span >
532+ < code className = "break-all font-mono" > { project . id } </ code >
533+ </ div >
534+ { project . database_url && (
535+ < div className = "flex flex-col gap-0.5" >
536+ < span className = "text-muted-foreground" > Database</ span >
537+ < code className = "break-all font-mono" > { project . database_url } </ code >
538+ </ div >
539+ ) }
540+ </ div >
541+ ) }
542+
543+ < div className = "grid gap-1.5" >
544+ < Label htmlFor = "delete-project-confirm" >
545+ Please type{ ' ' }
546+ < code className = "font-mono text-xs" > { project ?. display_name } </ code > { ' ' }
547+ to confirm.
548+ </ Label >
549+ < Input
550+ id = "delete-project-confirm"
551+ value = { deleteConfirmText }
552+ onChange = { ( e ) => setDeleteConfirmText ( e . target . value ) }
553+ placeholder = { project ?. display_name ?? '' }
554+ autoComplete = "off"
555+ autoFocus
556+ disabled = { deleting }
557+ />
558+ </ div >
559+
560+ < DialogFooter >
561+ < Button
562+ variant = "ghost"
563+ onClick = { ( ) => {
564+ setDeleteDialogOpen ( false ) ;
565+ setDeleteConfirmText ( '' ) ;
566+ } }
567+ disabled = { deleting }
568+ >
569+ Cancel
570+ </ Button >
571+ < Button
572+ variant = "destructive"
573+ onClick = { handleConfirmDelete }
574+ disabled = {
575+ deleting ||
576+ ! project ||
577+ deleteConfirmText !== project . display_name
578+ }
579+ >
580+ { deleting ? (
581+ < >
582+ < Loader2 className = "mr-2 h-3.5 w-3.5 animate-spin" />
583+ Deleting…
584+ </ >
585+ ) : (
586+ 'I understand, delete this project'
587+ ) }
588+ </ Button >
589+ </ DialogFooter >
590+ </ DialogContent >
591+ </ Dialog >
479592 </ main >
480593 ) ;
481594}
0 commit comments