//src/app/hq-command/dashboard/applications/page.ts "use client"; import { useState, useEffect, useRef, useCallback } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowLeft, Layers, DatabaseZap, Loader2, X, CheckCircle2, FileText, LayoutTemplate, AlignLeft, Plus, Trash2, AlertCircle, Eye, EyeOff, Sparkles, Bold, Italic, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Table, Minus, Image as ImageIcon, Video, Box, Type, Code, RotateCcw, RotateCw, Maximize2, Minimize2, ChevronDown, FolderOpen, Upload, FolderPlus, ChevronRight, File, ArrowUpFromLine, Search, Grid3X3, LayoutList, Copy, Check, GripVertical } from "lucide-react"; import { getApplications, createApplication, updateApplicationData, toggleApplication, deleteApplication, reorderApplications } from "./actions"; import { useHqUi } from "@/components/hq/Toast"; // AssetBucketBrowser is the unified picker. The applications page uses an // onInsert(markdownSyntax) callback, so we wrap with an adapter that maps // the picker's onSelect(item) into the markdown syntax the editor expects. import AssetBucketBrowser, { type SelectedAsset } from "@/components/hq/AssetBucketBrowser"; interface AssetManagerProps { slug: string; isOpen: boolean; onClose: () => void; onInsert: (markdownSyntax: string) => void; } function AssetManager({ slug, isOpen, onClose, onInsert }: AssetManagerProps) { const handleSelect = (item: SelectedAsset) => { 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})`; } onInsert(syntax); onClose(); }; return ( ); } // ───────────────────────────────────────────────────────────────────────────── // 🔥 MARKDOWN EDITOR — Rich Toolbar + Asset Manager // ───────────────────────────────────────────────────────────────────────────── interface MarkdownToolbarProps { name: string; defaultValue?: string; required?: boolean; rows?: number; placeholder?: string; slug?: string; } function MarkdownEditor({ name, defaultValue = "", required, rows = 12, placeholder, slug }: MarkdownToolbarProps) { 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 handleClickOutside = (e: MouseEvent) => { if (insertMenuRef.current && !insertMenuRef.current.contains(e.target as Node)) setShowInsertMenu(false); }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const historyTimeout = useRef(null); const pushHistory = useCallback((newValue: string) => { if (historyTimeout.current) clearTimeout(historyTimeout.current); historyTimeout.current = setTimeout(() => { setHistory(prev => [...prev.slice(0, historyIndex + 1), newValue].slice(-50)); setHistoryIndex(prev => Math.min(prev + 1, 49)); }, 500); }, [historyIndex]); const handleChange = (newValue: string) => { setValue(newValue); pushHistory(newValue); }; 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 = (newText: string, cursorOffset?: number) => { const { before, after } = getSelection(); handleChange(before + newText + after); const pos = cursorOffset !== undefined ? before.length + cursorOffset : before.length + newText.length; setTimeout(() => { const ta = textareaRef.current; if (ta) { ta.focus(); ta.setSelectionRange(pos, pos); } }, 0); }; const wrapSelection = (prefix: string, suffix: string) => { const { selected } = getSelection(); if (selected) { replaceSelection(`${prefix}${selected}${suffix}`, prefix.length + selected.length + suffix.length); } else { replaceSelection(`${prefix}text${suffix}`, prefix.length); } }; const insertAtCursor = (text: string, cursorOffset?: number) => { replaceSelection(text, cursorOffset); }; const handleAssetInsert = (markdownSyntax: string) => { insertAtCursor(`\n${markdownSyntax}\n`); }; const prependLine = (prefix: string) => { const { start, selected } = getSelection(); const lineStart = value.lastIndexOf('\n', start - 1) + 1; const before = value.substring(0, lineStart); const currentLine = selected || value.substring(lineStart).split('\n')[0]; const afterLine = value.substring(lineStart + currentLine.length); handleChange(before + prefix + currentLine + afterLine); setTimeout(() => { const ta = textareaRef.current; if (ta) { ta.focus(); ta.setSelectionRange(lineStart + prefix.length, lineStart + prefix.length + currentLine.length); } }, 0); }; 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 1 |\n| Data 3 | Data 4 | Value 2 |\n", 2), image: () => { insertAtCursor(`\n![Image description](/applications/${slug || "your-slug"}/image-name.jpg)\n`, 3); }, video: () => { insertAtCursor(`\n[VIDEO:/applications/${slug || "your-slug"}/videos/video-name.mp4]\n`, 8); }, model3d: () => { insertAtCursor(`\n[3D:/applications/${slug || "your-slug"}/models/model-name.glb]\n`, 5); }, }; const handleKeyDown = (e: React.KeyboardEvent) => { const isMod = e.metaKey || e.ctrlKey; if (isMod && e.key === 'b') { e.preventDefault(); actions.bold(); } if (isMod && e.key === 'i') { e.preventDefault(); actions.italic(); } if (isMod && 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 && (