Merge pull request #55 from ciphera-net/staging
feat: soft-delete sites with 7-day grace period
This commit is contained in:
114
app/page.tsx
114
app/page.tsx
@@ -5,12 +5,13 @@ import Link from 'next/link'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useAuth } from '@/lib/auth/context'
|
||||
import { initiateOAuthFlow, initiateSignupFlow } from '@/lib/api/oauth'
|
||||
import { listSites, deleteSite, type Site } from '@/lib/api/sites'
|
||||
import { listSites, listDeletedSites, restoreSite, type Site } from '@/lib/api/sites'
|
||||
import { getStats } from '@/lib/api/stats'
|
||||
import type { Stats } from '@/lib/api/stats'
|
||||
import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing'
|
||||
import { LoadingOverlay } from '@ciphera-net/ui'
|
||||
import SiteList from '@/components/sites/SiteList'
|
||||
import DeleteSiteModal from '@/components/sites/DeleteSiteModal'
|
||||
import { Button } from '@ciphera-net/ui'
|
||||
import Image from 'next/image'
|
||||
import { BarChartIcon, LockIcon, ZapIcon, CheckCircleIcon, XIcon, GlobeIcon } from '@ciphera-net/ui'
|
||||
@@ -118,6 +119,9 @@ export default function HomePage() {
|
||||
const [subscription, setSubscription] = useState<SubscriptionDetails | null>(null)
|
||||
const [subscriptionLoading, setSubscriptionLoading] = useState(false)
|
||||
const [showFinishSetupBanner, setShowFinishSetupBanner] = useState(true)
|
||||
const [deleteModalSite, setDeleteModalSite] = useState<Site | null>(null)
|
||||
const [deletedSites, setDeletedSites] = useState<Site[]>([])
|
||||
const [permanentDeleteSiteModal, setPermanentDeleteSiteModal] = useState<Site | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.org_id) {
|
||||
@@ -178,6 +182,12 @@ export default function HomePage() {
|
||||
setSitesLoading(true)
|
||||
const data = await listSites()
|
||||
setSites(Array.isArray(data) ? data : [])
|
||||
try {
|
||||
const deleted = await listDeletedSites()
|
||||
setDeletedSites(deleted)
|
||||
} catch {
|
||||
setDeletedSites([])
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to load your sites')
|
||||
setSites([])
|
||||
@@ -198,20 +208,26 @@ export default function HomePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this site? This action cannot be undone.')) {
|
||||
return
|
||||
}
|
||||
const handleDelete = (id: string) => {
|
||||
const site = sites.find((s) => s.id === id)
|
||||
if (site) setDeleteModalSite(site)
|
||||
}
|
||||
|
||||
const handleRestore = async (id: string) => {
|
||||
try {
|
||||
await deleteSite(id)
|
||||
toast.success('Site deleted successfully')
|
||||
await restoreSite(id)
|
||||
toast.success('Site restored successfully')
|
||||
loadSites()
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to delete site')
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to restore site')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePermanentDelete = (id: string) => {
|
||||
const site = deletedSites.find((s) => s.id === id)
|
||||
if (site) setPermanentDeleteSiteModal(site)
|
||||
}
|
||||
|
||||
if (authLoading) {
|
||||
return <LoadingOverlay logoSrc="/pulse_icon_no_margins.png" title="Pulse" portal={false} />
|
||||
}
|
||||
@@ -387,15 +403,22 @@ export default function HomePage() {
|
||||
const siteLimit = getSitesLimitForPlan(subscription?.plan_id)
|
||||
const atLimit = siteLimit != null && sites.length >= siteLimit
|
||||
return atLimit ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-3 py-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||
Limit reached ({sites.length}/{siteLimit})
|
||||
</span>
|
||||
<Link href="/pricing">
|
||||
<Button variant="primary" className="text-sm">
|
||||
Upgrade
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-medium text-neutral-500 dark:text-neutral-400 bg-neutral-100 dark:bg-neutral-800 px-3 py-1.5 rounded-lg border border-neutral-200 dark:border-neutral-700">
|
||||
Limit reached ({sites.length}/{siteLimit})
|
||||
</span>
|
||||
<Link href="/pricing">
|
||||
<Button variant="primary" className="text-sm">
|
||||
Upgrade
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{deletedSites.length > 0 && (
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-2">
|
||||
You have a site pending deletion. Restore it or permanently delete it to free the slot.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
})() ?? (
|
||||
@@ -512,6 +535,63 @@ export default function HomePage() {
|
||||
{(sitesLoading || sites.length > 0) && (
|
||||
<SiteList sites={sites} siteStats={siteStats} loading={sitesLoading} onDelete={handleDelete} />
|
||||
)}
|
||||
|
||||
<DeleteSiteModal
|
||||
open={!!deleteModalSite}
|
||||
onClose={() => setDeleteModalSite(null)}
|
||||
onDeleted={loadSites}
|
||||
siteName={deleteModalSite?.name || ''}
|
||||
siteDomain={deleteModalSite?.domain || ''}
|
||||
siteId={deleteModalSite?.id || ''}
|
||||
/>
|
||||
|
||||
<DeleteSiteModal
|
||||
open={!!permanentDeleteSiteModal}
|
||||
onClose={() => setPermanentDeleteSiteModal(null)}
|
||||
onDeleted={loadSites}
|
||||
siteName={permanentDeleteSiteModal?.name || ''}
|
||||
siteDomain={permanentDeleteSiteModal?.domain || ''}
|
||||
siteId={permanentDeleteSiteModal?.id || ''}
|
||||
permanentOnly
|
||||
/>
|
||||
|
||||
{deletedSites.length > 0 && (
|
||||
<div className="mt-8">
|
||||
<h3 className="text-sm font-medium text-neutral-500 dark:text-neutral-400 mb-4">Scheduled for Deletion</h3>
|
||||
<div className="space-y-3">
|
||||
{deletedSites.map((site) => {
|
||||
const purgeAt = site.deleted_at ? new Date(new Date(site.deleted_at).getTime() + 7 * 24 * 60 * 60 * 1000) : null
|
||||
const daysLeft = purgeAt ? Math.max(0, Math.ceil((purgeAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24))) : 0
|
||||
|
||||
return (
|
||||
<div key={site.id} className="flex items-center justify-between p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 opacity-60">
|
||||
<div>
|
||||
<span className="font-medium text-neutral-700 dark:text-neutral-300">{site.name}</span>
|
||||
<span className="ml-2 text-sm text-neutral-400">{site.domain}</span>
|
||||
<span className="ml-3 inline-flex items-center rounded-full bg-red-50 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:text-red-400">
|
||||
Deleting in {daysLeft} day{daysLeft !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleRestore(site.id)}
|
||||
className="px-3 py-1.5 text-xs font-medium text-neutral-700 dark:text-neutral-300 border border-neutral-300 dark:border-neutral-700 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePermanentDelete(site.id)}
|
||||
className="px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 border border-red-200 dark:border-red-900 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
Delete Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { updateSite, resetSiteData, deleteSite, type Site, type GeoDataLevel } from '@/lib/api/sites'
|
||||
import { updateSite, resetSiteData, type Site, type GeoDataLevel } from '@/lib/api/sites'
|
||||
import { createGoal, updateGoal, deleteGoal, type Goal } from '@/lib/api/goals'
|
||||
import { createReportSchedule, updateReportSchedule, deleteReportSchedule, testReportSchedule, type ReportSchedule, type CreateReportScheduleRequest, type EmailConfig, type WebhookConfig } from '@/lib/api/report-schedules'
|
||||
import { getGSCAuthURL, disconnectGSC } from '@/lib/api/gsc'
|
||||
@@ -13,6 +13,7 @@ import { getAuthErrorMessage } from '@ciphera-net/ui'
|
||||
import { formatDateTime } from '@/lib/utils/formatDate'
|
||||
import { SettingsFormSkeleton, GoalsListSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons'
|
||||
import VerificationModal from '@/components/sites/VerificationModal'
|
||||
import DeleteSiteModal from '@/components/sites/DeleteSiteModal'
|
||||
import ScriptSetupBlock from '@/components/sites/ScriptSetupBlock'
|
||||
import { PasswordInput } from '@ciphera-net/ui'
|
||||
import { Select, Modal, Button } from '@ciphera-net/ui'
|
||||
@@ -59,6 +60,7 @@ export default function SiteSettingsPage() {
|
||||
|
||||
const { data: site, isLoading: siteLoading, mutate: mutateSite } = useSite(siteId)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'visibility' | 'data' | 'goals' | 'reports' | 'integrations'>('general')
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
@@ -462,20 +464,8 @@ export default function SiteSettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteSite = async () => {
|
||||
const confirmation = prompt('To confirm deletion, please type the site domain:')
|
||||
if (confirmation !== site?.domain) {
|
||||
if (confirmation) toast.error('Domain does not match')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteSite(siteId)
|
||||
toast.success('Site deleted successfully')
|
||||
router.push('/')
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to delete site')
|
||||
}
|
||||
const handleDeleteSite = () => {
|
||||
setShowDeleteModal(true)
|
||||
}
|
||||
|
||||
const copyLink = () => {
|
||||
@@ -804,13 +794,13 @@ export default function SiteSettingsPage() {
|
||||
<div className="p-4 border border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/10 rounded-2xl flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-red-900 dark:text-red-200">Delete Site</h3>
|
||||
<p className="text-sm text-red-700 dark:text-red-300 mt-1">Permanently delete this site and all data.</p>
|
||||
<p className="text-sm text-red-700 dark:text-red-300 mt-1">Schedule this site for deletion with a 7-day grace period.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDeleteSite}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
|
||||
>
|
||||
Delete Site
|
||||
Delete Site...
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2038,6 +2028,15 @@ export default function SiteSettingsPage() {
|
||||
site={site}
|
||||
onVerified={() => mutateSite()}
|
||||
/>
|
||||
|
||||
<DeleteSiteModal
|
||||
open={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
onDeleted={() => router.push('/')}
|
||||
siteName={site?.name || ''}
|
||||
siteDomain={site?.domain || ''}
|
||||
siteId={siteId}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
228
components/sites/DeleteSiteModal.tsx
Normal file
228
components/sites/DeleteSiteModal.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { toast, getAuthErrorMessage, AlertTriangleIcon, XIcon } from '@ciphera-net/ui'
|
||||
import { deleteSite, permanentDeleteSite } from '@/lib/api/sites'
|
||||
|
||||
interface DeleteSiteModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onDeleted: () => void
|
||||
siteName: string
|
||||
siteDomain: string
|
||||
siteId: string
|
||||
permanentOnly?: boolean
|
||||
}
|
||||
|
||||
export default function DeleteSiteModal({ open, onClose, onDeleted, siteName, siteDomain, siteId, permanentOnly }: DeleteSiteModalProps) {
|
||||
const [deleteConfirm, setDeleteConfirm] = useState('')
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showPermanent, setShowPermanent] = useState(!!permanentOnly)
|
||||
const [permanentConfirm, setPermanentConfirm] = useState('')
|
||||
const [isPermanentDeleting, setIsPermanentDeleting] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && permanentOnly) {
|
||||
setShowPermanent(true)
|
||||
}
|
||||
}, [open, permanentOnly])
|
||||
|
||||
const handleClose = () => {
|
||||
setDeleteConfirm('')
|
||||
setShowPermanent(false)
|
||||
setPermanentConfirm('')
|
||||
setIsDeleting(false)
|
||||
setIsPermanentDeleting(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleSoftDelete = async () => {
|
||||
if (deleteConfirm !== 'DELETE') return
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
await deleteSite(siteId)
|
||||
toast.success('Site scheduled for deletion. You have 7 days to restore it.')
|
||||
handleClose()
|
||||
onDeleted()
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to delete site')
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePermanentDelete = async () => {
|
||||
if (permanentConfirm !== siteDomain) return
|
||||
setIsPermanentDeleting(true)
|
||||
try {
|
||||
await permanentDeleteSite(siteId)
|
||||
toast.success('Site permanently deleted')
|
||||
handleClose()
|
||||
onDeleted()
|
||||
} catch (error: unknown) {
|
||||
toast.error(getAuthErrorMessage(error) || 'Failed to permanently delete site')
|
||||
setIsPermanentDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document === 'undefined') return null
|
||||
|
||||
return createPortal(
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-white/80 dark:bg-neutral-900/80 backdrop-blur-sm p-4 pointer-events-none"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.95, opacity: 0 }}
|
||||
className="w-full max-w-sm bg-white dark:bg-neutral-900 p-6 rounded-2xl border border-red-200 dark:border-red-900 shadow-xl pointer-events-auto"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-red-600 dark:text-red-500">Delete {siteName || 'Site'}?</h3>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-white"
|
||||
>
|
||||
<XIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!showPermanent ? (
|
||||
<>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
This site will be scheduled for deletion with a <span className="font-bold">7-day grace period</span>. You can restore it at any time during this period.
|
||||
</p>
|
||||
|
||||
<div className="mb-5 space-y-2">
|
||||
<div className="flex items-center gap-3 p-3 bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-900/20 rounded-lg">
|
||||
<AlertTriangleIcon className="h-4 w-4 text-red-500 shrink-0" />
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
All events and analytics data
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-900/20 rounded-lg">
|
||||
<AlertTriangleIcon className="h-4 w-4 text-red-500 shrink-0" />
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
Report schedules and goals
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
Type <span className="font-mono font-bold text-red-600 dark:text-red-400">DELETE</span> to confirm
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={deleteConfirm}
|
||||
onChange={(e) => setDeleteConfirm(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400"
|
||||
placeholder="DELETE"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-xl transition-colors"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSoftDelete}
|
||||
disabled={deleteConfirm !== 'DELETE' || isDeleting}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Schedule Deletion'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPermanent(true)}
|
||||
className="w-full text-center text-xs text-neutral-400 hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
Permanently delete now (cannot be undone)
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
|
||||
This action is <span className="font-bold">irreversible</span>. The site and all its data will be permanently deleted immediately.
|
||||
</p>
|
||||
|
||||
<div className="mb-5 space-y-2">
|
||||
<div className="flex items-center gap-3 p-3 bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-900/20 rounded-lg">
|
||||
<AlertTriangleIcon className="h-4 w-4 text-red-500 shrink-0" />
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
All analytics data will be permanently lost
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-900/20 rounded-lg">
|
||||
<AlertTriangleIcon className="h-4 w-4 text-red-500 shrink-0" />
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
This cannot be undone
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
Type <span className="font-mono font-bold text-red-600 dark:text-red-400">{siteDomain}</span> to confirm
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={permanentConfirm}
|
||||
onChange={(e) => setPermanentConfirm(e.target.value)}
|
||||
autoComplete="off"
|
||||
className="w-full px-3 py-2 text-sm border border-neutral-300 dark:border-neutral-700 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400"
|
||||
placeholder={siteDomain}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (permanentOnly) {
|
||||
handleClose()
|
||||
} else {
|
||||
setShowPermanent(false)
|
||||
setPermanentConfirm('')
|
||||
}
|
||||
}}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-xl transition-colors"
|
||||
disabled={isPermanentDeleting}
|
||||
>
|
||||
{permanentOnly ? 'Cancel' : 'Back'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePermanentDelete}
|
||||
disabled={permanentConfirm !== siteDomain || isPermanentDeleting}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isPermanentDeleting ? 'Deleting...' : 'Delete Forever'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export interface Site {
|
||||
is_verified?: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at?: string | null
|
||||
}
|
||||
|
||||
export interface CreateSiteRequest {
|
||||
@@ -77,8 +78,8 @@ export async function updateSite(id: string, data: UpdateSiteRequest): Promise<S
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteSite(id: string): Promise<void> {
|
||||
await apiRequest(`/sites/${id}`, {
|
||||
export async function deleteSite(id: string): Promise<{ message: string; purge_at: string }> {
|
||||
return apiRequest<{ message: string; purge_at: string }>(`/sites/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
@@ -94,3 +95,20 @@ export async function verifySite(id: string): Promise<void> {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export async function restoreSite(id: string): Promise<void> {
|
||||
await apiRequest(`/sites/${id}/restore`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export async function permanentDeleteSite(id: string): Promise<void> {
|
||||
await apiRequest(`/sites/${id}/permanent`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export async function listDeletedSites(): Promise<Site[]> {
|
||||
const response = await apiRequest<{ sites: Site[] }>('/sites/deleted')
|
||||
return response?.sites || []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user