MotorSignal-App/website/components/ops/operations-cms.tsx

1618 lines
78 KiB
TypeScript

'use client';
import React, { useState, useEffect, useCallback } from 'react';
import {
OperationsRole,
CmsDocumentSummary,
CmsDocumentDetail,
CmsRevisionItem,
} from './types';
interface OperationsCmsProps {
role: OperationsRole;
}
const DOCUMENT_SECTIONS = [
{ slug: 'homepage_hero', label: 'Homepage Hero', icon: 'splitscreen', desc: 'Hero headline, copy, and primary CTA' },
{ slug: 'features', label: 'Key Features', icon: 'featured_play_list', desc: '20Hz telemetry & sensor fusion cards' },
{ slug: 'screenshots', label: 'Screenshots', icon: 'screenshot_monitor', desc: 'App gallery visuals and captions' },
{ slug: 'faqs', label: 'FAQs', icon: 'help_outline', desc: 'Mounting & offline recording guidelines' },
{ slug: 'download_links', label: 'Download Links', icon: 'download', desc: 'APK, Play Store, and build versions' },
{ slug: 'changelog', label: 'Changelog', icon: 'history', desc: 'Release versions, dates, and notes' },
{ slug: 'policy_pages', label: 'Legal & Policies', icon: 'policy', desc: 'Privacy policy & terms of service' },
] as const;
export function OperationsCms({ role: _role }: OperationsCmsProps) {
// Document list & selection state
const [documents, setDocuments] = useState<CmsDocumentSummary[]>([]);
const [selectedSlug, setSelectedSlug] = useState<string>('homepage_hero');
const [activeDoc, setActiveDoc] = useState<CmsDocumentDetail | null>(null);
const [isLoadingList, setIsLoadingList] = useState(true);
const [isLoadingDetail, setIsLoadingDetail] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [errorBanner, setErrorBanner] = useState<string | null>(null);
const [successBanner, setSuccessBanner] = useState<string | null>(null);
// Editor form state
const [editMode, setEditMode] = useState<'structured' | 'json'>('structured');
const [workingContent, setWorkingContent] = useState<Record<string, any>>({});
const [rawJsonText, setRawJsonText] = useState<string>('{}');
const [jsonError, setJsonError] = useState<string | null>(null);
const [changeSummary, setChangeSummary] = useState<string>('Content updated');
const [isSubmitting, setIsSubmitting] = useState(false);
// Preview modal state
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
// Revision history modal & rollback state
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [selectedRevisionForRollback, setSelectedRevisionForRollback] = useState<CmsRevisionItem | null>(null);
const [rollbackReason, setRollbackReason] = useState<string>('');
const [isRollingBack, setIsRollingBack] = useState(false);
const [rollbackError, setRollbackError] = useState<string | null>(null);
// Fetch document list
const fetchDocuments = useCallback(async (isManual = false) => {
if (isManual) setIsRefreshing(true);
else setIsLoadingList(true);
setErrorBanner(null);
try {
const response = await fetch('/api/ops/cms/documents', {
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
});
if (response.status === 401) {
setErrorBanner('Your Operations session has expired.');
return;
}
if (response.status === 403) {
setErrorBanner('You do not have authority to view website content.');
return;
}
if (!response.ok) {
setErrorBanner('Failed to load CMS documents from platform.');
return;
}
const data = await response.json();
if (Array.isArray(data)) {
setDocuments(data);
}
} catch {
setErrorBanner('Network error connecting to Operations CMS service.');
} finally {
setIsLoadingList(false);
setIsRefreshing(false);
}
}, []);
// Fetch single document detail
const fetchDocumentDetail = useCallback(async (slug: string) => {
setIsLoadingDetail(true);
setErrorBanner(null);
setSuccessBanner(null);
try {
const response = await fetch(`/api/ops/cms/documents/${encodeURIComponent(slug)}`, {
method: 'GET',
headers: { accept: 'application/json' },
cache: 'no-store',
});
if (response.status === 401) {
setErrorBanner('Your Operations session has expired.');
return;
}
if (response.status === 403) {
setErrorBanner('You do not have authority to view this content document.');
return;
}
if (response.status === 404) {
setErrorBanner('CMS document not found.');
return;
}
if (!response.ok) {
setErrorBanner('Failed to load content details.');
return;
}
const data: CmsDocumentDetail = await response.json();
setActiveDoc(data);
// Initialize working content from draft if present, else published
const initialContent = data.draft ? JSON.parse(JSON.stringify(data.draft)) : JSON.parse(JSON.stringify(data.published));
setWorkingContent(initialContent);
setRawJsonText(JSON.stringify(initialContent, null, 2));
setJsonError(null);
setChangeSummary('Content updated');
} catch {
setErrorBanner('Network error loading document detail.');
} finally {
setIsLoadingDetail(false);
}
}, []);
// Initial load
useEffect(() => {
void fetchDocuments();
}, [fetchDocuments]);
// Load detail whenever selectedSlug changes
useEffect(() => {
if (selectedSlug) {
void fetchDocumentDetail(selectedSlug);
}
}, [selectedSlug, fetchDocumentDetail]);
// Update working content and sync JSON string
const updateContent = (updater: (prev: Record<string, any>) => Record<string, any>) => {
setWorkingContent((prev) => {
const next = updater(prev);
setRawJsonText(JSON.stringify(next, null, 2));
setJsonError(null);
return next;
});
};
// Handle raw JSON textarea changes
const handleJsonChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = e.target.value;
setRawJsonText(text);
try {
const parsed = JSON.parse(text);
if (typeof parsed === 'object' && parsed !== null) {
setWorkingContent(parsed);
setJsonError(null);
} else {
setJsonError('JSON root must be an object.');
}
} catch (err: any) {
setJsonError(err.message ?? 'Invalid JSON syntax');
}
};
// Save draft or publish live
const handleSaveOrPublish = async (action: 'draft' | 'publish') => {
if (jsonError) {
setErrorBanner('Please fix JSON syntax errors before submitting.');
return;
}
setIsSubmitting(true);
setErrorBanner(null);
setSuccessBanner(null);
try {
const response = await fetch(`/api/ops/cms/documents/${encodeURIComponent(selectedSlug)}`, {
method: 'PUT',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
action,
content: workingContent,
change_summary: changeSummary.trim() || (action === 'draft' ? 'Draft saved' : 'Published live'),
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
setErrorBanner(errorData?.detail ?? `Failed to ${action} changes.`);
return;
}
const updatedDoc: CmsDocumentDetail = await response.json();
setActiveDoc(updatedDoc);
const nextContent = updatedDoc.draft
? JSON.parse(JSON.stringify(updatedDoc.draft))
: JSON.parse(JSON.stringify(updatedDoc.published));
setWorkingContent(nextContent);
setRawJsonText(JSON.stringify(nextContent, null, 2));
setJsonError(null);
setSuccessBanner(
action === 'publish'
? 'Content successfully published live!'
: 'Draft successfully saved! Pending review.'
);
// Re-fetch summaries to update "has_draft" badges
void fetchDocuments(false);
} catch {
setErrorBanner(`Network error attempting to ${action} changes.`);
} finally {
setIsSubmitting(false);
}
};
// Rollback to revision
const handleRollbackSubmit = async () => {
if (!selectedRevisionForRollback || rollbackReason.trim().length < 3) {
setRollbackError('Rollback reason must be at least 3 characters.');
return;
}
setIsRollingBack(true);
setRollbackError(null);
try {
const response = await fetch(
`/api/ops/cms/documents/${encodeURIComponent(selectedSlug)}/rollback`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json',
},
body: JSON.stringify({
revision_id: selectedRevisionForRollback.revision_id,
reason: rollbackReason.trim(),
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => null);
setRollbackError(errorData?.detail ?? 'Failed to restore revision.');
return;
}
const updatedDoc: CmsDocumentDetail = await response.json();
setActiveDoc(updatedDoc);
const initialContent = updatedDoc.published;
setWorkingContent(initialContent);
setRawJsonText(JSON.stringify(initialContent, null, 2));
setSelectedRevisionForRollback(null);
setRollbackReason('');
setIsHistoryOpen(false);
setSuccessBanner(`Successfully restored revision ${selectedRevisionForRollback.revision_id}`);
// Re-fetch summaries
void fetchDocuments(false);
} catch {
setRollbackError('Network error during rollback.');
} finally {
setIsRollingBack(false);
}
};
// Keyboard accessibility for modals
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (selectedRevisionForRollback) {
setSelectedRevisionForRollback(null);
} else if (isHistoryOpen) {
setIsHistoryOpen(false);
} else if (isPreviewOpen) {
setIsPreviewOpen(false);
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedRevisionForRollback, isHistoryOpen, isPreviewOpen]);
const hasDraftPending = activeDoc ? activeDoc.draft !== null : false;
return (
<div className="space-y-6">
{/* Header Bar */}
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-outline-variant/30">
<div>
<span className="text-[11px] font-mono text-primary-fixed-dim block font-semibold">
Content Management System
</span>
<div className="flex items-center gap-2.5 mt-1">
<span className="material-symbols-outlined text-primary-container text-2xl sm:text-3xl">
edit_note
</span>
<h1 className="font-headline-xl text-2xl sm:text-3xl lg:text-4xl font-extrabold text-on-surface tracking-tight">
Website Content CMS
</h1>
</div>
<p className="text-xs sm:text-sm text-outline mt-1.5 max-w-2xl">
Manage public website copy, hero positioning, feature lists, download links, and legal policies.
</p>
</div>
{/* Refresh & Quick Actions */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
void fetchDocuments(true);
if (selectedSlug) void fetchDocumentDetail(selectedSlug);
}}
disabled={isRefreshing || isLoadingList || isLoadingDetail}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border border-outline-variant/30 bg-surface-container-low hover:bg-surface-container-high text-xs sm:text-sm font-semibold text-on-surface transition-colors disabled:opacity-50 self-start sm:self-auto shrink-0 shadow-sm"
>
<span
className={`material-symbols-outlined text-[18px] ${
isRefreshing ? 'animate-spin text-primary-container' : 'text-outline'
}`}
>
refresh
</span>
<span>{isRefreshing ? 'Refreshing...' : 'Refresh'}</span>
</button>
</div>
</div>
{/* Banners */}
{errorBanner && (
<div className="p-3.5 rounded-lg bg-error-container/20 border border-error-container/40 text-on-error-container flex items-start gap-2.5 text-xs animate-in fade-in duration-150">
<span className="material-symbols-outlined text-error text-base shrink-0 mt-0.5">
error
</span>
<div className="flex-1">{errorBanner}</div>
<button
type="button"
onClick={() => setErrorBanner(null)}
className="text-outline hover:text-on-surface"
>
<span className="material-symbols-outlined text-sm">close</span>
</button>
</div>
)}
{successBanner && (
<div className="p-3.5 rounded-lg bg-primary-container/15 border border-primary-container/30 text-primary-container flex items-start gap-2.5 text-xs animate-in fade-in duration-150">
<span className="material-symbols-outlined text-base shrink-0 mt-0.5">
check_circle
</span>
<div className="flex-1">{successBanner}</div>
<button
type="button"
onClick={() => setSuccessBanner(null)}
className="text-outline hover:text-on-surface"
>
<span className="material-symbols-outlined text-sm">close</span>
</button>
</div>
)}
{/* Main Grid: Document Navigation + Editor */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
{/* Document Navigation Tabs (Sidebar on Desktop, Horizontal Scroller on Mobile) */}
<aside className="lg:col-span-3 space-y-1.5">
<div className="text-[11px] font-mono font-semibold text-outline px-2 mb-2">
Content Documents ({DOCUMENT_SECTIONS.length})
</div>
<div className="flex lg:flex-col gap-1.5 overflow-x-auto pb-2 lg:pb-0">
{DOCUMENT_SECTIONS.map((sec) => {
const isSelected = selectedSlug === sec.slug;
const summary = documents.find((d) => d.slug === sec.slug);
const hasDraft = summary ? summary.has_draft : false;
return (
<button
key={sec.slug}
type="button"
onClick={() => setSelectedSlug(sec.slug)}
className={`flex-1 lg:w-full text-left p-2.5 rounded-lg border transition-all flex items-center justify-between gap-2 shrink-0 ${
isSelected
? 'bg-surface-container-high border-primary-container/50 text-primary-container shadow-[0_0_12px_rgba(0,242,254,0.1)]'
: 'bg-surface-container-lowest border-outline-variant/30 text-on-surface-variant hover:text-on-surface hover:bg-surface-container-low'
}`}
>
<div className="flex items-center gap-2.5 min-w-0">
<span
className={`material-symbols-outlined text-[18px] shrink-0 ${
isSelected ? 'text-primary-container' : 'text-outline'
}`}
>
{sec.icon}
</span>
<div className="truncate">
<div className="text-xs font-semibold truncate">{sec.label}</div>
<div className="text-[10px] text-outline truncate hidden sm:block">
{sec.desc}
</div>
</div>
</div>
{hasDraft && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono font-semibold bg-amber-500/15 text-amber-400 border border-amber-500/30 shrink-0">
Draft
</span>
)}
</button>
);
})}
</div>
</aside>
{/* Main Editor Body */}
<section className="lg:col-span-9 space-y-4">
{isLoadingDetail ? (
<div className="p-12 rounded-xl bg-surface-container-lowest border border-outline-variant/30 flex flex-col items-center justify-center gap-3 text-outline">
<span className="material-symbols-outlined text-3xl animate-spin text-primary-container">
progress_activity
</span>
<span className="text-xs font-mono">Loading content document...</span>
</div>
) : activeDoc ? (
<div className="rounded-2xl bg-surface-container-lowest border border-outline-variant/30 p-6 sm:p-7 space-y-6 shadow-sm">
{/* Document Status Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-5 border-b border-outline-variant/20">
<div>
<div className="flex items-center gap-2.5">
<h2 className="text-lg sm:text-xl font-bold text-on-surface font-headline-sm">
{activeDoc.title}
</h2>
{hasDraftPending ? (
<span className="px-2.5 py-0.5 rounded-full text-[11px] font-mono font-medium tracking-wide bg-amber-500/15 text-amber-400 border border-amber-500/30">
Draft Pending
</span>
) : (
<span className="px-2.5 py-0.5 rounded-full text-[11px] font-mono font-medium tracking-wide bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
Published
</span>
)}
</div>
<p className="text-xs sm:text-sm text-on-surface-variant mt-1">
{activeDoc.description}
</p>
<div className="text-[11px] font-mono text-outline mt-2 flex flex-wrap items-center gap-3">
<span>
Updated: {new Date(activeDoc.updated_at).toLocaleString()}
</span>
<span>•</span>
<span>By: {activeDoc.updated_by}</span>
<span>•</span>
<span>Revisions: {activeDoc.revisions?.length ?? 0}</span>
</div>
</div>
{/* View/Edit Mode Toggles */}
<div className="flex items-center gap-1 self-start sm:self-center p-1 rounded-xl bg-surface-container-low border border-outline-variant/30 text-xs sm:text-sm">
<button
type="button"
onClick={() => setEditMode('structured')}
className={`px-3 py-1.5 rounded-lg font-semibold transition-colors ${
editMode === 'structured'
? 'bg-surface-container-high text-primary-container shadow-sm'
: 'text-outline hover:text-on-surface'
}`}
>
Structured
</button>
<button
type="button"
onClick={() => setEditMode('json')}
className={`px-3 py-1.5 rounded-lg font-semibold transition-colors ${
editMode === 'json'
? 'bg-surface-container-high text-primary-container shadow-sm'
: 'text-outline hover:text-on-surface'
}`}
>
Raw JSON
</button>
</div>
</div>
{/* Action Toolbar */}
<div className="flex flex-wrap items-center justify-between gap-3 p-4 rounded-xl bg-surface-container-low border border-outline-variant/20 shadow-sm">
<div className="flex items-center gap-2.5">
<button
type="button"
onClick={() => setIsPreviewOpen(true)}
className="flex items-center gap-2 px-3.5 py-2 rounded-xl border border-outline-variant/40 bg-surface-container hover:bg-surface-container-high text-on-surface text-xs sm:text-sm font-semibold transition-colors"
>
<span className="material-symbols-outlined text-[18px] text-primary-fixed-dim">
visibility
</span>
<span>Preview Changes</span>
</button>
<button
type="button"
onClick={() => setIsHistoryOpen(true)}
className="flex items-center gap-2 px-3.5 py-2 rounded-xl border border-outline-variant/40 bg-surface-container hover:bg-surface-container-high text-on-surface text-xs sm:text-sm font-medium transition-colors"
>
<span className="material-symbols-outlined text-[18px] text-outline">
history
</span>
<span>Revision History</span>
</button>
</div>
<div className="flex items-center gap-2.5">
<button
type="button"
onClick={() => void handleSaveOrPublish('draft')}
disabled={isSubmitting || Boolean(jsonError)}
className="flex items-center gap-2 px-4 py-2 rounded-xl border border-amber-500/40 bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 text-xs sm:text-sm font-semibold transition-colors disabled:opacity-50"
>
<span className="material-symbols-outlined text-[18px]">save</span>
<span>{isSubmitting ? 'Saving...' : 'Save Draft'}</span>
</button>
<button
type="button"
onClick={() => void handleSaveOrPublish('publish')}
disabled={isSubmitting || Boolean(jsonError)}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-primary-container hover:brightness-110 text-on-primary-container text-xs sm:text-sm font-bold transition-all shadow-[0_0_12px_rgba(0,242,254,0.25)] disabled:opacity-50"
>
<span className="material-symbols-outlined text-[18px]">cloud_upload</span>
<span>{isSubmitting ? 'Publishing...' : 'Publish Live'}</span>
</button>
</div>
</div>
{/* Change Summary Field */}
<div>
<label
htmlFor="change-summary"
className="block text-[11px] font-mono text-outline mb-1.5"
>
Change Summary / Audit Note
</label>
<input
id="change-summary"
type="text"
value={changeSummary}
onChange={(e) => setChangeSummary(e.target.value)}
placeholder="e.g. Updated 20Hz sensor copy for iOS 18 release"
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs placeholder:text-outline focus:outline-none focus:border-primary-container"
/>
</div>
{/* Editor Mode: Structured Form vs Raw JSON */}
{editMode === 'json' ? (
<div className="space-y-2">
<div className="flex items-center justify-between text-xs text-outline font-mono">
<span>Direct Document Payload (JSON)</span>
{jsonError && (
<span className="text-error font-medium">{jsonError}</span>
)}
</div>
<textarea
rows={18}
value={rawJsonText}
onChange={handleJsonChange}
className="w-full p-3.5 rounded-lg bg-surface-container-lowest border border-outline-variant/40 font-mono text-xs text-on-surface leading-relaxed focus:outline-none focus:border-primary-container"
spellCheck={false}
/>
</div>
) : (
<div className="space-y-6 pt-2">
{/* Homepage Hero Structured Form */}
{selectedSlug === 'homepage_hero' && (
<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Headline
</label>
<input
type="text"
value={workingContent.headline ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, headline: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Subheadline
</label>
<textarea
rows={3}
value={workingContent.subheadline ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, subheadline: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
CTA Button Text
</label>
<input
type="text"
value={workingContent.cta_text ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, cta_text: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
CTA Link Target
</label>
<input
type="text"
value={workingContent.cta_link ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, cta_link: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Pill Badge Text
</label>
<input
type="text"
value={workingContent.badge_text ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, badge_text: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
</div>
)}
{/* Features Structured Form */}
{selectedSlug === 'features' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-on-surface font-headline-sm">
Features List ({Array.isArray(workingContent.items) ? workingContent.items.length : 0})
</span>
<button
type="button"
onClick={() =>
updateContent((prev) => ({
...prev,
items: [
...(Array.isArray(prev.items) ? prev.items : []),
{ title: 'New Feature', description: 'Feature description', icon: 'speed' },
],
}))
}
className="flex items-center gap-1 text-xs font-medium text-primary-container hover:underline"
>
<span className="material-symbols-outlined text-sm">add</span>
<span>Add Feature Item</span>
</button>
</div>
{Array.isArray(workingContent.items) &&
workingContent.items.map((item: any, idx: number) => (
<div
key={idx}
className="p-3.5 rounded-lg bg-surface-container-low border border-outline-variant/20 space-y-3"
>
<div className="flex items-center justify-between gap-2">
<span className="text-[11px] font-mono text-outline">
Item #{idx + 1}
</span>
<button
type="button"
onClick={() =>
updateContent((prev) => ({
...prev,
items: prev.items.filter((_: any, i: number) => i !== idx),
}))
}
className="text-outline hover:text-error text-xs flex items-center gap-0.5"
>
<span className="material-symbols-outlined text-sm">delete</span>
<span>Remove</span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="md:col-span-2">
<label className="block text-[11px] text-on-surface-variant mb-1">
Title
</label>
<input
type="text"
value={item.title ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextItems = [...prev.items];
nextItems[idx] = { ...nextItems[idx], title: e.target.value };
return { ...prev, items: nextItems };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Material Icon Name
</label>
<input
type="text"
value={item.icon ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextItems = [...prev.items];
nextItems[idx] = { ...nextItems[idx], icon: e.target.value };
return { ...prev, items: nextItems };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Description
</label>
<textarea
rows={2}
value={item.description ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextItems = [...prev.items];
nextItems[idx] = { ...nextItems[idx], description: e.target.value };
return { ...prev, items: nextItems };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
))}
</div>
)}
{/* Screenshots Structured Form */}
{selectedSlug === 'screenshots' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-on-surface font-headline-sm">
Gallery Screenshots ({Array.isArray(workingContent.items) ? workingContent.items.length : 0})
</span>
<button
type="button"
onClick={() =>
updateContent((prev) => ({
...prev,
items: [
...(Array.isArray(prev.items) ? prev.items : []),
{ title: 'New Screenshot', caption: 'Visual caption', image_url: '/screenshots/sample.png' },
],
}))
}
className="flex items-center gap-1 text-xs font-medium text-primary-container hover:underline"
>
<span className="material-symbols-outlined text-sm">add</span>
<span>Add Screenshot</span>
</button>
</div>
{Array.isArray(workingContent.items) &&
workingContent.items.map((item: any, idx: number) => (
<div
key={idx}
className="p-3.5 rounded-lg bg-surface-container-low border border-outline-variant/20 space-y-3"
>
<div className="flex items-center justify-between gap-2">
<span className="text-[11px] font-mono text-outline">
Screenshot #{idx + 1}
</span>
<button
type="button"
onClick={() =>
updateContent((prev) => ({
...prev,
items: prev.items.filter((_: any, i: number) => i !== idx),
}))
}
className="text-outline hover:text-error text-xs flex items-center gap-0.5"
>
<span className="material-symbols-outlined text-sm">delete</span>
<span>Remove</span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Title
</label>
<input
type="text"
value={item.title ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextItems = [...prev.items];
nextItems[idx] = { ...nextItems[idx], title: e.target.value };
return { ...prev, items: nextItems };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Image URL
</label>
<input
type="text"
value={item.image_url ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextItems = [...prev.items];
nextItems[idx] = { ...nextItems[idx], image_url: e.target.value };
return { ...prev, items: nextItems };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Caption
</label>
<input
type="text"
value={item.caption ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextItems = [...prev.items];
nextItems[idx] = { ...nextItems[idx], caption: e.target.value };
return { ...prev, items: nextItems };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
))}
</div>
)}
{/* FAQs Structured Form */}
{selectedSlug === 'faqs' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-on-surface font-headline-sm">
FAQ Items ({Array.isArray(workingContent.items) ? workingContent.items.length : 0})
</span>
<button
type="button"
onClick={() =>
updateContent((prev) => ({
...prev,
items: [
...(Array.isArray(prev.items) ? prev.items : []),
{ question: 'Question topic?', answer: 'Detailed guidance answer.' },
],
}))
}
className="flex items-center gap-1 text-xs font-medium text-primary-container hover:underline"
>
<span className="material-symbols-outlined text-sm">add</span>
<span>Add FAQ</span>
</button>
</div>
{Array.isArray(workingContent.items) &&
workingContent.items.map((item: any, idx: number) => (
<div
key={idx}
className="p-3.5 rounded-lg bg-surface-container-low border border-outline-variant/20 space-y-3"
>
<div className="flex items-center justify-between gap-2">
<span className="text-[11px] font-mono text-outline">
FAQ #{idx + 1}
</span>
<button
type="button"
onClick={() =>
updateContent((prev) => ({
...prev,
items: prev.items.filter((_: any, i: number) => i !== idx),
}))
}
className="text-outline hover:text-error text-xs flex items-center gap-0.5"
>
<span className="material-symbols-outlined text-sm">delete</span>
<span>Remove</span>
</button>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Question
</label>
<input
type="text"
value={item.question ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextItems = [...prev.items];
nextItems[idx] = { ...nextItems[idx], question: e.target.value };
return { ...prev, items: nextItems };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Answer
</label>
<textarea
rows={2}
value={item.answer ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextItems = [...prev.items];
nextItems[idx] = { ...nextItems[idx], answer: e.target.value };
return { ...prev, items: nextItems };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
))}
</div>
)}
{/* Download Links Structured Form */}
{selectedSlug === 'download_links' && (
<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Android APK Direct URL
</label>
<input
type="text"
value={workingContent.android_url ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, android_url: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Google Play Store URL
</label>
<input
type="text"
value={workingContent.play_store_url ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, play_store_url: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Apple TestFlight URL
</label>
<input
type="text"
value={workingContent.testflight_url ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, testflight_url: e.target.value }))
}
placeholder="Optional TestFlight public beta link"
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Version Name
</label>
<input
type="text"
value={workingContent.version_name ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, version_name: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Build Number
</label>
<input
type="number"
value={workingContent.build_number ?? 0}
onChange={(e) =>
updateContent((prev) => ({
...prev,
build_number: parseInt(e.target.value, 10) || 0,
}))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
</div>
)}
{/* Changelog Structured Form */}
{selectedSlug === 'changelog' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-on-surface font-headline-sm">
Releases List ({Array.isArray(workingContent.releases) ? workingContent.releases.length : 0})
</span>
<button
type="button"
onClick={() =>
updateContent((prev) => ({
...prev,
releases: [
{
version: 'v1.0.5',
release_date: new Date().toISOString().split('T')[0],
title: 'New Release',
notes: 'Description of changes',
},
...(Array.isArray(prev.releases) ? prev.releases : []),
],
}))
}
className="flex items-center gap-1 text-xs font-medium text-primary-container hover:underline"
>
<span className="material-symbols-outlined text-sm">add</span>
<span>Add Release</span>
</button>
</div>
{Array.isArray(workingContent.releases) &&
workingContent.releases.map((rel: any, idx: number) => (
<div
key={idx}
className="p-3.5 rounded-lg bg-surface-container-low border border-outline-variant/20 space-y-3"
>
<div className="flex items-center justify-between gap-2">
<span className="text-[11px] font-mono text-primary-container font-semibold">
{rel.version || `Release #${idx + 1}`}
</span>
<button
type="button"
onClick={() =>
updateContent((prev) => ({
...prev,
releases: prev.releases.filter((_: any, i: number) => i !== idx),
}))
}
className="text-outline hover:text-error text-xs flex items-center gap-0.5"
>
<span className="material-symbols-outlined text-sm">delete</span>
<span>Remove</span>
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Version Tag
</label>
<input
type="text"
value={rel.version ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextReleases = [...prev.releases];
nextReleases[idx] = { ...nextReleases[idx], version: e.target.value };
return { ...prev, releases: nextReleases };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Release Date
</label>
<input
type="text"
value={rel.release_date ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextReleases = [...prev.releases];
nextReleases[idx] = { ...nextReleases[idx], release_date: e.target.value };
return { ...prev, releases: nextReleases };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Title
</label>
<input
type="text"
value={rel.title ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextReleases = [...prev.releases];
nextReleases[idx] = { ...nextReleases[idx], title: e.target.value };
return { ...prev, releases: nextReleases };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
<div>
<label className="block text-[11px] text-on-surface-variant mb-1">
Release Notes / Changelog Details
</label>
<textarea
rows={2}
value={rel.notes ?? ''}
onChange={(e) =>
updateContent((prev) => {
const nextReleases = [...prev.releases];
nextReleases[idx] = { ...nextReleases[idx], notes: e.target.value };
return { ...prev, releases: nextReleases };
})
}
className="w-full px-2.5 py-1.5 rounded bg-surface-container border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-primary-container"
/>
</div>
</div>
))}
</div>
)}
{/* Policy Pages Structured Form */}
{selectedSlug === 'policy_pages' && (
<div className="space-y-4">
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Privacy Policy Body
</label>
<textarea
rows={6}
value={workingContent.privacy_policy ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, privacy_policy: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs leading-relaxed focus:outline-none focus:border-primary-container"
/>
</div>
<div>
<label className="block text-xs font-medium text-on-surface-variant mb-1">
Terms of Service Body
</label>
<textarea
rows={6}
value={workingContent.terms_of_service ?? ''}
onChange={(e) =>
updateContent((prev) => ({ ...prev, terms_of_service: e.target.value }))
}
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs leading-relaxed focus:outline-none focus:border-primary-container"
/>
</div>
</div>
)}
</div>
)}
</div>
) : (
<div className="p-12 rounded-xl bg-surface-container-lowest border border-outline-variant/30 text-center text-outline text-xs font-mono">
Select a content document to edit.
</div>
)}
</section>
</div>
{/* ========================================================================= */}
{/* PREVIEW DRAWER / MODAL */}
{/* ========================================================================= */}
{isPreviewOpen && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="preview-modal-title"
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200"
>
<div className="w-full max-w-5xl lg:max-w-6xl max-h-[90vh] flex flex-col rounded-2xl bg-surface-container-lowest border border-outline-variant/40 shadow-2xl overflow-hidden">
<div className="px-6 sm:px-8 py-5 border-b border-outline-variant/20 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-primary-container text-2xl">
visibility
</span>
<h3 id="preview-modal-title" className="text-base sm:text-lg font-bold text-on-surface font-headline-sm">
Preview: {activeDoc?.title}
</h3>
</div>
<button
type="button"
onClick={() => setIsPreviewOpen(false)}
className="p-1 rounded-lg text-outline hover:text-on-surface transition-colors"
aria-label="Close preview modal"
>
<span className="material-symbols-outlined text-xl">close</span>
</button>
</div>
<div className="p-6 overflow-y-auto flex-1 space-y-6">
{/* Homepage Hero Preview */}
{selectedSlug === 'homepage_hero' && (
<div className="rounded-xl border border-primary-container/30 bg-surface-container-low p-8 text-center space-y-4">
{workingContent.badge_text && (
<span className="inline-block px-3 py-1 rounded-full text-[10px] font-mono font-semibold bg-primary-container/15 text-primary-container border border-primary-container/30">
{workingContent.badge_text}
</span>
)}
<h1 className="text-2xl sm:text-3xl font-extrabold text-on-surface font-headline-sm tracking-tight">
{workingContent.headline || 'Headline placeholder'}
</h1>
<p className="text-sm text-on-surface-variant max-w-xl mx-auto leading-relaxed">
{workingContent.subheadline || 'Subheadline placeholder'}
</p>
<div className="pt-2">
<span className="inline-flex items-center gap-2 px-5 py-2 rounded-lg bg-primary-container text-on-primary-container font-bold text-xs shadow-lg">
<span>{workingContent.cta_text || 'Download'}</span>
<span className="material-symbols-outlined text-sm">arrow_forward</span>
</span>
</div>
</div>
)}
{/* Features Preview */}
{selectedSlug === 'features' && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Array.isArray(workingContent.items) &&
workingContent.items.map((item: any, i: number) => (
<div
key={i}
className="p-4 rounded-xl border border-outline-variant/30 bg-surface-container-low space-y-2"
>
<div className="w-8 h-8 rounded-lg bg-primary-container/15 text-primary-container flex items-center justify-center">
<span className="material-symbols-outlined text-base">
{item.icon || 'star'}
</span>
</div>
<h4 className="text-xs font-bold text-on-surface">{item.title}</h4>
<p className="text-[11px] text-on-surface-variant leading-relaxed">
{item.description}
</p>
</div>
))}
</div>
)}
{/* Screenshots Preview */}
{selectedSlug === 'screenshots' && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Array.isArray(workingContent.items) &&
workingContent.items.map((item: any, i: number) => (
<div
key={i}
className="rounded-xl border border-outline-variant/30 bg-surface-container-low overflow-hidden space-y-2"
>
<div className="h-40 bg-surface-container flex items-center justify-center text-outline">
<span className="material-symbols-outlined text-3xl">image</span>
</div>
<div className="p-3">
<div className="text-xs font-semibold text-on-surface">{item.title}</div>
<div className="text-[11px] text-outline">{item.caption}</div>
</div>
</div>
))}
</div>
)}
{/* FAQs Preview */}
{selectedSlug === 'faqs' && (
<div className="space-y-3">
{Array.isArray(workingContent.items) &&
workingContent.items.map((item: any, i: number) => (
<div
key={i}
className="p-4 rounded-xl border border-outline-variant/30 bg-surface-container-low space-y-1.5"
>
<div className="text-xs font-bold text-on-surface flex items-center gap-2">
<span className="text-primary-container font-mono">Q:</span>
<span>{item.question}</span>
</div>
<p className="text-xs text-on-surface-variant pl-4 leading-relaxed">
{item.answer}
</p>
</div>
))}
</div>
)}
{/* Download Links Preview */}
{selectedSlug === 'download_links' && (
<div className="p-6 rounded-xl border border-outline-variant/30 bg-surface-container-low space-y-4">
<div className="flex items-center justify-between pb-3 border-b border-outline-variant/20">
<div>
<div className="text-xs font-bold text-on-surface">MotorSignal Telemetry</div>
<div className="text-[10px] font-mono text-outline">
Production Version {workingContent.version_name} (Build {workingContent.build_number})
</div>
</div>
</div>
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between p-2 rounded bg-surface-container">
<span className="font-mono text-on-surface-variant">Android Direct APK</span>
<span className="text-primary-container font-medium truncate max-w-xs">
{workingContent.android_url}
</span>
</div>
<div className="flex items-center justify-between p-2 rounded bg-surface-container">
<span className="font-mono text-on-surface-variant">Google Play Store</span>
<span className="text-primary-container font-medium truncate max-w-xs">
{workingContent.play_store_url}
</span>
</div>
{workingContent.testflight_url && (
<div className="flex items-center justify-between p-2 rounded bg-surface-container">
<span className="font-mono text-on-surface-variant">Apple TestFlight</span>
<span className="text-primary-container font-medium truncate max-w-xs">
{workingContent.testflight_url}
</span>
</div>
)}
</div>
</div>
)}
{/* Changelog Preview */}
{selectedSlug === 'changelog' && (
<div className="space-y-4">
{Array.isArray(workingContent.releases) &&
workingContent.releases.map((rel: any, i: number) => (
<div
key={i}
className="p-4 rounded-xl border border-outline-variant/30 bg-surface-container-low space-y-2"
>
<div className="flex items-center justify-between">
<span className="px-2 py-0.5 rounded font-mono text-xs font-bold bg-primary-container/15 text-primary-container">
{rel.version}
</span>
<span className="text-[10px] font-mono text-outline">
{rel.release_date}
</span>
</div>
<h4 className="text-xs font-bold text-on-surface">{rel.title}</h4>
<p className="text-xs text-on-surface-variant leading-relaxed">
{rel.notes}
</p>
</div>
))}
</div>
)}
{/* Policy Pages Preview */}
{selectedSlug === 'policy_pages' && (
<div className="space-y-6">
<div className="p-4 rounded-xl border border-outline-variant/30 bg-surface-container-low space-y-2">
<h4 className="text-xs font-bold text-on-surface font-mono">
Privacy Policy
</h4>
<p className="text-xs text-on-surface-variant leading-relaxed whitespace-pre-wrap">
{workingContent.privacy_policy}
</p>
</div>
<div className="p-4 rounded-xl border border-outline-variant/30 bg-surface-container-low space-y-2">
<h4 className="text-xs font-bold text-on-surface font-mono">
Terms of Service
</h4>
<p className="text-xs text-on-surface-variant leading-relaxed whitespace-pre-wrap">
{workingContent.terms_of_service}
</p>
</div>
</div>
)}
</div>
<div className="p-4 border-t border-outline-variant/20 bg-surface-container-lowest flex justify-end">
<button
type="button"
onClick={() => setIsPreviewOpen(false)}
className="px-4 py-1.5 rounded-lg bg-surface-container hover:bg-surface-container-high text-on-surface text-xs font-medium"
>
Close Preview
</button>
</div>
</div>
</div>
)}
{/* ========================================================================= */}
{/* REVISION HISTORY DRAWER / MODAL */}
{/* ========================================================================= */}
{isHistoryOpen && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="history-modal-title"
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200"
>
<div className="w-full max-w-4xl lg:max-w-5xl max-h-[90vh] flex flex-col rounded-2xl bg-surface-container-lowest border border-outline-variant/40 shadow-2xl overflow-hidden">
<div className="px-6 sm:px-8 py-5 border-b border-outline-variant/20 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-primary-container text-2xl">
history
</span>
<h3 id="history-modal-title" className="text-base sm:text-lg font-bold text-on-surface font-headline-sm">
Revision History: {activeDoc?.title}
</h3>
</div>
<button
type="button"
onClick={() => setIsHistoryOpen(false)}
className="p-1 rounded-lg text-outline hover:text-on-surface transition-colors"
aria-label="Close revision history modal"
>
<span className="material-symbols-outlined text-xl">close</span>
</button>
</div>
<div className="p-6 overflow-y-auto flex-1 space-y-3">
{activeDoc?.revisions && activeDoc.revisions.length > 0 ? (
activeDoc.revisions.map((rev) => (
<div
key={rev.revision_id}
className="p-4 rounded-xl border border-outline-variant/20 bg-surface-container-low flex flex-col sm:flex-row sm:items-center justify-between gap-3"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="font-mono text-xs font-semibold text-on-surface">
{rev.revision_id}
</span>
{rev.status === 'published' ? (
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono capitalize bg-emerald-500/15 text-emerald-400 border border-emerald-500/30">
published
</span>
) : (
<span className="px-1.5 py-0.5 rounded text-[9px] font-mono capitalize bg-amber-500/15 text-amber-400 border border-amber-500/30">
draft
</span>
)}
</div>
<div className="text-xs text-on-surface-variant font-medium">
{rev.change_summary}
</div>
<div className="text-[10px] font-mono text-outline flex items-center gap-2">
<span>{new Date(rev.created_at).toLocaleString()}</span>
<span>•</span>
<span>{rev.created_by}</span>
</div>
</div>
<button
type="button"
onClick={() => {
setSelectedRevisionForRollback(rev);
setRollbackReason('');
setRollbackError(null);
}}
className="self-start sm:self-center px-3 py-1.5 rounded-lg border border-outline-variant/40 bg-surface-container hover:bg-surface-container-high text-xs font-semibold text-on-surface transition-colors shrink-0"
>
Restore Revision
</button>
</div>
))
) : (
<div className="p-8 text-center text-xs font-mono text-outline">
No historical revisions found for this document.
</div>
)}
</div>
<div className="p-4 border-t border-outline-variant/20 bg-surface-container-lowest flex justify-end">
<button
type="button"
onClick={() => setIsHistoryOpen(false)}
className="px-4 py-1.5 rounded-lg bg-surface-container hover:bg-surface-container-high text-on-surface text-xs font-medium"
>
Close History
</button>
</div>
</div>
</div>
)}
{/* ========================================================================= */}
{/* ROLLBACK CONFIRMATION MODAL */}
{/* ========================================================================= */}
{selectedRevisionForRollback && (
<div
role="dialog"
aria-modal="true"
aria-labelledby="rollback-dialog-title"
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200"
>
<div className="w-full max-w-md rounded-2xl bg-surface-container-lowest border border-amber-500/40 shadow-2xl p-6 space-y-4">
<div className="flex items-center gap-2 text-amber-400">
<span className="material-symbols-outlined text-2xl">warning</span>
<h3 id="rollback-dialog-title" className="text-sm font-bold font-headline-sm">
Confirm Content Rollback
</h3>
</div>
<p className="text-xs text-on-surface-variant leading-relaxed">
You are about to restore revision{' '}
<code className="px-1 py-0.5 rounded bg-surface-container font-mono text-on-surface">
{selectedRevisionForRollback.revision_id}
</code>
. This will immediately overwrite current published website content.
</p>
{rollbackError && (
<div className="p-2.5 rounded bg-error-container/20 border border-error-container/40 text-on-error-container text-xs">
{rollbackError}
</div>
)}
<div>
<label
htmlFor="rollback-reason"
className="block text-[11px] font-mono text-outline mb-1.5"
>
Rollback Reason (min 3 characters) *
</label>
<input
id="rollback-reason"
type="text"
value={rollbackReason}
onChange={(e) => setRollbackReason(e.target.value)}
placeholder="e.g. Revert inaccurate 20Hz sensor copy"
className="w-full px-3 py-2 rounded-lg bg-surface-container-low border border-outline-variant/30 text-on-surface text-xs focus:outline-none focus:border-amber-400"
/>
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<button
type="button"
onClick={() => setSelectedRevisionForRollback(null)}
disabled={isRollingBack}
className="px-3 py-1.5 rounded-lg border border-outline-variant/30 text-xs font-medium text-outline hover:text-on-surface"
>
Cancel
</button>
<button
type="button"
onClick={() => void handleRollbackSubmit()}
disabled={isRollingBack || rollbackReason.trim().length < 3}
className="px-4 py-1.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-black text-xs font-bold transition-all disabled:opacity-50"
>
{isRollingBack ? 'Restoring...' : 'Restore Revision'}
</button>
</div>
</div>
</div>
)}
</div>
);
}