YT-AI-Automation / frontend /src /components /AssetPreviewModal.tsx
github-actions
Sync Docker Space
5f3e9f5
import { Check, ChevronLeft, ChevronRight, Copy, Download, ExternalLink, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { useFocusTrap } from '../hooks/useFocusTrap'
import { useToast } from '../store/toast'
interface Props {
kind: 'html' | 'image' | 'video'
src: string
title: string
subtitle?: string
onClose: () => void
onPrevious?: () => void
onNext?: () => void
}
export default function AssetPreviewModal({
kind,
src,
title,
subtitle,
onClose,
onPrevious,
onNext,
}: Props) {
const dialogRef = useFocusTrap<HTMLDivElement>(true)
const [copyState, setCopyState] = useState<'idle' | 'copying' | 'ok'>('idle')
const toast = useToast()
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (kind === 'image' && e.key === 'ArrowLeft') onPrevious?.()
if (kind === 'image' && e.key === 'ArrowRight') onNext?.()
}
window.addEventListener('keydown', onKey)
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
window.removeEventListener('keydown', onKey)
document.body.style.overflow = prev
}
}, [kind, onClose, onNext, onPrevious])
const onCopyHtml = async () => {
if (kind !== 'html') return
setCopyState('copying')
try {
const res = await fetch(src)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const text = await res.text()
await navigator.clipboard.writeText(text)
setCopyState('ok')
toast.push({ variant: 'success', message: 'HTML copied to clipboard.' })
window.setTimeout(() => setCopyState('idle'), 1500)
} catch (e) {
setCopyState('idle')
toast.push({
variant: 'error',
title: 'Copy failed',
message: e instanceof Error ? e.message : String(e),
})
}
}
const downloadName = title.split('/').pop() ?? title
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-slate-950/70 backdrop-blur-sm" onClick={onClose} aria-hidden />
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label={title}
tabIndex={-1}
className="glass-strong relative flex h-full max-h-[92vh] w-full max-w-6xl flex-col overflow-hidden"
>
<div className="flex items-start justify-between gap-4 border-b border-slate-200 px-4 py-3 dark:border-white/10">
<div className="min-w-0">
<div className="truncate font-display text-sm font-semibold text-slate-900 dark:text-slate-50">
{title}
</div>
{subtitle && (
<div className="mt-0.5 truncate text-xs text-slate-500 dark:text-slate-400">
{subtitle}
</div>
)}
</div>
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
{kind === 'image' && onPrevious && (
<button type="button" className="btn-secondary btn-sm" onClick={onPrevious}>
<ChevronLeft size={12} /> Previous
</button>
)}
{kind === 'image' && onNext && (
<button type="button" className="btn-secondary btn-sm" onClick={onNext}>
Next <ChevronRight size={12} />
</button>
)}
{kind === 'html' && (
<button
type="button"
className="btn-secondary btn-sm"
onClick={onCopyHtml}
disabled={copyState === 'copying'}
>
{copyState === 'ok' ? <Check size={14} /> : <Copy size={14} />}
{copyState === 'ok' ? 'Copied' : copyState === 'copying' ? 'Copying...' : 'Copy HTML'}
</button>
)}
<a href={src} download={downloadName} className="btn-secondary btn-sm">
<Download size={14} /> Download
</a>
<a href={src} target="_blank" rel="noopener noreferrer" className="btn-secondary btn-sm">
<ExternalLink size={14} /> Open
</a>
<button type="button" className="btn-ghost !px-2" onClick={onClose} aria-label="Close preview">
<X size={16} />
</button>
</div>
</div>
<div className="flex min-h-0 flex-1 items-center justify-center overflow-auto bg-slate-50 p-4 dark:bg-slate-950/40">
{kind === 'html' ? (
<iframe
src={src}
title={title}
sandbox="allow-same-origin allow-scripts allow-forms"
className="h-full min-h-[70vh] w-full rounded-md border-0 bg-white dark:bg-slate-950"
/>
) : kind === 'video' ? (
<video src={src} controls autoPlay className="max-h-full max-w-full rounded-md bg-black" />
) : (
<img src={src} alt={title} className="max-h-full max-w-full rounded-md object-contain shadow-lg" />
)}
</div>
</div>
</div>,
document.body,
)
}