production: docker + nginx config for rf-flux.com
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
2026-03-20 13:46:05 -05:00
parent b275b19f08
commit fc24313f15
187 changed files with 20977 additions and 767 deletions
+485
View File
@@ -0,0 +1,485 @@
//src/app/hq-command/dashboard/news/page.tsx
"use client";
export const dynamic = "force-dynamic";
import { useState, useEffect, useRef, useCallback } from "react";
import Link from "next/link";
import {
ArrowLeft, Newspaper, Plus, Trash2, Loader2, X, Linkedin, Edit3, Sparkles,
Bold, Italic, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Table, Minus,
RotateCcw, RotateCw, Maximize2, Minimize2, ChevronDown,
FolderOpen, Upload, FolderPlus, ChevronRight, File, ArrowUpFromLine,
Grid3X3, LayoutList, Copy, Check, Image as ImageIcon, Video, Box, Search
} from "lucide-react";
import { getNewsArticles, createNewsArticle, updateNewsArticle, deleteNewsArticle } from "./actions";
// ─────────────────────────────────────────────────────────────────────────────
// ASSET MANAGER — Reusable file browser (same as Network but for news scope)
// ─────────────────────────────────────────────────────────────────────────────
interface AssetItem {
name: string; type: "file" | "folder"; mediaType?: string; extension?: string;
path: string; publicUrl?: string; size?: string; childCount?: number;
}
interface AssetManagerProps {
slug: string; scope?: string; isOpen: boolean; onClose: () => void;
onSelect: (item: { name: string; publicUrl: string; mediaType: string; path: string }) => void;
accentColor?: string; initialPath?: string;
}
function AssetManager({ slug, scope = "news", isOpen, onClose, onSelect, accentColor = "#00F0FF", initialPath = "" }: AssetManagerProps) {
const [currentPath, setCurrentPath] = useState(initialPath);
const [items, setItems] = useState<AssetItem[]>([]);
const [breadcrumbs, setBreadcrumbs] = useState<{name: string; path: string}[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [showNewFolder, setShowNewFolder] = useState(false);
const [newFolderName, setNewFolderName] = useState("");
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [searchQuery, setSearchQuery] = useState("");
const [copiedPath, setCopiedPath] = useState<string | null>(null);
const fetchAssets = useCallback(async (dirPath: string = "") => {
setIsLoading(true); setError(null);
try {
const params = new URLSearchParams({ scope, slug, path: dirPath });
const res = await fetch(`/api/assets?${params}`);
const data = await res.json();
if (data.success) { setItems(data.items); setBreadcrumbs(data.breadcrumbs); setCurrentPath(dirPath); }
else setError(data.error || "Failed to load");
} catch { setError("Connection error — check /api/assets/route.ts"); }
setIsLoading(false);
}, [scope, slug]);
useEffect(() => { if (isOpen) { fetchAssets(initialPath || currentPath); setSearchQuery(""); } }, [isOpen]); // eslint-disable-line
const uploadFile = async (file: File) => {
setIsUploading(true); setUploadProgress(`Uploading ${file.name}...`);
try {
const fd = new FormData();
fd.append("scope", scope); fd.append("slug", slug); fd.append("path", currentPath); fd.append("file", file);
const res = await fetch("/api/assets", { method: "POST", body: fd });
const data = await res.json();
if (data.success) { setUploadProgress("✓ " + data.file.name); await fetchAssets(currentPath); setTimeout(() => setUploadProgress(""), 2000); }
else { setUploadProgress("✗ " + data.error); setTimeout(() => setUploadProgress(""), 4000); }
} catch { setUploadProgress("✗ Failed"); setTimeout(() => setUploadProgress(""), 3000); }
setIsUploading(false);
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files) Array.from(e.target.files).forEach(uploadFile); e.target.value = ""; };
const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); };
const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); };
const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files.length > 0) Array.from(e.dataTransfer.files).forEach(uploadFile); };
const createFolder = async () => {
if (!newFolderName.trim()) return;
try {
const res = await fetch("/api/assets", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scope, slug, folderName: newFolderName, parentPath: currentPath }) });
const data = await res.json();
if (data.success) { setShowNewFolder(false); setNewFolderName(""); await fetchAssets(currentPath); }
else alert(data.error);
} catch { alert("Connection error"); }
};
const deleteFile = async (filePath: string, fileName: string) => {
if (!confirm('Delete "' + fileName + '"?')) return;
try {
const res = await fetch("/api/assets", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scope, slug, filePath }) });
const data = await res.json(); if (data.success) await fetchAssets(currentPath); else alert(data.error);
} catch { alert("Failed"); }
};
const handleSelect = (item: AssetItem, e?: React.MouseEvent) => {
if (e) { e.preventDefault(); e.stopPropagation(); }
if (item.type === "folder") { fetchAssets(item.path); return; }
onSelect({ name: item.name, publicUrl: item.publicUrl || "/" + scope + "/" + slug + "/" + item.path, mediaType: item.mediaType || "unknown", path: item.path });
onClose();
};
const copyPath = (item: AssetItem) => {
navigator.clipboard.writeText(item.publicUrl || "/" + scope + "/" + slug + "/" + item.path);
setCopiedPath(item.path); setTimeout(() => setCopiedPath(null), 1500);
};
const filtered = searchQuery ? items.filter(i => i.name.toLowerCase().includes(searchQuery.toLowerCase())) : items;
const typeBadge = (mt?: string) => {
const s: Record<string, string> = { image: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", video: "bg-blue-500/10 text-blue-400 border-blue-500/20", model: "bg-purple-500/10 text-purple-400 border-purple-500/20" };
return s[mt || ""] || "bg-white/5 text-[#86868B] border-white/10";
};
const renderThumb = (item: AssetItem) => {
if (item.type === "folder") return <div className="w-full h-full flex items-center justify-center bg-white/[0.02]"><FolderOpen size={28} style={{ color: accentColor, opacity: 0.7 }} /></div>;
if (item.mediaType === "image" && item.publicUrl) return <img src={item.publicUrl} alt={item.name} className="w-full h-full object-cover" loading="lazy" />;
if (item.mediaType === "video") return <div className="w-full h-full flex items-center justify-center bg-blue-500/5"><Video size={24} className="text-blue-400/60" /></div>;
return <div className="w-full h-full flex items-center justify-center bg-white/[0.02]"><File size={24} className="text-[#86868B]/50" /></div>;
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[70] flex items-center justify-center p-4 bg-black/85 backdrop-blur-md" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
<div className="bg-[#0D0D0D] border border-white/10 w-full max-w-4xl rounded-[2rem] shadow-2xl flex flex-col max-h-[85vh] relative overflow-hidden" onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} onClick={(e) => e.stopPropagation()}>
{isDragging && (
<div className="absolute inset-0 z-50 border-2 border-dashed rounded-[2rem] flex items-center justify-center backdrop-blur-sm" style={{ borderColor: accentColor + "80", background: accentColor + "15" }}>
<div className="text-center"><ArrowUpFromLine size={48} style={{ color: accentColor }} className="mx-auto mb-3 animate-bounce" /><p style={{ color: accentColor }} className="font-medium text-lg">Drop files to upload</p></div>
</div>
)}
<div className="px-6 py-5 border-b border-white/10 shrink-0">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3"><div className="p-2 rounded-xl" style={{ background: accentColor + "20", color: accentColor }}><FolderOpen size={20} /></div><div><h3 className="text-lg font-medium text-white">Asset Manager</h3><p className="text-[10px] text-[#86868B] uppercase tracking-widest font-mono">/public/{scope}/{slug}/</p></div></div>
<button onClick={onClose} className="text-[#86868B] hover:text-white p-2 hover:bg-white/5 rounded-xl"><X size={20} /></button>
</div>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-1 text-sm overflow-x-auto [scrollbar-width:none] flex-1 min-w-0">
{breadcrumbs.map((crumb, idx) => (<span key={idx} className="flex items-center shrink-0">{idx > 0 && <ChevronRight size={12} className="text-[#86868B]/50 mx-0.5" />}<button onClick={() => fetchAssets(crumb.path)} className={`px-2 py-1 rounded-lg text-xs ${idx === breadcrumbs.length - 1 ? "bg-white/10" : "text-[#86868B] hover:text-white hover:bg-white/5"}`} style={idx === breadcrumbs.length - 1 ? { color: accentColor } : {}}>{crumb.name}</button></span>))}
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="relative"><Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#86868B]" /><input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} placeholder="Filter..." className="bg-white/5 border border-white/10 rounded-lg pl-8 pr-3 py-1.5 text-xs text-white w-32 focus:w-48 transition-all outline-none" /></div>
<div className="flex bg-white/5 rounded-lg border border-white/10 overflow-hidden">
<button onClick={() => setViewMode("grid")} className={`p-1.5 ${viewMode === "grid" ? "text-[#00F0FF] bg-white/10" : "text-[#86868B]"}`}><Grid3X3 size={14} /></button>
<button onClick={() => setViewMode("list")} className={`p-1.5 ${viewMode === "list" ? "text-[#00F0FF] bg-white/10" : "text-[#86868B]"}`}><LayoutList size={14} /></button>
</div>
<button onClick={() => setShowNewFolder(!showNewFolder)} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-[#86868B] hover:text-white bg-white/5 border border-white/10 rounded-lg"><FolderPlus size={13} /> Folder</button>
<button onClick={() => fileInputRef.current?.click()} disabled={isUploading} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-black rounded-lg font-medium disabled:opacity-50" style={{ background: accentColor }}><Upload size={13} /> Upload</button>
<input ref={fileInputRef} type="file" multiple accept=".jpg,.jpeg,.png,.webp,.gif,.svg,.avif,.mp4,.webm,.mov,.pdf" onChange={handleFileSelect} className="hidden" />
</div>
</div>
{showNewFolder && (<div className="flex items-center gap-2 mt-3 pt-3 border-t border-white/5"><FolderPlus size={14} style={{ color: accentColor }} /><input value={newFolderName} onChange={e => setNewFolderName(e.target.value)} onKeyDown={e => { if (e.key === "Enter") createFolder(); if (e.key === "Escape") { setShowNewFolder(false); setNewFolderName(""); } }} placeholder="folder-name" autoFocus className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-sm text-white outline-none font-mono" /><button onClick={createFolder} className="px-3 py-1.5 text-xs text-black rounded-lg font-medium" style={{ background: accentColor }}>Create</button><button onClick={() => { setShowNewFolder(false); setNewFolderName(""); }} className="px-3 py-1.5 text-xs text-[#86868B]">Cancel</button></div>)}
{uploadProgress && <div className="mt-3 pt-3 border-t border-white/5 flex items-center gap-2 text-xs">{isUploading && <Loader2 size={12} className="animate-spin" style={{ color: accentColor }} />}<span className={uploadProgress.startsWith("✓") ? "text-emerald-400" : uploadProgress.startsWith("✗") ? "text-red-400" : ""} style={isUploading ? { color: accentColor } : {}}>{uploadProgress}</span></div>}
</div>
<div className="flex-1 overflow-y-auto p-4 [scrollbar-width:none]">
{isLoading ? <div className="flex items-center justify-center py-20"><Loader2 size={24} className="animate-spin" style={{ color: accentColor }} /></div>
: error ? <div className="flex flex-col items-center justify-center py-20 text-center"><X size={32} className="text-red-400/50 mb-3" /><p className="text-red-400/80 text-sm">{error}</p></div>
: filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center"><FolderOpen size={48} className="text-[#86868B]/20 mb-4" />
{searchQuery ? <p className="text-[#86868B] text-sm">No matches</p> : <><p className="text-[#86868B] text-sm mb-2">Empty directory</p><div className="flex gap-2 mt-4">{["images", "gallery"].map(f => (<button key={f} onClick={async () => { await fetch("/api/assets", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scope, slug, folderName: f, parentPath: currentPath }) }); fetchAssets(currentPath); }} className="px-3 py-2 text-xs border rounded-lg" style={{ color: accentColor, borderColor: accentColor + "30" }}>+ {f}/</button>))}</div></>}
</div>
) : viewMode === "grid" ? (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3">
{filtered.map(item => (
<div key={item.path} className="group relative bg-[#111] border border-white/5 rounded-xl overflow-hidden hover:border-white/20 transition-all cursor-pointer" onClick={(e) => handleSelect(item, e)}>
<div className="aspect-square overflow-hidden bg-black/20">{renderThumb(item)}</div>
<div className="p-2"><p className="text-[11px] text-white truncate font-medium">{item.name}</p>
<div className="flex items-center justify-between mt-1">{item.type === "folder" ? <span className="text-[9px] text-[#86868B]">{item.childCount} items</span> : <span className={`text-[9px] px-1.5 py-0.5 rounded border ${typeBadge(item.mediaType)}`}>{item.mediaType}</span>}{item.size && <span className="text-[9px] text-[#86868B]">{item.size}</span>}</div>
</div>
{item.type === "file" && <div className="absolute top-1 right-1 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"><button onClick={e => { e.stopPropagation(); copyPath(item); }} className="p-1.5 bg-black/70 backdrop-blur-sm rounded-lg text-[#86868B] hover:text-white">{copiedPath === item.path ? <Check size={11} className="text-emerald-400" /> : <Copy size={11} />}</button><button onClick={e => { e.stopPropagation(); deleteFile(item.path, item.name); }} className="p-1.5 bg-black/70 backdrop-blur-sm rounded-lg text-[#86868B] hover:text-red-400"><Trash2 size={11} /></button></div>}
</div>
))}
</div>
) : (
<div className="space-y-1">{filtered.map(item => (
<div key={item.path} className="group flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-white/[0.03] cursor-pointer" onClick={(e) => handleSelect(item, e)}>
<div className="w-8 h-8 rounded-lg overflow-hidden shrink-0 border border-white/5">{renderThumb(item)}</div>
<span className="text-sm text-white flex-1 truncate">{item.name}</span>
{item.type === "file" && <span className={`text-[9px] px-2 py-0.5 rounded border shrink-0 ${typeBadge(item.mediaType)}`}>{item.extension}</span>}
{item.type === "folder" && <><span className="text-[9px] text-[#86868B]">{item.childCount} items</span><ChevronRight size={14} className="text-[#86868B]/50" /></>}
{item.size && <span className="text-[10px] text-[#86868B] w-16 text-right shrink-0">{item.size}</span>}
</div>
))}</div>
)}
</div>
<div className="px-6 py-3 border-t border-white/10 flex items-center justify-between text-[10px] text-[#86868B] shrink-0 bg-black/20"><span>{filtered.length} items Click to select</span><span className="font-mono">Drag & drop</span></div>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// MARKDOWN EDITOR — Cyan-themed for News articles
// ─────────────────────────────────────────────────────────────────────────────
function MarkdownEditorCyan({ name, defaultValue = "", required, rows = 12, placeholder, slug }: {
name: string; defaultValue?: string; required?: boolean; rows?: number; placeholder?: string; slug?: string;
}) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [value, setValue] = useState(defaultValue);
const [isExpanded, setIsExpanded] = useState(false);
const [showInsertMenu, setShowInsertMenu] = useState(false);
const [history, setHistory] = useState<string[]>([defaultValue]);
const [historyIndex, setHistoryIndex] = useState(0);
const insertMenuRef = useRef<HTMLDivElement>(null);
const [isAssetManagerOpen, setIsAssetManagerOpen] = useState(false);
useEffect(() => {
const h = (e: MouseEvent) => { if (insertMenuRef.current && !insertMenuRef.current.contains(e.target as Node)) setShowInsertMenu(false); };
document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h);
}, []);
const historyTimeout = useRef<NodeJS.Timeout | null>(null);
const pushHistory = useCallback((v: string) => {
if (historyTimeout.current) clearTimeout(historyTimeout.current);
historyTimeout.current = setTimeout(() => { setHistory(p => [...p.slice(0, historyIndex + 1), v].slice(-50)); setHistoryIndex(p => Math.min(p + 1, 49)); }, 500);
}, [historyIndex]);
const handleChange = (v: string) => { setValue(v); pushHistory(v); };
const undo = () => { if (historyIndex > 0) { const i = historyIndex - 1; setHistoryIndex(i); setValue(history[i]); } };
const redo = () => { if (historyIndex < history.length - 1) { const i = historyIndex + 1; setHistoryIndex(i); setValue(history[i]); } };
const getSelection = () => { const ta = textareaRef.current; if (!ta) return { start: 0, end: 0, selected: "", before: "", after: "" }; return { start: ta.selectionStart, end: ta.selectionEnd, selected: value.substring(ta.selectionStart, ta.selectionEnd), before: value.substring(0, ta.selectionStart), after: value.substring(ta.selectionEnd) }; };
const replaceSelection = (t: string, o?: number) => { const { before, after } = getSelection(); handleChange(before + t + after); const p = o !== undefined ? before.length + o : before.length + t.length; setTimeout(() => { const ta = textareaRef.current; if (ta) { ta.focus(); ta.setSelectionRange(p, p); } }, 0); };
const wrapSelection = (pre: string, suf: string) => { const { selected } = getSelection(); if (selected) replaceSelection(pre + selected + suf); else replaceSelection(pre + "text" + suf, pre.length); };
const insertAtCursor = (t: string, o?: number) => replaceSelection(t, o);
const prependLine = (pre: string) => { const { start, selected } = getSelection(); const ls = value.lastIndexOf('\n', start - 1) + 1; const bef = value.substring(0, ls); const line = selected || value.substring(ls).split('\n')[0]; const aft = value.substring(ls + line.length); handleChange(bef + pre + line + aft); setTimeout(() => { const ta = textareaRef.current; if (ta) { ta.focus(); ta.setSelectionRange(ls + pre.length, ls + pre.length + line.length); } }, 0); };
const handleAssetInsert = (item: { publicUrl: string; mediaType: string; name: string }) => {
const syntax = item.mediaType === "image" ? "![" + item.name + "](" + item.publicUrl + ")" : "[" + item.name + "](" + item.publicUrl + ")";
insertAtCursor("\n" + syntax + "\n");
};
const basePath = "/news/" + (slug || "slug");
const actions = {
bold: () => wrapSelection("**", "**"), italic: () => wrapSelection("*", "*"),
h1: () => prependLine("# "), h2: () => prependLine("## "), h3: () => prependLine("### "),
quote: () => prependLine("> "),
ul: () => insertAtCursor("\n- Item 1\n- Item 2\n- Item 3\n", 3),
ol: () => insertAtCursor("\n1. First\n2. Second\n3. Third\n", 4),
hr: () => insertAtCursor("\n---\n"),
table: () => insertAtCursor("\n| Feature | Previous | FLUX Update |\n|---|---|---|\n| Speed | 10 mt/min | 20 mt/min |\n", 2),
image: () => insertAtCursor("\n![Image description](" + basePath + "/image.jpg)\n", 3),
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const m = e.metaKey || e.ctrlKey;
if (m && e.key === 'b') { e.preventDefault(); actions.bold(); }
if (m && e.key === 'i') { e.preventDefault(); actions.italic(); }
if (m && e.key === 'z') { e.preventDefault(); e.shiftKey ? redo() : undo(); }
if (e.key === 'Tab') { e.preventDefault(); insertAtCursor(" "); }
};
const ToolBtn = ({ icon: Icon, label, onClick, className = "" }: { icon: any; label: string; onClick: () => void; className?: string }) => (
<button type="button" onClick={onClick} title={label} className={"p-1.5 rounded-lg text-[#86868B] hover:text-white hover:bg-white/10 transition-all active:scale-90 " + className}><Icon size={15} strokeWidth={2} /></button>
);
const Divider = () => <div className="w-px h-5 bg-white/10 mx-1" />;
return (
<div className={"flex flex-col " + (isExpanded ? "fixed inset-0 z-[60] bg-[#0A0A0A] p-6" : "")}>
<input type="hidden" name={name} value={value} />
<div className={"flex flex-wrap items-center gap-0.5 px-2 py-1.5 bg-black/60 border border-white/10 rounded-t-xl " + (isExpanded ? "border-b-0 rounded-t-2xl" : "")}>
<ToolBtn icon={Bold} label="Bold" onClick={actions.bold} />
<ToolBtn icon={Italic} label="Italic" onClick={actions.italic} />
<Divider />
<ToolBtn icon={Heading1} label="H1" onClick={actions.h1} />
<ToolBtn icon={Heading2} label="H2" onClick={actions.h2} />
<ToolBtn icon={Heading3} label="H3" onClick={actions.h3} />
<Divider />
<ToolBtn icon={List} label="Bullet" onClick={actions.ul} />
<ToolBtn icon={ListOrdered} label="Numbered" onClick={actions.ol} />
<ToolBtn icon={Quote} label="Quote" onClick={actions.quote} />
<ToolBtn icon={Table} label="Table" onClick={actions.table} />
<ToolBtn icon={Minus} label="HR" onClick={actions.hr} />
<Divider />
<div className="relative" ref={insertMenuRef}>
<button type="button" onClick={() => setShowInsertMenu(!showInsertMenu)} className="flex items-center gap-1 px-2 py-1.5 rounded-lg text-[#00F0FF] hover:bg-[#00F0FF]/10 text-[11px] font-semibold uppercase tracking-wider">
<Plus size={13} /> Insert <ChevronDown size={11} className={"transition-transform " + (showInsertMenu ? "rotate-180" : "")} />
</button>
{showInsertMenu && (
<div className="absolute top-full left-0 mt-1 w-52 bg-[#1A1A1A] border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden">
<div className="p-1">
<button type="button" onClick={() => { actions.image(); setShowInsertMenu(false); }} className="w-full flex items-center gap-3 px-3 py-2.5 text-white/80 hover:text-white hover:bg-white/5 rounded-lg text-left"><div className="p-1.5 bg-emerald-500/15 rounded-lg text-emerald-400"><ImageIcon size={14} /></div><div><p className="text-xs font-medium">Image</p><p className="text-[10px] text-[#86868B]">.jpg .png .webp</p></div></button>
<button type="button" onClick={() => { actions.table(); setShowInsertMenu(false); }} className="w-full flex items-center gap-3 px-3 py-2.5 text-white/80 hover:text-white hover:bg-white/5 rounded-lg text-left"><div className="p-1.5 bg-amber-500/15 rounded-lg text-amber-400"><Table size={14} /></div><div><p className="text-xs font-medium">Table</p><p className="text-[10px] text-[#86868B]">Auto-highlight</p></div></button>
</div>
</div>
)}
</div>
{slug && (<><Divider /><button type="button" onClick={() => setIsAssetManagerOpen(true)} className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10 text-[11px] font-semibold uppercase tracking-wider"><FolderOpen size={14} /> Assets</button></>)}
<div className="flex-1" />
<ToolBtn icon={RotateCcw} label="Undo" onClick={undo} className={historyIndex <= 0 ? "opacity-30 pointer-events-none" : ""} />
<ToolBtn icon={RotateCw} label="Redo" onClick={redo} className={historyIndex >= history.length - 1 ? "opacity-30 pointer-events-none" : ""} />
<Divider />
<ToolBtn icon={isExpanded ? Minimize2 : Maximize2} label="Fullscreen" onClick={() => setIsExpanded(!isExpanded)} />
</div>
<textarea ref={textareaRef} value={value} onChange={e => handleChange(e.target.value)} onKeyDown={handleKeyDown} required={required} rows={isExpanded ? 40 : rows} placeholder={placeholder} className={"w-full bg-black/40 border border-white/10 border-t-0 p-4 text-white font-mono text-sm focus:border-[#00F0FF] outline-none resize-none leading-relaxed " + (isExpanded ? "flex-1 rounded-b-2xl text-base leading-loose" : "rounded-b-xl")} style={{ tabSize: 2 }} />
<div className="flex items-center justify-between mt-1.5 px-1">
<div className="flex items-center gap-3 text-[10px] text-[#86868B] font-mono"><span>{value.split('\n').length} lines</span><span className="text-white/10">|</span><span>{value.length} chars</span></div>
<div className="flex items-center gap-2 text-[10px] text-[#86868B]"><span className="opacity-60">B</span><span className="opacity-60">I</span><span className="opacity-60">Tab</span></div>
</div>
{!isExpanded && (
<div className="bg-[#00F0FF]/5 border border-[#00F0FF]/20 rounded-xl p-4 text-xs text-[#86868B] font-mono leading-relaxed mt-3">
<p className="text-[#00F0FF] font-semibold uppercase tracking-widest mb-2 text-[10px]">Markdown Cheat Sheet:</p>
<p><strong># Title 1</strong> &nbsp;|&nbsp; <strong>## Title 2</strong> &nbsp;|&nbsp; <strong>**Bold**</strong></p>
<p className="mt-1"><strong>- List Item</strong> &nbsp;|&nbsp; <strong>&gt; Blockquote</strong></p>
<p className="mt-1 text-white"><strong>![Image](/news/{slug || "slug"}/image.jpg)</strong></p>
<div className="mt-3 pt-3 border-t border-[#00F0FF]/10">
<p><strong>Tables (Last column highlights):</strong></p>
<p>| Feature | Previous | FLUX Update |</p>
<p>|---|---|---|</p>
<p>| Speed | 10 mt/min | 20 mt/min |</p>
</div>
</div>
)}
{slug && <AssetManager scope="news" slug={slug} isOpen={isAssetManagerOpen} onClose={() => setIsAssetManagerOpen(false)} onSelect={handleAssetInsert} accentColor="#00F0FF" />}
</div>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// MAIN PAGE — News Manager (Inside Flux / Editorial Desk)
// ─────────────────────────────────────────────────────────────────────────────
export default function NewsManager() {
const [articles, setArticles] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState("");
const [editingArticle, setEditingArticle] = useState<any | null>(null);
const [gallery, setGallery] = useState<string[]>([]);
// Asset Manager states
const [coverAssetsOpen, setCoverAssetsOpen] = useState(false);
const [galleryAssetsOpen, setGalleryAssetsOpen] = useState(false);
// Derive slug for folder naming
const articleSlug = editingArticle?.title?.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '') || "new-article";
const fetchArticles = async () => {
setIsLoading(true);
const res = await getNewsArticles();
if (res.success && res.articles) setArticles(res.articles);
setIsLoading(false);
};
useEffect(() => { fetchArticles(); }, []);
const openCreateModal = () => { setEditingArticle(null); setGallery([]); setIsModalOpen(true); };
const openEditModal = (article: any) => {
setEditingArticle(article);
try { setGallery(JSON.parse(article.galleryJson || "[]")); } catch { setGallery([]); }
setIsModalOpen(true);
};
const handleSave = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); setIsSubmitting(true); setError("");
const formData = new FormData(e.currentTarget);
formData.append("galleryJson", JSON.stringify(gallery.filter(img => img.trim() !== "")));
let res;
if (editingArticle) res = await updateNewsArticle(formData);
else res = await createNewsArticle(formData);
if (res.error) setError(res.error);
else { setIsModalOpen(false); fetchArticles(); }
setIsSubmitting(false);
};
const handleDelete = async (id: string) => {
if (confirm("Delete this article?")) { await deleteNewsArticle(id); fetchArticles(); }
};
return (
<div className="min-h-screen p-6 md:p-12 max-w-7xl mx-auto">
<div className="mb-10">
<Link href="/hq-command/dashboard" className="inline-flex items-center gap-2 text-sm text-[#86868B] hover:text-[#00F0FF] transition-colors mb-6 group"><ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" /> Back to Command Center</Link>
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6">
<div><h1 className="text-3xl font-light text-white flex items-center gap-3"><Newspaper className="text-[#00F0FF]" /> Inside Flux</h1><p className="text-[#86868B] mt-2">Manage company news, tech updates, and behind-the-scenes articles.</p></div>
<button onClick={openCreateModal} className="flex items-center gap-2 bg-[#00F0FF] text-black px-5 py-2.5 rounded-xl font-medium hover:bg-white transition-all"><Plus size={18} /> Write Article</button>
</div>
</div>
{error && <div className="mb-8 p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400 text-sm">{error}</div>}
<div className="bg-[#111] border border-white/5 rounded-3xl overflow-hidden shadow-2xl"><div className="overflow-x-auto"><table className="w-full text-left border-collapse"><thead><tr className="border-b border-white/5 text-[10px] uppercase tracking-widest text-[#86868B] bg-black/40"><th className="p-6 font-semibold">Article / Date</th><th className="p-6 font-semibold text-center">Order</th><th className="p-6 font-semibold">Category</th><th className="p-6 font-semibold text-right">Actions</th></tr></thead>
<tbody>
{isLoading ? <tr><td colSpan={4} className="p-12 text-center text-[#86868B]"><Loader2 className="animate-spin mx-auto mb-2" size={24} /> Loading editorial database...</td></tr>
: articles.length === 0 ? <tr><td colSpan={4} className="p-12 text-center text-[#86868B]">No articles published yet.</td></tr>
: articles.map(article => (
<tr key={article.id} className="border-b border-white/5 hover:bg-white/[0.02] transition-colors group">
<td className="p-6">
<div className="flex items-center gap-2 mb-1"><p className="font-medium text-white text-base">{article.title}</p>{article.linkedinUrl && <Linkedin size={14} className="text-[#0A66C2]" />}</div>
<p className="text-xs text-[#86868B] max-w-md truncate">{article.excerpt}</p>
<span className="text-[10px] text-white/30 uppercase tracking-widest mt-2 block font-mono">{new Date(article.publishedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
</td>
<td className="p-6 text-center"><span className="text-white/50 bg-white/5 px-3 py-1 rounded font-mono text-sm">{article.order}</span></td>
<td className="p-6"><span className="bg-[#00F0FF]/10 text-[#00F0FF] border border-[#00F0FF]/20 px-3 py-1 rounded-full text-[10px] uppercase tracking-wider">{article.category}</span></td>
<td className="p-6 text-right"><div className="flex justify-end gap-2"><button onClick={() => openEditModal(article)} className="text-[#86868B] hover:text-[#00F0FF] hover:bg-[#00F0FF]/10 p-2 rounded-lg transition-colors opacity-0 group-hover:opacity-100"><Edit3 size={18} /></button><button onClick={() => handleDelete(article.id)} className="text-[#86868B] hover:text-red-400 hover:bg-red-400/10 p-2 rounded-lg transition-colors opacity-0 group-hover:opacity-100"><Trash2 size={18} /></button></div></td>
</tr>
))}
</tbody></table></div></div>
{/* EDITORIAL DESK MODAL */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
<div className="bg-[#111] border border-white/10 w-full max-w-4xl rounded-[2rem] p-0 relative shadow-2xl flex flex-col max-h-[90vh]">
<div className="p-6 md:p-8 border-b border-white/10 relative">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#00F0FF] to-transparent"></div>
<button onClick={() => setIsModalOpen(false)} className="absolute top-6 right-6 text-[#86868B] hover:text-white"><X size={20} /></button>
<h3 className="text-2xl font-light mb-1 text-[#00F0FF] flex items-center gap-2"><Newspaper size={24} /> {editingArticle ? "Edit Article" : "Editorial Desk"}</h3>
</div>
<form id="news-form" onSubmit={handleSave} className="flex-1 overflow-y-auto p-6 md:p-8 [scrollbar-width:none]">
<input type="hidden" name="id" value={editingArticle?.id || ""} />
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="md:col-span-3"><label className="block text-[10px] uppercase tracking-widest text-[#86868B] mb-1.5">Article Title</label><input name="title" defaultValue={editingArticle?.title} required className="w-full bg-black/60 border border-white/10 rounded-xl px-4 py-3 text-white text-lg font-medium focus:border-[#00F0FF] outline-none" /></div>
<div><label className="block text-[10px] uppercase tracking-widest text-[#86868B] mb-1.5">Display Order</label><input name="order" type="number" defaultValue={editingArticle?.order || 0} required className="w-full bg-black/60 border border-white/10 rounded-xl px-4 py-3 text-white text-lg font-medium focus:border-[#00F0FF] outline-none text-center" /></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div><label className="block text-[10px] uppercase tracking-widest text-[#86868B] mb-1.5">Category</label><select name="category" defaultValue={editingArticle?.category} className="w-full bg-black/60 border border-white/10 rounded-xl px-4 py-3 text-white text-sm focus:border-[#00F0FF] outline-none appearance-none"><option value="Inside Flux">Inside Flux</option><option value="Tech Update">Tech Update</option><option value="Event">Event / Tradeshow</option></select></div>
<div>
<label className="block text-[10px] uppercase tracking-widest text-[#86868B] mb-1.5">Cover Image public/news</label>
<div className="flex gap-2">
<input name="coverImage" defaultValue={editingArticle?.coverImage} className="flex-1 bg-black/60 border border-white/10 rounded-xl px-4 py-3 text-[#00F0FF] font-mono text-sm focus:border-[#00F0FF] outline-none" />
<button type="button" onClick={() => setCoverAssetsOpen(true)} className="flex items-center gap-1.5 px-3 py-3 text-xs text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 rounded-xl hover:bg-emerald-500/20 shrink-0"><FolderOpen size={14} /></button>
</div>
</div>
<div><label className="block text-[10px] uppercase tracking-widest text-[#86868B] mb-1.5 flex items-center gap-1"><Linkedin size={10}/> LinkedIn URL</label><input name="linkedinUrl" defaultValue={editingArticle?.linkedinUrl} placeholder="https://linkedin.com/..." className="w-full bg-black/60 border border-white/10 rounded-xl px-4 py-3 text-white text-sm focus:border-[#00F0FF] outline-none" /></div>
</div>
<div><label className="block text-[10px] uppercase tracking-widest text-[#86868B] mb-1.5">Short Excerpt (Summary)</label><textarea name="excerpt" defaultValue={editingArticle?.excerpt} required rows={2} className="w-full bg-black/60 border border-white/10 rounded-xl px-4 py-3 text-white text-sm focus:border-[#00F0FF] outline-none resize-none" /></div>
{/* RICH MARKDOWN EDITOR */}
<div>
<label className="block text-[10px] uppercase tracking-widest text-[#86868B] mb-2 flex justify-between"><span>Full Content (Markdown)</span><span className="text-[#00F0FF]/60 text-[9px] font-normal normal-case">Rich Editor + Assets</span></label>
<MarkdownEditorCyan name="content" defaultValue={editingArticle?.content || ""} required rows={12} placeholder="Write the article here..." slug={articleSlug} />
</div>
{/* MEDIA GALLERY */}
<div className="bg-black/20 border border-white/5 p-4 rounded-xl">
<label className="block text-[10px] uppercase tracking-widest text-[#86868B] mb-3">Media Gallery public/news</label>
<div className="space-y-3 mb-3">
{gallery.map((img, idx) => (
<div key={idx} className="flex gap-2">
<input value={img} onChange={e => { const n = [...gallery]; n[idx] = e.target.value; setGallery(n); }} className="flex-1 bg-black/60 border border-white/10 rounded-lg px-4 py-2 text-[#00F0FF] font-mono text-sm focus:border-[#00F0FF] outline-none" />
<button type="button" onClick={() => setGallery(gallery.filter((_, i) => i !== idx))} className="text-[#86868B] hover:text-red-400 px-3 bg-black/40 rounded-lg"><Trash2 size={14}/></button>
</div>
))}
</div>
<div className="flex gap-2">
<button type="button" onClick={() => setGallery([...gallery, ""])} className="flex-1 border border-dashed border-white/20 text-[#86868B] hover:text-white hover:border-[#00F0FF] py-3 rounded-lg flex justify-center items-center gap-2 text-xs uppercase tracking-widest"><Plus size={14} /> Add Gallery Image</button>
<button type="button" onClick={() => setGalleryAssetsOpen(true)} className="flex items-center gap-1.5 px-4 py-3 text-xs text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 rounded-lg hover:bg-emerald-500/20 font-medium shrink-0"><FolderOpen size={14} /> Browse Assets</button>
</div>
</div>
{/* AI SWITCH */}
<div className="bg-gradient-to-r from-[#00F0FF]/10 to-transparent border border-[#00F0FF]/20 p-4 rounded-xl flex items-center justify-between mt-6">
<div className="flex items-center gap-3"><div className="p-2 bg-[#00F0FF]/20 rounded-lg text-[#00F0FF]"><Sparkles size={18} /></div><div><p className="text-sm text-white font-medium">Flux AI Auto-Translate</p><p className="text-[10px] text-[#86868B] uppercase tracking-widest mt-0.5">IT, VEC, ES, DE</p></div></div>
<label className="relative inline-flex items-center cursor-pointer"><input type="checkbox" name="autoTranslate" defaultChecked className="sr-only peer" /><div className="w-11 h-6 bg-black/50 border border-white/10 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#00F0FF]"></div></label>
</div>
</div>
</form>
{/* Asset Managers OUTSIDE the form */}
<AssetManager scope="news" slug={articleSlug} isOpen={coverAssetsOpen} onClose={() => setCoverAssetsOpen(false)} onSelect={(item) => {
const inp = document.querySelector('input[name="coverImage"]') as HTMLInputElement;
if (inp) { inp.value = item.name; inp.dispatchEvent(new Event('input', { bubbles: true })); }
}} accentColor="#00F0FF" />
<AssetManager scope="news" slug={articleSlug} isOpen={galleryAssetsOpen} onClose={() => setGalleryAssetsOpen(false)} onSelect={(item) => {
setGallery(prev => [...prev, item.name]);
}} accentColor="#00F0FF" />
<div className="p-6 border-t border-white/10 bg-[#111] rounded-b-[2rem] flex justify-end gap-3">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-3 text-sm font-medium text-[#86868B] hover:text-white">Cancel</button>
<button onClick={() => (document.getElementById("news-form") as HTMLFormElement) ?.requestSubmit()} disabled={isSubmitting} className="flex items-center gap-2 bg-[#00F0FF] text-black px-8 py-3 rounded-xl text-sm font-semibold hover:bg-white disabled:opacity-50 shadow-[0_0_15px_rgba(0,240,255,0.3)]">{isSubmitting ? <><Loader2 size={18} className="animate-spin" /> Processing AI...</> : (editingArticle ? "Save & Sync" : "Publish to World")}</button>
</div>
</div>
</div>
)}
</div>
);
}