//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(null); const [value, setValue] = useState(defaultValue); const [isExpanded, setIsExpanded] = useState(false); const [showInsertMenu, setShowInsertMenu] = useState(false); const [history, setHistory] = useState([defaultValue]); const [historyIndex, setHistoryIndex] = useState(0); const insertMenuRef = useRef(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(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) => { 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 }) => ( ); const Divider = () =>
; return (
{showInsertMenu && (