Files
git-website-engine-control-…/src/components/ContentTab.tsx
T

439 lines
19 KiB
TypeScript

import React, { useState } from "react";
import {
FileText,
Folder,
Search,
CheckCircle2,
RefreshCw,
Youtube,
ExternalLink,
Code,
Eye,
Tag,
AlertTriangle,
ShieldCheck,
Layers,
Image as ImageIcon,
} from "lucide-react";
import { ContentItem, MediaAsset } from "../types";
interface ContentTabProps {
contentItems: ContentItem[];
mediaAssets: MediaAsset[];
onTriggerDiscovery: () => Promise<void>;
}
export const ContentTab: React.FC<ContentTabProps> = ({
contentItems,
mediaAssets,
onTriggerDiscovery,
}) => {
const [selectedItemId, setSelectedItemId] = useState<string>(
contentItems[0]?.id || "",
);
const [activeSubTab, setActiveSubTab] = useState<"markdown" | "media">(
"markdown",
);
const [searchTerm, setSearchTerm] = useState("");
const [isScanning, setIsScanning] = useState(false);
const [previewContent, setPreviewContent] = useState<ContentItem | null>(
null,
);
const selectedItem =
contentItems.find((i) => i.id === selectedItemId) || contentItems[0];
const filteredItems = contentItems.filter(
(item) =>
item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.sourceRepo.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.path.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.tags.some((t) => t.toLowerCase().includes(searchTerm.toLowerCase())),
);
const handleScan = async () => {
setIsScanning(true);
await onTriggerDiscovery();
setIsScanning(false);
};
return (
<div className="space-y-6">
{/* HEADER BAR */}
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div className="flex items-center space-x-2">
<h1 className="text-xl font-serif italic text-white">Artifacts</h1>
<span className="px-2.5 py-0.5 rounded-xs text-[10px] uppercase font-bold tracking-widest bg-emerald-950/40 text-emerald-400 border border-emerald-500/20">
PRD AC-05, AC-06, AC-07, AC-08 & AC-09
</span>
</div>
<p className="text-xs text-neutral-400 mt-1">
Discovers artifacts across repositories, parses front matter,
validates YouTube directives, Wikipedia links, raw HTML policy, and
media mappings under{" "}
<code className="text-[#C5A059] font-mono">/media</code>.
</p>
</div>
<div className="flex items-center space-x-3">
<button
onClick={handleScan}
disabled={isScanning}
className="px-4 py-2 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-all flex items-center space-x-1.5 uppercase tracking-wider"
>
<RefreshCw
className={`w-3.5 h-3.5 ${isScanning ? "animate-spin" : ""}`}
/>
<span>{isScanning ? "Scanning..." : "Run Discovery Scan"}</span>
</button>
</div>
</div>
{/* SUB-TABS: MARKDOWN VS MEDIA */}
<div className="flex border-b border-[#1F1F1F] space-x-6 text-xs font-semibold">
<button
onClick={() => setActiveSubTab("markdown")}
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 uppercase tracking-wider ${
activeSubTab === "markdown"
? "border-[#C5A059] text-[#C5A059]"
: "border-transparent text-neutral-400 hover:text-white"
}`}
>
<FileText className="w-4 h-4" />
<span>Artifacts ({contentItems.length})</span>
</button>
<button
onClick={() => setActiveSubTab("media")}
className={`pb-3 transition-colors flex items-center space-x-2 border-b-2 uppercase tracking-wider ${
activeSubTab === "media"
? "border-[#C5A059] text-[#C5A059]"
: "border-transparent text-neutral-400 hover:text-white"
}`}
>
<ImageIcon className="w-4 h-4" />
<span>Public Media Assets ({mediaAssets.length})</span>
</button>
</div>
{activeSubTab === "markdown" ? (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* LEFT LIST: DISCOVERED CONTENT ITEMS (5 COLS) */}
<div className="lg:col-span-5 space-y-3">
<div className="relative">
<Search className="w-4 h-4 text-neutral-500 absolute left-3 top-2.5" />
<input
type="text"
placeholder="Search artifacts, tags, repositories..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-3 py-2 bg-[#141414] border border-[#222222] rounded-sm text-xs text-white focus:outline-none focus:border-[#C5A059] shadow-xs"
/>
</div>
<div className="space-y-3">
{filteredItems.map((item) => {
const isSelected = item.id === selectedItemId;
return (
<div
key={item.id}
onClick={() => setSelectedItemId(item.id)}
className={`p-4 rounded-sm border cursor-pointer transition-all ${
isSelected
? "bg-[#181818] border-[#C5A059] shadow-xs"
: "bg-[#111111] border-[#1F1F1F] hover:border-[#2A2A2A]"
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-mono font-semibold text-[#C5A059]">
{item.sourceRepo}
</span>
<span className="text-[10px] text-neutral-500 font-mono">
{item.published}
</span>
</div>
<h3 className="text-sm font-bold text-white line-clamp-1 mb-1">
{item.title}
</h3>
<p className="text-xs text-neutral-400 line-clamp-2 mb-3">
{item.summary}
</p>
<div className="flex items-center justify-between text-[11px] pt-2 border-t border-[#1F1F1F] text-neutral-500">
<span className="font-mono text-neutral-300 truncate max-w-[200px]">
{item.route}
</span>
<span className="flex items-center space-x-1 text-emerald-400 font-medium">
<CheckCircle2 className="w-3 h-3" />
<span>Valid</span>
</span>
</div>
</div>
);
})}
</div>
</div>
{/* RIGHT COLUMN: CONTENT INSPECTOR & DIRECTIVES (7 COLS) */}
<div className="lg:col-span-7 bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-6">
{selectedItem ? (
<>
{/* ITEM HEADER */}
<div className="border-b border-[#1F1F1F] pb-4 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<div className="flex items-center space-x-2 mb-1">
<span className="px-2 py-0.5 text-[10px] font-semibold bg-[#1A1A1A] text-[#C5A059] border border-[#2A2A2A] rounded-xs font-mono">
{selectedItem.sourceRepo}
</span>
<span className="text-xs text-neutral-500 font-mono">
{selectedItem.path}
</span>
</div>
<h2 className="text-lg font-bold text-white">
{selectedItem.title}
</h2>
</div>
<button
onClick={() => setPreviewContent(selectedItem)}
className="px-3.5 py-1.5 bg-[#C5A059] hover:bg-[#b38f4a] text-black font-semibold rounded-sm text-xs transition-colors flex items-center space-x-1.5 self-start uppercase tracking-wider"
>
<Eye className="w-3.5 h-3.5" />
<span>View Rendered</span>
</button>
</div>
{/* EXPLAIN WHY / OMITTED STATUS */}
{selectedItem.explainWhy && (
<div className="p-4 bg-amber-950/20 border border-amber-900/40 rounded-sm text-xs">
<h3 className="font-bold text-amber-400 mb-1">
Artifact Omitted
</h3>
<p className="text-neutral-400 font-bold mb-1 mt-2">
Reason:
</p>
<p className="text-neutral-300">
{selectedItem.explainWhy}
</p>
</div>
)}
{/* METADATA & FRONT MATTER INSPECTOR */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
<span className="font-bold text-neutral-300 uppercase tracking-widest block text-[10px]">
Final Generated Route
</span>
<code className="font-mono text-[#C5A059] bg-[#141414] px-2.5 py-1 rounded-xs border border-[#222222] block text-xs truncate">
{selectedItem.route}
</code>
<p className="text-neutral-500 text-[11px]">
Resolved via{" "}
<code className="text-neutral-300 font-mono">
{selectedItem.styleInstanceId}
</code>{" "}
pattern.
</p>
</div>
<div className="p-4 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] space-y-2">
<span className="font-bold text-neutral-300 uppercase tracking-widest block text-[10px]">
Repository Entry Defaults
</span>
<div className="font-mono text-neutral-300 space-y-0.5 text-[11px]">
<div>
Project:{" "}
<strong className="text-white">
{selectedItem.metadata?.projectName}
</strong>
</div>
<div>
Project ID:{" "}
<strong className="text-[#C5A059]">
{selectedItem.metadata?.projectId}
</strong>
</div>
</div>
</div>
</div>
{/* SPECIAL DIRECTIVES DETECTED */}
<div className="space-y-3">
<span className="text-[10px] font-bold text-neutral-400 uppercase tracking-widest block">
Parsed Markdown Extensions & Embeds
</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
{/* YouTube Directive */}
<div className="p-3.5 bg-red-950/20 border border-red-900/40 rounded-sm space-y-1">
<div className="flex items-center space-x-1.5 text-red-400 font-bold font-mono">
<Youtube className="w-4 h-4" />
<span>YouTube Directives (AC-06)</span>
</div>
{selectedItem.youtubeDirectives.length > 0 ? (
selectedItem.youtubeDirectives.map((yt, idx) => (
<div
key={idx}
className="text-[11px] text-red-200 font-mono"
>
Title: "{yt.title}" (ID: {yt.videoId})
</div>
))
) : (
<p className="text-neutral-500 text-[11px]">
No YouTube directives in item.
</p>
)}
</div>
{/* Wikipedia Links */}
<div className="p-3.5 bg-blue-950/20 border border-blue-900/40 rounded-sm space-y-1">
<div className="flex items-center space-x-1.5 text-blue-400 font-bold font-mono">
<ExternalLink className="w-4 h-4" />
<span>Wikipedia Links (AC-07)</span>
</div>
{selectedItem.wikipediaLinks.length > 0 ? (
selectedItem.wikipediaLinks.map((wiki, idx) => (
<div
key={idx}
className="text-[11px] text-blue-200 font-mono truncate"
>
{wiki.text} -&gt; {wiki.url}
</div>
))
) : (
<p className="text-neutral-500 text-[11px]">
No Wikipedia links in item.
</p>
)}
</div>
</div>
</div>
{/* RAW HTML POLICY SAFETY STATUS */}
<div className="p-4 bg-[#0A0A0A] text-neutral-300 rounded-sm text-xs space-y-2 border border-[#1F1F1F] font-mono">
<div className="flex items-center justify-between">
<span className="font-bold text-[#C5A059] uppercase tracking-widest text-[10px]">
Raw HTML Security Policy (AC-08)
</span>
<span className="px-2 py-0.5 bg-emerald-950/40 text-emerald-400 rounded-xs font-bold text-[10px] border border-emerald-500/20 uppercase">
Policy: Disabled / Sanitized
</span>
</div>
<p className="text-neutral-400 text-[11px]">
Raw HTML blocks and arbitrary script tags are
stripped/escaped automatically during build time to prevent
XSS vulnerabilities.
</p>
</div>
</>
) : (
<div className="text-center py-12 text-neutral-500 text-xs">
Select an artifact to inspect details.
</div>
)}
</div>
</div>
) : (
/* PUBLIC MEDIA ASSETS INVENTORY (AC-09) */
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] p-6 shadow-xs space-y-4">
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
<div>
<h2 className="text-base font-serif italic text-white">
Public Media Assets Namespace Mappings (/media)
</h2>
<p className="text-xs text-neutral-400 mt-0.5">
Maps source media directories into isolated public media
namespaces to prevent path collisions.
</p>
</div>
<span className="px-3 py-1 bg-amber-950/40 text-amber-400 rounded-xs text-xs font-semibold border border-amber-500/20 uppercase tracking-widest">
AC-09 Verified
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
{mediaAssets.map((asset) => (
<div
key={asset.id}
className="p-4 rounded-sm border border-[#1F1F1F] bg-[#0A0A0A] space-y-2 font-mono"
>
<div className="flex items-center justify-between text-[#C5A059] font-bold">
<span>{asset.filename}</span>
<span className="text-[10px] text-neutral-500">
{(asset.sizeBytes / 1024).toFixed(1)} KB
</span>
</div>
<div className="text-[11px] text-neutral-400 space-y-1">
<div>
Source Repo:{" "}
<strong className="text-white">{asset.sourceRepo}</strong>
</div>
<div>
Original Path:{" "}
<code className="text-neutral-300">
{asset.originalPath}
</code>
</div>
<div className="pt-2 border-t border-[#1F1F1F]">
<span className="text-neutral-500 block text-[10px] uppercase">
Public Namespace Route:
</span>
<code className="text-[#C5A059] font-semibold text-[11px] block truncate">
{asset.publicNamespacePath}
</code>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* RENDERED CONTENT PREVIEW MODAL */}
{previewContent && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-xs z-50 flex items-center justify-center p-4">
<div className="bg-[#111111] rounded-sm border border-[#1F1F1F] max-w-2xl w-full p-6 shadow-2xl space-y-4 max-h-[85vh] overflow-y-auto">
<div className="flex items-center justify-between border-b border-[#1F1F1F] pb-3">
<div>
<span className="text-xs font-mono text-[#C5A059] font-bold">
{previewContent.sourceRepo} &bull; {previewContent.published}
</span>
<h3 className="text-lg font-bold text-white">
{previewContent.title}
</h3>
</div>
<button
onClick={() => setPreviewContent(null)}
className="text-neutral-500 hover:text-white text-2xl leading-none"
>
&times;
</button>
</div>
<div className="prose prose-invert max-w-none text-xs text-neutral-200 leading-relaxed font-sans space-y-3">
<div className="p-3 bg-[#0A0A0A] rounded-sm border border-[#1F1F1F] font-mono text-[11px] text-neutral-400">
Summary: {previewContent.summary}
</div>
<pre className="bg-[#0A0A0A] text-neutral-200 p-4 rounded-sm text-[11px] font-mono border border-[#1F1F1F] overflow-x-auto">
<code>{previewContent.contentMarkdown}</code>
</pre>
</div>
<div className="flex items-center justify-end pt-3 border-t border-[#1F1F1F]">
<button
onClick={() => setPreviewContent(null)}
className="px-4 py-2 bg-[#1A1A1A] hover:bg-[#262626] text-white rounded-sm text-xs font-semibold uppercase tracking-wider"
>
Close Preview
</button>
</div>
</div>
</div>
)}
</div>
);
};