//src/app/hq-command/dashboard/network/page.tsx "use client"; import { useState, useEffect, useRef, useCallback } from "react"; import Link from "next/link"; import { ArrowLeft, Globe, Plus, Trash2, Loader2, X, MapPin, Eye, EyeOff, Search, BookOpen, Calendar, Image as ImageIcon, Video, Box, Cpu, FileText, 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 } from "lucide-react"; import { getNodes, createNode, deleteNode, toggleNodeStatus, updateNodeCaseStudy, searchSatelliteLocation } from "./actions"; import { getApplications } from "../applications/actions"; // ───────────────────────────────────────────────────────────────────────────── // ASSET MANAGER — Reusable file browser for /public/cases/{slug}/ // ───────────────────────────────────────────────────────────────────────────── 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 = "cases", isOpen, onClose, onSelect, accentColor = "#00F0FF", initialPath = "" }: AssetManagerProps) { const [currentPath, setCurrentPath] = useState(initialPath); const [items, setItems] = useState([]); const [breadcrumbs, setBreadcrumbs] = useState<{name: string; path: string}[]>([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(""); const fileInputRef = useRef(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(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) => { 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 to delete"); } }; 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 = { 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
; if (item.mediaType === "image" && item.publicUrl) return {item.name}; if (item.mediaType === "video") return
; if (item.mediaType === "model") return
; return
; }; if (!isOpen) return null; return (
{ e.preventDefault(); e.stopPropagation(); }}>
e.stopPropagation()}> {isDragging && (

Drop files to upload

to /{scope}/{slug}/{currentPath || "root"}

)}

Asset Manager

/public/{scope}/{slug}/

{breadcrumbs.map((crumb, idx) => ( {idx > 0 && } ))}
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" />
{showNewFolder && (
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" />
)} {uploadProgress &&
{isUploading && }{uploadProgress}
}
{isLoading ?
: error ?

{error}

: filtered.length === 0 ? (
{searchQuery ?

No matches

: ( <>

Empty directory

{["images", "videos", "models", "renders"].map(f => ( ))}
)}
) : viewMode === "grid" ? (
{filtered.map(item => (
handleSelect(item, e)}>
{renderThumb(item)}

{item.name}

{item.type === "folder" ? {item.childCount} items : {item.mediaType}} {item.size && {item.size}}
{item.type === "file" &&
}
))}
) : (
{filtered.map(item => (
handleSelect(item, e)}>
{renderThumb(item)}
{item.name} {item.type === "file" && {item.extension}} {item.type === "folder" && <>{item.childCount} items} {item.size && {item.size}}
))}
)}
{filtered.length} items • Click to selectDrag & drop supported
); } // ───────────────────────────────────────────────────────────────────────────── // MARKDOWN EDITOR — Cyan-themed for Network/Cases // ───────────────────────────────────────────────────────────────────────────── function MarkdownEditorCyan({ name, defaultValue = "", required, rows = 10, 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 }) => { let syntax = ""; switch (item.mediaType) { case "image": syntax = "![" + item.name + "](" + item.publicUrl + ")"; break; case "video": syntax = "[VIDEO:" + item.publicUrl + "]"; break; case "model": syntax = "[3D:" + item.publicUrl + "]"; break; default: syntax = "[" + item.name + "](" + item.publicUrl + ")"; } insertAtCursor("\n" + syntax + "\n"); }; const basePath = "/cases/" + (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| Column A | Column B | Highlight |\n|---|---|---|\n| Data 1 | Data 2 | Value |\n", 2), image: () => insertAtCursor("\n![Image description](" + basePath + "/images/photo.jpg)\n", 3), video: () => insertAtCursor("\n[VIDEO:" + basePath + "/videos/clip.mp4]\n", 8), model3d: () => insertAtCursor("\n[3D:" + basePath + "/models/machine.glb]\n", 5), }; 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 && (