|
|
|
@@ -6,10 +6,10 @@ 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
|
|
|
|
|
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 } from "./actions";
|
|
|
|
|
import { getApplications, createApplication, updateApplicationData, toggleApplication, deleteApplication, reorderApplications } from "./actions";
|
|
|
|
|
import { useHqUi } from "@/components/hq/Toast";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -258,9 +258,29 @@ export default function ApplicationsManager() {
|
|
|
|
|
const [sections, setSections] = useState<any[]>([]);
|
|
|
|
|
const [dashboardMetrics, setDashboardMetrics] = useState<any[]>([]);
|
|
|
|
|
|
|
|
|
|
const [draggedSlug, setDraggedSlug] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const fetchApps = async () => { setIsLoading(true); const res = await getApplications(); if (res.success && res.apps) setApps(res.apps); setIsLoading(false); };
|
|
|
|
|
useEffect(() => { fetchApps(); }, []);
|
|
|
|
|
|
|
|
|
|
// Drag-to-reorder — same pattern as the Hero panel. Optimistic local
|
|
|
|
|
// reorder, then persist the new order to the DB.
|
|
|
|
|
const onDropApp = async (targetSlug: string) => {
|
|
|
|
|
if (!draggedSlug || draggedSlug === targetSlug) return;
|
|
|
|
|
const slugs = apps.map((a) => a.slug);
|
|
|
|
|
const from = slugs.indexOf(draggedSlug);
|
|
|
|
|
const to = slugs.indexOf(targetSlug);
|
|
|
|
|
if (from < 0 || to < 0) return;
|
|
|
|
|
const reordered = [...slugs];
|
|
|
|
|
reordered.splice(from, 1);
|
|
|
|
|
reordered.splice(to, 0, draggedSlug);
|
|
|
|
|
setApps((prev) => reordered.map((s) => prev.find((a) => a.slug === s)!));
|
|
|
|
|
setDraggedSlug(null);
|
|
|
|
|
const res = await reorderApplications(reordered);
|
|
|
|
|
if (res?.error) { ui.toast(res.error, "error"); fetchApps(); }
|
|
|
|
|
else ui.toast("Order updated.", "success");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openEditModal = (app: any) => {
|
|
|
|
|
setEditingApp(app); setActiveTab("basic");
|
|
|
|
|
try { setAdvantages(JSON.parse(app.advantagesJson || "[]")); } catch { setAdvantages([]); }
|
|
|
|
@@ -299,7 +319,7 @@ export default function ApplicationsManager() {
|
|
|
|
|
<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"><Layers className="text-purple-400" /> Knowledge Base</h1>
|
|
|
|
|
<p className="text-[#86868B] mt-2">Manage the technical literature and general specifications of your application categories.</p>
|
|
|
|
|
<p className="text-[#86868B] mt-2">Manage the technical literature and specifications. Drag rows by the handle to reorder how applications appear on the site.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button onClick={() => setIsCreateModalOpen(true)} className="flex items-center gap-2 bg-purple-500/10 text-purple-400 border border-purple-500/20 px-5 py-2.5 rounded-xl font-medium hover:bg-purple-500 hover:text-white transition-all"><Plus size={18} /> New Application</button>
|
|
|
|
|
</div>
|
|
|
|
@@ -308,14 +328,24 @@ export default function ApplicationsManager() {
|
|
|
|
|
<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">Application Sector</th><th className="p-6 font-semibold">Status</th><th className="p-6 font-semibold">Visibility</th><th className="p-6 font-semibold text-right">Actions</th></tr></thead>
|
|
|
|
|
<thead><tr className="border-b border-white/5 text-[10px] uppercase tracking-widest text-[#86868B] bg-black/40"><th className="p-6 font-semibold w-10"></th><th className="p-6 font-semibold">Application Sector</th><th className="p-6 font-semibold">Status</th><th className="p-6 font-semibold">Visibility</th><th className="p-6 font-semibold text-right">Actions</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<tr><td colSpan={4} className="p-8 text-center text-[#86868B]"><Loader2 className="animate-spin mx-auto mb-2" size={24} /> Loading database...</td></tr>
|
|
|
|
|
<tr><td colSpan={5} className="p-8 text-center text-[#86868B]"><Loader2 className="animate-spin mx-auto mb-2" size={24} /> Loading database...</td></tr>
|
|
|
|
|
) : apps.length === 0 ? (
|
|
|
|
|
<tr><td colSpan={5} className="p-8 text-center text-[#86868B]">No applications yet.</td></tr>
|
|
|
|
|
) : apps.map((app) => {
|
|
|
|
|
const isPopulated = app.heroDescription && app.heroDescription.length > 10;
|
|
|
|
|
return (
|
|
|
|
|
<tr key={app.slug} className={`border-b border-white/5 transition-colors group ${app.isActive ? 'hover:bg-white/[0.02]' : 'opacity-50'}`}>
|
|
|
|
|
<tr
|
|
|
|
|
key={app.slug}
|
|
|
|
|
draggable
|
|
|
|
|
onDragStart={() => setDraggedSlug(app.slug)}
|
|
|
|
|
onDragOver={(e) => e.preventDefault()}
|
|
|
|
|
onDrop={() => onDropApp(app.slug)}
|
|
|
|
|
className={`border-b border-white/5 transition-colors group ${draggedSlug === app.slug ? 'opacity-40' : ''} ${app.isActive ? 'hover:bg-white/[0.02]' : 'opacity-50'}`}
|
|
|
|
|
>
|
|
|
|
|
<td className="p-6"><span className="cursor-grab text-[#86868B] hover:text-white inline-flex" title="Drag to reorder"><GripVertical size={16} /></span></td>
|
|
|
|
|
<td className="p-6"><p className="font-medium text-white">{app.title}</p><p className="text-xs text-[#86868B] font-mono mt-1">/{app.slug}</p></td>
|
|
|
|
|
<td className="p-6">{isPopulated ? <span className="bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-3 py-1 rounded-full text-[10px] uppercase tracking-wider flex items-center gap-1.5 w-fit font-semibold"><CheckCircle2 size={12} /> Populated</span> : <span className="bg-white/5 text-[#86868B] border border-white/10 px-3 py-1 rounded-full text-[10px] uppercase tracking-wider flex items-center gap-1.5 w-fit"><AlertCircle size={12} /> Pending Setup</span>}</td>
|
|
|
|
|
<td className="p-6"><button onClick={() => {toggleApplication(app.slug, app.isActive); fetchApps();}} className={`text-xs font-medium px-3 py-1 rounded-full flex items-center gap-1.5 transition-colors ${app.isActive ? 'bg-purple-500/10 text-purple-400' : 'bg-red-500/10 text-red-400'}`}>{app.isActive ? <><Eye size={12} /> Visible</> : <><EyeOff size={12} /> Hidden</>}</button></td>
|
|
|
|
|