191 lines
9.7 KiB
TypeScript

import { useState } from 'react'
import { X, Plus, Trash2, FileText, Clock, Layers, ChevronRight } from 'lucide-react'
import type { Page } from '../App'
function formatDate(ts: number) {
const d = new Date(ts)
const now = Date.now()
const diff = now - ts
if (diff < 60_000) return 'Just now'
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: d.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined })
}
function countBlocksDeep(blocks: Page['blocks']): number {
let n = 0
for (const b of blocks) {
n++
if (b.slots) for (const slot of b.slots) n += countBlocksDeep(slot)
}
return n
}
export function PagesModal({
pages,
currentPageId,
onLoad,
onNew,
onDelete,
onClose,
}: {
pages: Page[]
currentPageId: string | null
onLoad: (page: Page) => void
onNew: () => void
onDelete: (id: string) => Promise<void>
onClose: () => void
}) {
const [confirmDelete, setConfirmDelete] = useState<string | null>(null)
return (
<div
data-testid="cms-pages-modal"
role="dialog"
aria-modal="true"
aria-labelledby="cms-pages-title"
style={{ position: 'fixed', inset: 0, zIndex: 60, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(4px)' }}
onClick={onClose}
>
<div
style={{ background: '#fff', borderRadius: '16px', width: '640px', maxWidth: '95vw', maxHeight: '80vh', display: 'flex', flexDirection: 'column', boxShadow: '0 24px 64px rgba(0,0,0,0.2)', overflow: 'hidden' }}
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '1.25rem 1.5rem', borderBottom: '1px solid #f1f5f9' }}>
<div>
<h2 id="cms-pages-title" style={{ margin: 0, fontSize: '1rem', fontWeight: 700, color: '#0f172a' }}>Pages</h2>
<p style={{ margin: '2px 0 0', fontSize: '0.75rem', color: '#94a3b8' }}>{pages.length} saved page{pages.length !== 1 ? 's' : ''}</p>
</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<button
data-testid="cms-pages-create"
onClick={onNew}
style={{ display: 'flex', alignItems: 'center', gap: '0.375rem', padding: '0.4rem 0.875rem', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 600 }}
>
<Plus size={13} />New page
</button>
<button
onClick={onClose}
aria-label="Close pages"
style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', border: 'none', background: '#f1f5f9', borderRadius: '8px', cursor: 'pointer', color: '#64748b' }}
>
<X size={15} />
</button>
</div>
</div>
{/* Page list */}
<div style={{ flex: 1, overflowY: 'auto', padding: '0.75rem' }}>
{pages.length === 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '3rem 1rem', color: '#94a3b8' }}>
<FileText size={36} style={{ opacity: 0.3, marginBottom: '0.75rem' }} />
<p style={{ margin: 0, fontSize: '0.9rem', fontWeight: 500 }}>No saved pages yet</p>
<p style={{ margin: '0.25rem 0 1rem', fontSize: '0.8rem' }}>Save your current canvas to get started</p>
<button
data-testid="cms-pages-create-first"
onClick={onNew}
style={{ display: 'flex', alignItems: 'center', gap: '0.375rem', padding: '0.5rem 1rem', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 600 }}
>
<Plus size={13} />Create first page
</button>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
{pages.map(page => {
const isCurrent = page.id === currentPageId
const blockCount = countBlocksDeep(page.blocks)
const isDeleting = confirmDelete === page.id
return (
<div
data-testid={`cms-page-row-${page.id}`}
key={page.id}
style={{
display: 'flex', alignItems: 'center', gap: '0.875rem',
padding: '0.875rem 1rem', borderRadius: '10px',
border: isCurrent ? '2px solid #6366f1' : '2px solid #f1f5f9',
background: isCurrent ? '#fafafe' : '#fafafa',
cursor: 'pointer', transition: 'all 0.12s',
}}
onClick={() => { if (!isDeleting) { onLoad(page); onClose() } }}
>
{/* Theme swatch */}
<div style={{ flexShrink: 0, width: 44, height: 44, borderRadius: '8px', border: '1px solid #e2e8f0', overflow: 'hidden', display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr' }}>
{[page.theme.background, page.theme.primary, page.theme.muted, page.theme.accent].map((c, i) => (
<div key={i} style={{ background: /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : '#e2e8f0' }} />
))}
</div>
{/* Info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '0.875rem', fontWeight: 600, color: '#0f172a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{page.name}
</span>
{isCurrent && (
<span style={{ fontSize: '0.65rem', fontWeight: 700, background: '#6366f1', color: '#fff', padding: '1px 6px', borderRadius: '4px', letterSpacing: '0.04em', flexShrink: 0 }}>
OPEN
</span>
)}
</div>
<div style={{ display: 'flex', gap: '0.75rem', marginTop: '0.2rem' }}>
<span style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<Layers size={10} />{blockCount} block{blockCount !== 1 ? 's' : ''}
</span>
<span style={{ fontSize: '0.72rem', color: '#94a3b8', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
<Clock size={10} />Updated {formatDate(page.updatedAt)}
</span>
</div>
</div>
{/* Actions */}
<div style={{ display: 'flex', gap: '0.375rem', flexShrink: 0 }} onClick={e => e.stopPropagation()}>
{isDeleting ? (
<>
<span style={{ fontSize: '0.72rem', color: '#ef4444', fontWeight: 600, display: 'flex', alignItems: 'center' }}>Delete?</span>
<button
data-testid={`cms-page-delete-confirm-${page.id}`}
onClick={() => { void onDelete(page.id).then(() => setConfirmDelete(null)).catch(() => undefined) }}
style={{ padding: '0.25rem 0.625rem', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '0.72rem', fontWeight: 600 }}
>
Yes
</button>
<button
onClick={() => setConfirmDelete(null)}
style={{ padding: '0.25rem 0.625rem', background: '#f1f5f9', color: '#64748b', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '0.72rem' }}
>
No
</button>
</>
) : (
<>
<button
data-testid={`cms-page-delete-${page.id}`}
aria-label={`Delete ${page.name}`}
onClick={() => setConfirmDelete(page.id)}
style={{ width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'transparent', border: '1px solid #e2e8f0', borderRadius: '6px', cursor: 'pointer', color: '#94a3b8' }}
>
<Trash2 size={12} />
</button>
<button
data-testid={`cms-page-open-${page.id}`}
aria-label={`Open ${page.name}`}
onClick={() => { onLoad(page); onClose() }}
style={{ width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f1f5f9', border: 'none', borderRadius: '6px', cursor: 'pointer', color: '#6366f1' }}
>
<ChevronRight size={14} />
</button>
</>
)}
</div>
</div>
)
})}
</div>
)}
</div>
</div>
</div>
)
}