feat(applications): drag-to-reorder on the public site
Editors can now control the order applications appear in, the same way they already reorder Hero slides. - Application gains an `order Int @default(0)` column (additive migration 20260605120000_add_application_order, IF NOT EXISTS, safe for deploy) plus an (isActive, order) index. - New reorderApplications(orderedSlugs) server action — single $transaction renumbering, mirrors reorderHeroSlides. - HQ applications panel: rows are now draggable by a grip handle (HTML5 DnD, optimistic local reorder, persisted on drop, toast feedback). - All public-facing queries now order by [order asc, createdAt asc]: home ApplicationsDashboard + GlobalOperations, the footer apps list, and the HQ list itself. Existing rows default to 0 so current order is preserved until the editor drags something. Verified: production build compiles, TypeScript clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fbfffb28d9
commit
afcaf991b5
6 files changed
+72
-10
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user