Files
flux-srl/src/app/hq-command/dashboard/news/page.tsx
T
davidherran 014a9eb094
Deploy to VPS / deploy (push) Has been cancelled
refactor: unified AssetBucketBrowser replaces 4 inline AssetManagers
Every HQ Command panel had its own ~210-line AssetManager component
copy-pasted into the page file. Same UI, same API, four diverging
implementations — and no consistent metaphor for "where does this file
go?". Editors had to think about subfolder names (videos/, renders/)
that the front-end implicitly expects.

ONE COMPONENT. CLEAR BUCKETS. SAME PATHS.

src/components/hq/AssetBucketBrowser.tsx — the single picker. Takes
scope + slug, shows bucket tabs (Media / Videos / Renders / etc.) and
maps each to the on-disk path the public site already reads from:

  cases:        Media (root) | Videos (/videos) | Renders (/renders)
  applications: Media (root) | Videos (/videos) | Renders (/renders)
  news:         Media (root)
  parts:        Media (root) | Renders (/renders)
  footage:      Hero Reel (root)
  branding:     Brand Assets (root)

Drop a file into the Videos tab → POSTs to /api/assets with path=videos
→ lands at /public/{scope}/{slug}/videos/<file> — exactly where
ApplicationClient.tsx and CaseStudyModal.tsx already look. Zero
front-end path changes, zero data migration.

UX upgrades the editor sees:
- Tabs make the bucket layout discoverable instead of buried in folder
  navigation. Each tab has its own description and accent colour.
- Soft-warning hints flag obvious mismatches ("Videos bucket usually
  holds .mp4 — this file may not display correctly") without blocking
  the upload.
- Search + grid/list views.
- Hover actions per file: copy URL, delete (with confirm).
- Persistent on-screen path (/{scope}/{slug}/{bucket}) so editors can
  always see the canonical location.

REPLACEMENT (4 page files)
- network/page.tsx:      719 → 513 lines (-206) — direct alias
- news/page.tsx:         484 → 313 lines (-171) — direct alias
- applications/page.tsx: 555 → 433 lines (-122) — adapter wraps the
  picker's onSelect into the markdown-syntax onInsert callback this
  panel uses. No call-site changes.
- parts/page.tsx:        413 → 320 lines  (-93) — direct alias

Net: -592 lines of duplicated UI, +560 lines of single shared component.
Future bucket-layout changes live in one file instead of four.

NO PATH/API CHANGES — /api/assets is unchanged. The on-disk layout is
unchanged. Existing assets keep rendering on the public site. Existing
DB rows (mediaFileName, galleryJson, videosJson, rendersJson) are
unaffected because we never moved files.
2026-05-05 08:55:20 -05:00

314 lines
27 KiB
TypeScript

//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";
// AssetBucketBrowser is the unified picker — single source of truth across HQ.
// Aliased to AssetManager so existing JSX call sites remain untouched.
import AssetBucketBrowser from "@/components/hq/AssetBucketBrowser";
const AssetManager = AssetBucketBrowser;
// ─────────────────────────────────────────────────────────────────────────────
// 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>
);
}