File size: 5,196 Bytes
5f3e9f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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,
  )
}