Mikkel Manniche
Dansk English Deutsch

Controls

Scheduled deletion

Banner for something deleted but still recoverable: the final date, days left and a restore button.

Preview

Usage

scheduled-deletion-demo.tsx
import { useState } from 'react'
import { ScheduledDeletion } from '@/components/scheduled-deletion'

export default function ScheduledDeletionDemo() {
  // Deleted three days ago, so eleven of the fourteen days are left.
  const [deletedAt] = useState(() => new Date(Date.now() - 3 * 86_400_000))
  const [restored, setRestored] = useState(false)
  return (
    <div className="mx-auto w-full max-w-xl">
      {restored ? (
        <p role="status" className="rounded-2xl border bg-card p-4 text-sm">
          The collection Autumn linen is back.
        </p>
      ) : (
        <ScheduledDeletion subject="The collection Autumn linen" deletedAt={deletedAt} onRestore={() => setRestored(true)} locale="en-GB" />
      )}
    </div>
  )
}

Source

scheduled-deletion.tsx
import { Clock, RotateCcw } from 'lucide-react'
import { useEffect, useState } from 'react'
import { cn } from '@/lib/utils'

export type ScheduledDeletionProps = {
  /** What is going away, e.g. “The project Spring sale”. */
  subject: string
  /** When it was marked for deletion. */
  deletedAt: Date
  /** Days it stays recoverable. */
  graceDays?: number
  onRestore: () => void
  restoreLabel?: string
  locale?: string
  className?: string
}

const DAY = 86_400_000

/**
 * A banner for something that is deleted but still recoverable: it says the date it goes for good,
 * how many days are left, and offers to restore it. The server does the real deletion when the time is up.
 */
export function ScheduledDeletion({
  subject,
  deletedAt,
  graceDays = 14,
  onRestore,
  restoreLabel = 'Restore',
  locale,
  className,
}: ScheduledDeletionProps) {
  const [now, setNow] = useState(() => Date.now())
  // Once an hour is enough to keep the day count right on a tab left open.
  useEffect(() => {
    const t = setInterval(() => setNow(Date.now()), 3_600_000)
    return () => clearInterval(t)
  }, [])

  const goneAt = new Date(deletedAt.getTime() + graceDays * DAY)
  const left = Math.max(0, Math.ceil((goneAt.getTime() - now) / DAY))
  const used = Math.min(1, Math.max(0, (now - deletedAt.getTime()) / (graceDays * DAY)))
  const date = goneAt.toLocaleDateString(locale, { day: 'numeric', month: 'long', year: 'numeric' })

  return (
    <div role="status" className={cn('grid gap-3 rounded-2xl border border-destructive/30 bg-destructive/5 p-4', className)}>
      <div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-3">
        <div className="flex min-w-0 flex-1 basis-60 gap-3">
          <Clock className="mt-0.5 size-4 shrink-0 text-destructive" aria-hidden />
          <p className="text-sm text-pretty">
            {subject} is deleted for good on <strong className="font-medium">{date}</strong>.{' '}
            <span className="text-muted-foreground">
              {left === 0 ? 'That is today.' : left === 1 ? '1 day left to restore it.' : `${left} days left to restore it.`}
            </span>
          </p>
        </div>
        <button
          type="button"
          onClick={onRestore}
          className="inline-flex min-h-11 shrink-0 items-center gap-2 rounded-xl bg-primary px-4 text-sm font-medium text-primary-foreground transition-[background-color,transform] duration-150 ease-out-quint hover:bg-primary/90 active:scale-[0.97]"
        >
          <RotateCcw className="size-4" aria-hidden />
          {restoreLabel}
        </button>
      </div>
      <div className="h-1 overflow-hidden rounded-full bg-destructive/15" aria-hidden>
        <div className="h-full origin-left rounded-full bg-destructive/60" style={{ transform: `scaleX(${used})` }} />
      </div>
    </div>
  )
}

Install

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/scheduled-deletion.json

With the registry added (components.json)

npx shadcn@latest add @manniche/scheduled-deletion

Files

  • scheduled-deletion.tsx

Manniche Lab · open source

Free tools for AI agents and web development.