production: docker + nginx config for rf-flux.com
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
2026-03-20 13:46:05 -05:00
parent b275b19f08
commit fc24313f15
187 changed files with 20977 additions and 767 deletions
@@ -0,0 +1,163 @@
//src/app/hq-command/dashboard/applications/actions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { unstable_noStore as noStore } from "next/cache"; // 🔥 ANTI-CACHÉ
// 🔥 IMPORTAMOS EL TRADUCTOR DE IA
import { translateContentForCMS } from "@/lib/aiTranslator";
const generateSlug = (title: string) => {
return title.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
};
// 1. OBTENER LA LISTA DE APLICACIONES
export async function getApplications() {
noStore();
try {
const apps = await prisma.application.findMany({
orderBy: { createdAt: "asc" }
});
return { success: true, apps };
} catch (error) {
return { error: "Failed to fetch applications." };
}
}
// 2. OBTENER UNA APLICACIÓN ESPECÍFICA
export async function getApplicationBySlug(slug: string) {
try {
const app = await prisma.application.findUnique({
where: { slug }
});
if (!app) return { error: "Application not found." };
return { success: true, app };
} catch (error) {
return { error: "Failed to fetch application details." };
}
}
// 3. CREAR NUEVA APLICACIÓN (¡Ahora con IA!)
export async function createApplication(formData: FormData) {
try {
const title = formData.get("title") as string;
const subtitle = formData.get("subtitle") as string;
const category = formData.get("category") as string;
// Capturamos el switch de IA
const autoTranslate = formData.get("autoTranslate") === "on";
const shortDescription = "New application ready to be configured.";
const slug = generateSlug(title);
let translationsJson = "{}";
// 🔥 LA MAGIA DE LA IA EN LA CREACIÓN 🔥
if (autoTranslate) {
const aiResult = await translateContentForCMS({
title, subtitle, category, shortDescription
});
if (aiResult) translationsJson = JSON.stringify(aiResult);
}
await prisma.application.create({
data: {
slug, title, subtitle, category,
shortDescription,
heroDescription: "", sectionsJson: "[]", advantagesJson: "[]", datasheetJson: "{}", dashboardMetricsJson: "[]",
isActive: true,
translationsJson
}
});
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (error) {
return { error: "Failed to create application. Title might already exist." };
}
}
// 4. ACTUALIZAR TODA LA INFORMACIÓN (¡Traduciendo JSONs completos!)
export async function updateApplicationData(formData: FormData) {
try {
const slug = formData.get("slug") as string;
const title = formData.get("title") as string;
const subtitle = formData.get("subtitle") as string;
const category = formData.get("category") as string;
const shortDescription = formData.get("shortDescription") as string;
const heroDescription = formData.get("heroDescription") as string;
const sectionsJson = formData.get("sectionsJson") as string;
const advantagesJson = formData.get("advantagesJson") as string;
const datasheetJson = formData.get("datasheetJson") as string || "{}";
const dashboardMetricsJson = formData.get("dashboardMetricsJson") as string || "[]";
const autoTranslate = formData.get("autoTranslate") === "on";
let updateData: any = {
title, subtitle, category, shortDescription, heroDescription,
sectionsJson, advantagesJson, datasheetJson, dashboardMetricsJson
};
// 🔥 LA MAGIA DE LA IA PARA EL CONTENIDO PROFUNDO 🔥
// Nota: Le mandamos los JSON stringificados. GPT-4o los traducirá y nos los devolverá con la misma estructura.
if (autoTranslate) {
const aiResult = await translateContentForCMS({
title, subtitle, category, shortDescription, heroDescription,
sectionsJson, advantagesJson, dashboardMetricsJson
});
if (aiResult) {
updateData.translationsJson = JSON.stringify(aiResult);
}
}
await prisma.application.update({
where: { slug },
data: updateData
});
revalidatePath(`/applications/${slug}`);
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (error) {
console.error("Update Error:", error);
return { error: "Failed to update application data." };
}
}
// 5. OCULTAR / MOSTRAR APLICACIÓN
export async function toggleApplication(slug: string, currentStatus: boolean) {
try {
await prisma.application.update({
where: { slug },
data: { isActive: !currentStatus }
});
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (e) {
return { error: "Failed to toggle status." };
}
}
// 6. ELIMINAR APLICACIÓN
export async function deleteApplication(slug: string) {
try {
await prisma.application.delete({ where: { slug } });
revalidatePath("/hq-command/dashboard/applications");
revalidatePath("/[locale]", "layout");
return { success: true };
} catch (e) {
return { error: "Failed to delete application." };
}
}
// 7. LA SEMILLA: AUTO-POBLAR LA BASE DE DATOS (Intacto)
export async function seedInitialApplications() {
// ... Tu código actual de la semilla se queda exactamente igual ...
// (Para mantener este mensaje limpio, asume que la función de seedInitialApplications() que ya tienes va aquí)
}
@@ -0,0 +1,746 @@
//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
} from "lucide-react";
import { getApplications, createApplication, updateApplicationData, toggleApplication, deleteApplication } from "./actions";
// ─────────────────────────────────────────────────────────────────────────────
// 🔥 ASSET MANAGER — File Browser, Upload & Folder Creation
// ─────────────────────────────────────────────────────────────────────────────
// Connects to /api/assets to browse, upload, and organize media files
// within /public/applications/{slug}/
// ─────────────────────────────────────────────────────────────────────────────
interface AssetItem {
name: string;
type: "file" | "folder";
mediaType?: string;
extension?: string;
path: string;
publicUrl?: string;
size?: string;
sizeBytes?: number;
modifiedAt?: string;
childCount?: number;
}
interface AssetManagerProps {
slug: string;
isOpen: boolean;
onClose: () => void;
onInsert: (markdownSyntax: string) => void;
}
function AssetManager({ slug, isOpen, onClose, onInsert }: AssetManagerProps) {
const [currentPath, setCurrentPath] = useState("");
const [items, setItems] = useState<AssetItem[]>([]);
const [breadcrumbs, setBreadcrumbs] = useState<{name: string; path: string}[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState("");
const fileInputRef = useRef<HTMLInputElement>(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<string | null>(null);
const fetchAssets = useCallback(async (dirPath: string = "") => {
setIsLoading(true);
setError(null);
try {
const params = new URLSearchParams({ 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 directory");
}
} catch (err) {
setError("Connection error — make sure /api/assets/route.ts exists.");
}
setIsLoading(false);
}, [slug]);
useEffect(() => {
if (isOpen) { fetchAssets(currentPath); setSearchQuery(""); }
}, [isOpen, fetchAssets]); // eslint-disable-line react-hooks/exhaustive-deps
const navigateTo = (folderPath: string) => { fetchAssets(folderPath); };
const uploadFile = async (file: File) => {
setIsUploading(true);
setUploadProgress(`Uploading ${file.name}...`);
try {
const formData = new FormData();
formData.append("slug", slug);
formData.append("path", currentPath);
formData.append("file", file);
const res = await fetch("/api/assets", { method: "POST", body: formData });
const data = await res.json();
if (data.success) {
setUploadProgress(`${data.file.name} uploaded`);
await fetchAssets(currentPath);
setTimeout(() => setUploadProgress(""), 2000);
} else {
setUploadProgress(`✗ Error: ${data.error}`);
setTimeout(() => setUploadProgress(""), 4000);
}
} catch (err) {
setUploadProgress("✗ Upload failed");
setTimeout(() => setUploadProgress(""), 3000);
}
setIsUploading(false);
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files) Array.from(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);
const files = e.dataTransfer.files;
if (files.length > 0) Array.from(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({ slug, folderName: newFolderName, parentPath: currentPath }),
});
const data = await res.json();
if (data.success) {
setShowNewFolder(false); setNewFolderName(""); await fetchAssets(currentPath);
} else { alert(data.error || "Failed to create folder"); }
} catch { alert("Connection error creating folder"); }
};
const deleteFile = async (filePath: string, fileName: string) => {
if (!confirm(`Delete "${fileName}"? This cannot be undone.`)) return;
try {
const res = await fetch("/api/assets", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug, filePath }),
});
const data = await res.json();
if (data.success) await fetchAssets(currentPath);
else alert(data.error);
} catch { alert("Failed to delete file"); }
};
const insertAsset = (item: AssetItem) => {
if (item.type === "folder") return;
const url = item.publicUrl || `/applications/${slug}/${item.path}`;
let syntax = "";
switch (item.mediaType) {
case "image": syntax = `![${item.name}](${url})`; break;
case "video": syntax = `[VIDEO:${url}]`; break;
case "model": syntax = `[3D:${url}]`; break;
default: syntax = `[${item.name}](${url})`;
}
onInsert(syntax);
onClose();
};
const copyPath = (item: AssetItem) => {
const url = item.publicUrl || `/applications/${slug}/${item.path}`;
navigator.clipboard.writeText(url);
setCopiedPath(item.path);
setTimeout(() => setCopiedPath(null), 1500);
};
const filteredItems = searchQuery
? items.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
: items;
const renderThumbnail = (item: AssetItem) => {
if (item.type === "folder") return <div className="w-full h-full flex items-center justify-center bg-white/[0.02]"><FolderOpen size={28} className="text-purple-400/70" /></div>;
if (item.mediaType === "image" && item.publicUrl) return <img src={item.publicUrl} alt={item.name} className="w-full h-full object-cover" loading="lazy" />;
if (item.mediaType === "video") return <div className="w-full h-full flex items-center justify-center bg-blue-500/5"><Video size={24} className="text-blue-400/60" /></div>;
if (item.mediaType === "model") return <div className="w-full h-full flex items-center justify-center bg-purple-500/5"><Box size={24} className="text-purple-400/60" /></div>;
return <div className="w-full h-full flex items-center justify-center bg-white/[0.02]"><File size={24} className="text-[#86868B]/50" /></div>;
};
const typeBadge = (mediaType?: string) => {
const styles: Record<string, string> = {
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",
document: "bg-amber-500/10 text-amber-400 border-amber-500/20",
};
return styles[mediaType || ""] || "bg-white/5 text-[#86868B] border-white/10";
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[70] flex items-center justify-center p-4 bg-black/85 backdrop-blur-md">
<div className="bg-[#0D0D0D] border border-white/10 w-full max-w-4xl rounded-[2rem] shadow-2xl flex flex-col max-h-[85vh] relative overflow-hidden"
onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop}>
{isDragging && (
<div className="absolute inset-0 z-50 bg-purple-500/10 border-2 border-dashed border-purple-500/50 rounded-[2rem] flex items-center justify-center backdrop-blur-sm">
<div className="text-center">
<ArrowUpFromLine size={48} className="text-purple-400 mx-auto mb-3 animate-bounce" />
<p className="text-purple-400 font-medium text-lg">Drop files to upload</p>
<p className="text-[#86868B] text-sm mt-1">to /applications/{slug}/{currentPath || "root"}</p>
</div>
</div>
)}
{/* Header */}
<div className="px-6 py-5 border-b border-white/10 shrink-0">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-500/15 rounded-xl text-purple-400"><FolderOpen size={20} /></div>
<div>
<h3 className="text-lg font-medium text-white">Asset Manager</h3>
<p className="text-[10px] text-[#86868B] uppercase tracking-widest font-mono">/public/applications/{slug}/</p>
</div>
</div>
<button onClick={onClose} className="text-[#86868B] hover:text-white p-2 hover:bg-white/5 rounded-xl transition-all"><X size={20} /></button>
</div>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-1 text-sm overflow-x-auto [scrollbar-width:none] flex-1 min-w-0">
{breadcrumbs.map((crumb, idx) => (
<span key={idx} className="flex items-center shrink-0">
{idx > 0 && <ChevronRight size={12} className="text-[#86868B]/50 mx-0.5" />}
<button onClick={() => navigateTo(crumb.path)} className={`px-2 py-1 rounded-lg transition-colors text-xs ${idx === breadcrumbs.length - 1 ? "text-purple-400 bg-purple-500/10" : "text-[#86868B] hover:text-white hover:bg-white/5"}`}>{crumb.name}</button>
</span>
))}
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="relative">
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#86868B]" />
<input value={searchQuery} onChange={e => 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 focus:border-purple-500/50" />
</div>
<div className="flex bg-white/5 rounded-lg border border-white/10 overflow-hidden">
<button onClick={() => setViewMode("grid")} className={`p-1.5 transition-colors ${viewMode === "grid" ? "bg-purple-500/20 text-purple-400" : "text-[#86868B] hover:text-white"}`}><Grid3X3 size={14} /></button>
<button onClick={() => setViewMode("list")} className={`p-1.5 transition-colors ${viewMode === "list" ? "bg-purple-500/20 text-purple-400" : "text-[#86868B] hover:text-white"}`}><LayoutList size={14} /></button>
</div>
<button onClick={() => setShowNewFolder(!showNewFolder)} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-[#86868B] hover:text-white bg-white/5 border border-white/10 rounded-lg hover:bg-white/10 transition-all"><FolderPlus size={13} /> Folder</button>
<button onClick={() => fileInputRef.current?.click()} disabled={isUploading} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white bg-purple-500 rounded-lg hover:bg-purple-400 transition-all disabled:opacity-50 font-medium"><Upload size={13} /> Upload</button>
<input ref={fileInputRef} type="file" multiple accept=".jpg,.jpeg,.png,.webp,.gif,.svg,.avif,.mp4,.webm,.mov,.glb,.gltf,.usdz,.pdf" onChange={handleFileSelect} className="hidden" />
</div>
</div>
{showNewFolder && (
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-white/5">
<FolderPlus size={14} className="text-purple-400 shrink-0" />
<input value={newFolderName} onChange={e => setNewFolderName(e.target.value)} onKeyDown={e => { if (e.key === "Enter") createFolder(); if (e.key === "Escape") { setShowNewFolder(false); setNewFolderName(""); } }} placeholder="folder-name (lowercase, hyphens ok)" autoFocus className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-sm text-white focus:border-purple-500 outline-none font-mono" />
<button onClick={createFolder} className="px-3 py-1.5 text-xs bg-purple-500 text-white rounded-lg hover:bg-purple-400 font-medium">Create</button>
<button onClick={() => { setShowNewFolder(false); setNewFolderName(""); }} className="px-3 py-1.5 text-xs text-[#86868B] hover:text-white">Cancel</button>
</div>
)}
{uploadProgress && (
<div className="mt-3 pt-3 border-t border-white/5">
<div className="flex items-center gap-2 text-xs">
{isUploading && <Loader2 size={12} className="animate-spin text-purple-400" />}
<span className={isUploading ? "text-purple-400" : uploadProgress.startsWith("✓") ? "text-emerald-400" : "text-red-400"}>{uploadProgress}</span>
</div>
</div>
)}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 [scrollbar-width:none]">
{isLoading ? (
<div className="flex items-center justify-center py-20"><Loader2 size={24} className="animate-spin text-purple-400" /></div>
) : error ? (
<div className="flex flex-col items-center justify-center py-20 text-center">
<AlertCircle size={32} className="text-red-400/50 mb-3" />
<p className="text-red-400/80 text-sm mb-1">{error}</p>
<p className="text-[#86868B] text-xs">Make sure <code className="bg-white/5 px-1.5 py-0.5 rounded text-purple-400">/api/assets/route.ts</code> exists.</p>
</div>
) : filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center">
<FolderOpen size={48} className="text-[#86868B]/20 mb-4" />
{searchQuery ? (
<p className="text-[#86868B] text-sm">No files matching &quot;{searchQuery}&quot;</p>
) : (
<>
<p className="text-[#86868B] text-sm mb-2">This directory is empty</p>
<p className="text-[#86868B]/60 text-xs">Upload files or create subfolders to organize your assets</p>
<div className="flex gap-2 mt-4">
{["images", "videos", "models"].map(folder => (
<button key={folder} onClick={async () => {
await fetch("/api/assets", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ slug, folderName: folder, parentPath: currentPath }) });
fetchAssets(currentPath);
}} className="px-3 py-2 text-xs text-purple-400 bg-purple-500/10 border border-purple-500/20 rounded-lg hover:bg-purple-500/20 transition-colors">+ {folder}/</button>
))}
</div>
</>
)}
</div>
) : viewMode === "grid" ? (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3">
{filteredItems.map((item) => (
<div key={item.path} className="group relative bg-[#111] border border-white/5 rounded-xl overflow-hidden hover:border-purple-500/30 transition-all cursor-pointer" onClick={() => item.type === "folder" ? navigateTo(item.path) : insertAsset(item)}>
<div className="aspect-square overflow-hidden bg-black/20">{renderThumbnail(item)}</div>
<div className="p-2">
<p className="text-[11px] text-white truncate font-medium">{item.name}</p>
<div className="flex items-center justify-between mt-1">
{item.type === "folder" ? (
<span className="text-[9px] text-[#86868B]">{item.childCount} items</span>
) : (
<span className={`text-[9px] px-1.5 py-0.5 rounded border ${typeBadge(item.mediaType)}`}>{item.mediaType}</span>
)}
{item.size && <span className="text-[9px] text-[#86868B]">{item.size}</span>}
</div>
</div>
{item.type === "file" && (
<div className="absolute top-1 right-1 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={(e) => { e.stopPropagation(); copyPath(item); }} className="p-1.5 bg-black/70 backdrop-blur-sm rounded-lg text-[#86868B] hover:text-white transition-colors" title="Copy path">
{copiedPath === item.path ? <Check size={11} className="text-emerald-400" /> : <Copy size={11} />}
</button>
<button onClick={(e) => { e.stopPropagation(); deleteFile(item.path, item.name); }} className="p-1.5 bg-black/70 backdrop-blur-sm rounded-lg text-[#86868B] hover:text-red-400 transition-colors" title="Delete"><Trash2 size={11} /></button>
</div>
)}
</div>
))}
</div>
) : (
<div className="space-y-1">
{filteredItems.map((item) => (
<div key={item.path} className="group flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-white/[0.03] transition-colors cursor-pointer" onClick={() => item.type === "folder" ? navigateTo(item.path) : insertAsset(item)}>
<div className="w-8 h-8 rounded-lg overflow-hidden shrink-0 border border-white/5">
{item.type === "folder" ? <div className="w-full h-full flex items-center justify-center bg-purple-500/10"><FolderOpen size={14} className="text-purple-400" /></div>
: item.mediaType === "image" && item.publicUrl ? <img src={item.publicUrl} alt="" className="w-full h-full object-cover" loading="lazy" />
: <div className={`w-full h-full flex items-center justify-center ${item.mediaType === "video" ? "bg-blue-500/10" : item.mediaType === "model" ? "bg-purple-500/10" : "bg-white/5"}`}>
{item.mediaType === "video" ? <Video size={12} className="text-blue-400" /> : item.mediaType === "model" ? <Box size={12} className="text-purple-400" /> : <File size={12} className="text-[#86868B]" />}
</div>}
</div>
<span className="text-sm text-white flex-1 truncate">{item.name}</span>
{item.type === "file" && <span className={`text-[9px] px-2 py-0.5 rounded border shrink-0 ${typeBadge(item.mediaType)}`}>{item.extension}</span>}
{item.type === "folder" && <span className="text-[9px] text-[#86868B] shrink-0">{item.childCount} items</span>}
{item.size && <span className="text-[10px] text-[#86868B] w-16 text-right shrink-0">{item.size}</span>}
{item.type === "file" && (
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button onClick={(e) => { e.stopPropagation(); copyPath(item); }} className="p-1.5 rounded-lg text-[#86868B] hover:text-white hover:bg-white/10 transition-colors">{copiedPath === item.path ? <Check size={12} className="text-emerald-400" /> : <Copy size={12} />}</button>
<button onClick={(e) => { e.stopPropagation(); deleteFile(item.path, item.name); }} className="p-1.5 rounded-lg text-[#86868B] hover:text-red-400 hover:bg-red-500/10 transition-colors"><Trash2 size={12} /></button>
</div>
)}
{item.type === "folder" && <ChevronRight size={14} className="text-[#86868B]/50 shrink-0" />}
</div>
))}
</div>
)}
</div>
<div className="px-6 py-3 border-t border-white/10 flex items-center justify-between text-[10px] text-[#86868B] shrink-0 bg-black/20">
<span>{filteredItems.length} items{searchQuery ? " (filtered)" : ""} Click a file to insert into editor</span>
<span className="font-mono">Drag & drop supported</span>
</div>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// 🔥 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<HTMLTextAreaElement>(null);
const [value, setValue] = useState(defaultValue);
const [isExpanded, setIsExpanded] = useState(false);
const [showInsertMenu, setShowInsertMenu] = useState(false);
const [history, setHistory] = useState<string[]>([defaultValue]);
const [historyIndex, setHistoryIndex] = useState(0);
const insertMenuRef = useRef<HTMLDivElement>(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<NodeJS.Timeout | null>(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<HTMLTextAreaElement>) => {
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 }) => (
<button type="button" onClick={onClick} title={label} className={`p-1.5 rounded-lg text-[#86868B] hover:text-white hover:bg-white/10 transition-all active:scale-90 ${className}`}><Icon size={15} strokeWidth={2} /></button>
);
const Divider = () => <div className="w-px h-5 bg-white/10 mx-1" />;
return (
<div className={`flex flex-col ${isExpanded ? "fixed inset-0 z-[60] bg-[#0A0A0A] p-6" : ""}`}>
<input type="hidden" name={name} value={value} />
<div className={`flex flex-wrap items-center gap-0.5 px-2 py-1.5 bg-black/60 border border-white/10 rounded-t-xl ${isExpanded ? "border-b-0 rounded-t-2xl" : ""}`}>
<ToolBtn icon={Bold} label="Bold (⌘B)" onClick={actions.bold} />
<ToolBtn icon={Italic} label="Italic (⌘I)" onClick={actions.italic} />
<Divider />
<ToolBtn icon={Heading1} label="Heading 1" onClick={actions.h1} />
<ToolBtn icon={Heading2} label="Heading 2" onClick={actions.h2} />
<ToolBtn icon={Heading3} label="Heading 3" onClick={actions.h3} />
<Divider />
<ToolBtn icon={List} label="Bullet List" onClick={actions.ul} />
<ToolBtn icon={ListOrdered} label="Numbered List" onClick={actions.ol} />
<ToolBtn icon={Quote} label="Blockquote" onClick={actions.quote} />
<ToolBtn icon={Table} label="Insert Table" onClick={actions.table} />
<ToolBtn icon={Minus} label="Horizontal Rule" onClick={actions.hr} />
<Divider />
<div className="relative" ref={insertMenuRef}>
<button type="button" onClick={() => setShowInsertMenu(!showInsertMenu)} className="flex items-center gap-1 px-2 py-1.5 rounded-lg text-purple-400 hover:text-purple-300 hover:bg-purple-500/10 transition-all text-[11px] font-semibold uppercase tracking-wider">
<Plus size={13} /> Insert <ChevronDown size={11} className={`transition-transform ${showInsertMenu ? "rotate-180" : ""}`} />
</button>
{showInsertMenu && (
<div className="absolute top-full left-0 mt-1 w-56 bg-[#1A1A1A] border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-150">
<div className="p-1">
<button type="button" onClick={() => { actions.image(); setShowInsertMenu(false); }} className="w-full flex items-center gap-3 px-3 py-2.5 text-sm text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors text-left">
<div className="p-1.5 bg-emerald-500/15 rounded-lg text-emerald-400"><ImageIcon size={14} /></div>
<div><p className="text-xs font-medium">Image</p><p className="text-[10px] text-[#86868B]">.jpg .png .webp</p></div>
</button>
<button type="button" onClick={() => { actions.video(); setShowInsertMenu(false); }} className="w-full flex items-center gap-3 px-3 py-2.5 text-sm text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors text-left">
<div className="p-1.5 bg-blue-500/15 rounded-lg text-blue-400"><Video size={14} /></div>
<div><p className="text-xs font-medium">Video</p><p className="text-[10px] text-[#86868B]">.mp4 local file</p></div>
</button>
<button type="button" onClick={() => { actions.model3d(); setShowInsertMenu(false); }} className="w-full flex items-center gap-3 px-3 py-2.5 text-sm text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors text-left">
<div className="p-1.5 bg-purple-500/15 rounded-lg text-purple-400"><Box size={14} /></div>
<div><p className="text-xs font-medium">3D Model</p><p className="text-[10px] text-[#86868B]">.glb / .usdz (AR)</p></div>
</button>
<div className="border-t border-white/5 mt-1 pt-1">
<button type="button" onClick={() => { actions.table(); setShowInsertMenu(false); }} className="w-full flex items-center gap-3 px-3 py-2.5 text-sm text-white/80 hover:text-white hover:bg-white/5 rounded-lg transition-colors text-left">
<div className="p-1.5 bg-amber-500/15 rounded-lg text-amber-400"><Table size={14} /></div>
<div><p className="text-xs font-medium">Data Table</p><p className="text-[10px] text-[#86868B]">Auto-highlighted last column</p></div>
</button>
</div>
</div>
</div>
)}
</div>
{slug && (<><Divider /><button type="button" onClick={() => setIsAssetManagerOpen(true)} className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10 transition-all text-[11px] font-semibold uppercase tracking-wider" title="Browse & upload media files"><FolderOpen size={14} /> Assets</button></>)}
<div className="flex-1" />
<ToolBtn icon={RotateCcw} label="Undo (⌘Z)" onClick={undo} className={historyIndex <= 0 ? "opacity-30 pointer-events-none" : ""} />
<ToolBtn icon={RotateCw} label="Redo (⌘⇧Z)" onClick={redo} className={historyIndex >= history.length - 1 ? "opacity-30 pointer-events-none" : ""} />
<Divider />
<ToolBtn icon={isExpanded ? Minimize2 : Maximize2} label={isExpanded ? "Exit Fullscreen" : "Fullscreen"} onClick={() => setIsExpanded(!isExpanded)} />
</div>
<textarea ref={textareaRef} value={value} onChange={(e) => handleChange(e.target.value)} onKeyDown={handleKeyDown} required={required} rows={isExpanded ? 40 : rows} placeholder={placeholder} className={`w-full bg-black/40 border border-white/10 border-t-0 p-4 text-white font-mono text-sm focus:border-purple-500 outline-none resize-none leading-relaxed ${isExpanded ? "flex-1 rounded-b-2xl text-base leading-loose" : "rounded-b-xl"}`} style={{ tabSize: 2 }} />
<div className="flex items-center justify-between mt-1.5 px-1">
<div className="flex items-center gap-3 text-[10px] text-[#86868B] font-mono"><span>{value.split('\n').length} lines</span><span className="text-white/10">|</span><span>{value.length} chars</span></div>
<div className="flex items-center gap-2 text-[10px] text-[#86868B]"><span className="opacity-60">B Bold</span><span className="opacity-60">I Italic</span><span className="opacity-60">Tab Indent</span></div>
</div>
{!isExpanded && (
<div className="bg-purple-500/10 border border-purple-500/20 rounded-xl p-4 text-xs text-[#86868B] font-mono leading-relaxed mt-3">
<p className="text-purple-400 font-semibold uppercase tracking-widest mb-2 text-[10px]">Markdown Cheat Sheet:</p>
<p><strong># Title 1</strong> &nbsp;|&nbsp; <strong>## Title 2</strong> &nbsp;|&nbsp; <strong>**Bold**</strong></p>
<p className="mt-1"><strong>- List Item</strong> &nbsp;|&nbsp; <strong>&gt; Blockquote</strong></p>
<p className="mt-1 text-purple-400"><strong>![Image Description](/applications/{slug || "slug"}/image.jpg)</strong></p>
<div className="mt-3 pt-3 border-t border-purple-500/10">
<p className="mb-1"><strong>Media Assets <span className="text-emerald-400">(use the Assets button to browse & upload)</span>:</strong></p>
<p className="text-emerald-400/80">![alt](/applications/{slug || "slug"}/images/photo.jpg) Image</p>
<p className="text-blue-400/80 mt-1">[VIDEO:/applications/{slug || "slug"}/videos/clip.mp4] Video</p>
<p className="text-purple-400/80 mt-1">[3D:/applications/{slug || "slug"}/models/machine.glb] 3D Model</p>
</div>
</div>
)}
{slug && <AssetManager slug={slug} isOpen={isAssetManagerOpen} onClose={() => setIsAssetManagerOpen(false)} onInsert={handleAssetInsert} />}
</div>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// 🔥 MAIN PAGE — Applications Manager (Knowledge Base)
// ─────────────────────────────────────────────────────────────────────────────
export default function ApplicationsManager() {
const router = useRouter();
const [apps, setApps] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [editingApp, setEditingApp] = useState<any | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [activeTab, setActiveTab] = useState<"basic" | "advantages" | "sections" | "dashboard">("basic");
const [advantages, setAdvantages] = useState<any[]>([]);
const [sections, setSections] = useState<any[]>([]);
const [dashboardMetrics, setDashboardMetrics] = useState<any[]>([]);
const fetchApps = async () => { setIsLoading(true); const res = await getApplications(); if (res.success && res.apps) setApps(res.apps); setIsLoading(false); };
useEffect(() => { fetchApps(); }, []);
const openEditModal = (app: any) => {
setEditingApp(app); setActiveTab("basic");
try { setAdvantages(JSON.parse(app.advantagesJson || "[]")); } catch { setAdvantages([]); }
try { setSections(JSON.parse(app.sectionsJson || "[]")); } catch { setSections([]); }
try { setDashboardMetrics(JSON.parse(app.dashboardMetricsJson || "[]")); } catch { setDashboardMetrics([]); }
};
const handleSave = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); setIsSubmitting(true);
const formData = new FormData(e.currentTarget);
formData.append("advantagesJson", JSON.stringify(advantages));
formData.append("sectionsJson", JSON.stringify(sections));
formData.append("dashboardMetricsJson", JSON.stringify(dashboardMetrics));
const res = await updateApplicationData(formData);
if (res.error) { alert("Error saving data: " + res.error); }
else { setEditingApp(null); await fetchApps(); router.refresh(); }
setIsSubmitting(false);
};
const handleCreate = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); setIsSubmitting(true);
await createApplication(new FormData(e.currentTarget));
setIsCreateModalOpen(false); fetchApps(); setIsSubmitting(false);
};
return (
<div className="min-h-screen p-6 md:p-12 max-w-7xl mx-auto">
<div className="mb-10">
<Link href="/hq-command/dashboard" className="inline-flex items-center gap-2 text-sm text-[#86868B] hover:text-purple-400 transition-colors mb-6 group"><ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" /> Back to Command Center</Link>
<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>
</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>
</div>
<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>
<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>
) : 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'}`}>
<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>
<td className="p-6 text-right"><div className="flex justify-end gap-2"><button onClick={() => openEditModal(app)} className={`inline-flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold transition-all ${isPopulated ? 'bg-white/5 text-white hover:bg-white/10' : 'bg-purple-500 text-white hover:bg-purple-400'}`}><DatabaseZap size={14} /> Manage Core</button><button onClick={() => { if(confirm("Delete this application forever?")) { deleteApplication(app.slug); fetchApps(); } }} className="text-[#86868B] hover:text-red-400 p-2 rounded-lg transition-colors opacity-0 group-hover:opacity-100"><Trash2 size={18}/></button></div></td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
{isCreateModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
<div className="bg-[#111] border border-white/10 w-full max-w-lg rounded-[2rem] p-8 relative shadow-2xl">
<button onClick={() => setIsCreateModalOpen(false)} className="absolute top-6 right-6 text-[#86868B] hover:text-white"><X size={20} /></button>
<h3 className="text-2xl font-light mb-6 text-purple-400">Add New Application</h3>
<form onSubmit={handleCreate} className="space-y-4">
<div><label className="block text-[10px] uppercase text-[#86868B] mb-1">Title</label><input name="title" required placeholder="e.g. Digital Printing" className="w-full bg-black/40 border border-white/10 rounded-xl p-3 text-white focus:border-purple-500 outline-none" /></div>
<div><label className="block text-[10px] uppercase text-[#86868B] mb-1">Subtitle</label><input name="subtitle" required placeholder="e.g. Inkjet Drying" className="w-full bg-black/40 border border-white/10 rounded-xl p-3 text-white focus:border-purple-500 outline-none" /></div>
<div><label className="block text-[10px] uppercase text-[#86868B] mb-1">Category Label</label><input name="category" required placeholder="e.g. Graphic Arts" className="w-full bg-black/40 border border-white/10 rounded-xl p-3 text-white focus:border-purple-500 outline-none" /></div>
<div className="bg-gradient-to-r from-purple-500/10 to-transparent border border-purple-500/20 p-4 rounded-xl flex items-center justify-between mt-6">
<div className="flex items-center gap-3"><div className="p-2 bg-purple-500/20 rounded-lg text-purple-400"><Sparkles size={18} /></div><div><p className="text-sm text-white font-medium">Flux AI Auto-Translate</p><p className="text-[10px] text-[#86868B] uppercase tracking-widest mt-0.5">Generates Base Locales</p></div></div>
<label className="relative inline-flex items-center cursor-pointer"><input type="checkbox" name="autoTranslate" defaultChecked className="sr-only peer" /><div className="w-11 h-6 bg-black/50 border border-white/10 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-500"></div></label>
</div>
<button type="submit" disabled={isSubmitting} className="w-full bg-purple-500 text-white py-3 mt-2 rounded-xl text-sm font-semibold hover:bg-purple-400 flex items-center justify-center gap-2">{isSubmitting ? <Loader2 size={16} className="animate-spin" /> : "Create Application"}</button>
</form>
</div>
</div>
)}
{editingApp && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
<div className="bg-[#111] border border-white/10 w-full max-w-4xl rounded-[2rem] p-0 relative shadow-2xl flex flex-col max-h-[90vh]">
<div className="p-6 md:p-8 border-b border-white/10 relative pb-0 shrink-0">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-purple-500 to-transparent"></div>
<button onClick={() => setEditingApp(null)} className="absolute top-6 right-6 text-[#86868B] hover:text-white transition-colors"><X size={20} /></button>
<h3 className="text-2xl font-light mb-1 text-purple-400">Data Management Core</h3>
<p className="text-[#86868B] text-[10px] uppercase font-mono tracking-widest mb-6">Route: /{editingApp.slug.toUpperCase()}</p>
<div className="flex gap-6 border-b border-white/10 overflow-x-auto">
{[{ id: "basic", label: "Overview", icon: FileText },{ id: "advantages", label: "Advantages", icon: CheckCircle2 },{ id: "sections", label: "Tech Sections", icon: AlignLeft },{ id: "dashboard", label: "Dashboard UI", icon: LayoutTemplate }].map(t => (
<button key={t.id} type="button" onClick={() => setActiveTab(t.id as any)} className={`pb-4 text-xs uppercase tracking-widest font-semibold flex items-center gap-2 transition-all border-b-2 whitespace-nowrap ${activeTab === t.id ? "text-purple-400 border-purple-400" : "text-[#86868B] border-transparent hover:text-white"}`}><t.icon size={14} /> {t.label}</button>
))}
</div>
</div>
<form id="edit-app-form" onSubmit={handleSave} className="flex-1 overflow-y-auto p-6 md:p-8 [scrollbar-width:none]">
<input type="hidden" name="slug" value={editingApp.slug} />
<div className={activeTab === "basic" ? "space-y-6 animate-in fade-in" : "hidden"}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div><label className="block text-[10px] uppercase text-[#86868B] mb-1">Display Title</label><input name="title" defaultValue={editingApp.title} required className="w-full bg-black/40 border border-white/10 rounded-xl p-3 text-white focus:border-purple-500 outline-none" /></div>
<div><label className="block text-[10px] uppercase text-[#86868B] mb-1">Subtitle</label><input name="subtitle" defaultValue={editingApp.subtitle} required className="w-full bg-black/40 border border-white/10 rounded-xl p-3 text-white focus:border-purple-500 outline-none" /></div>
</div>
<div><label className="block text-[10px] uppercase text-[#86868B] mb-1">Category Label</label><input name="category" defaultValue={editingApp.category} required className="w-full bg-black/40 border border-white/10 rounded-xl p-3 text-white focus:border-purple-500 outline-none" /></div>
<div><label className="block text-[10px] uppercase text-purple-400 mb-1 flex items-center justify-between"><span>External Card Description (Homepage)</span><span className="text-[#86868B]">Max 150 chars</span></label><textarea name="shortDescription" defaultValue={editingApp.shortDescription} required rows={2} placeholder="Short summary for the public homepage cards..." className="w-full bg-purple-500/5 border border-purple-500/20 rounded-xl p-3 text-white focus:border-purple-500 outline-none resize-none" /></div>
<div>
<label className="block text-[10px] uppercase text-[#86868B] mb-2 flex justify-between items-center"><span>Deep Page Story (Markdown)</span><span className="text-purple-400/60 text-[9px] font-normal normal-case">Rich Editor + Asset Manager</span></label>
<MarkdownEditor name="heroDescription" defaultValue={editingApp.heroDescription} required rows={14} placeholder="Write the application's deep technical content here using Markdown..." slug={editingApp.slug} />
</div>
</div>
<div className={activeTab === "advantages" ? "space-y-4 animate-in fade-in" : "hidden"}>
{advantages.map((adv, idx) => (
<div key={idx} className="bg-black/40 border border-white/10 p-4 rounded-xl relative group">
<button type="button" onClick={() => setAdvantages(advantages.filter((_, i) => i !== idx))} className="absolute top-4 right-4 text-[#86868B] hover:text-red-400"><Trash2 size={16}/></button>
<input value={adv.title} onChange={e => { const n = [...advantages]; n[idx].title = e.target.value; setAdvantages(n); }} placeholder="Advantage Title" className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm text-white mb-3 focus:border-purple-500 outline-none" />
<textarea value={adv.description} onChange={e => { const n = [...advantages]; n[idx].description = e.target.value; setAdvantages(n); }} placeholder="Description..." rows={2} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm text-white focus:border-purple-500 outline-none resize-none" />
</div>
))}
<button type="button" onClick={() => setAdvantages([...advantages, { title: "", description: "" }])} className="w-full border border-dashed border-white/20 text-[#86868B] hover:text-white hover:border-purple-500 py-3 rounded-xl flex items-center justify-center gap-2"><Plus size={16}/> Add Advantage</button>
</div>
<div className={activeTab === "sections" ? "space-y-6 animate-in fade-in" : "hidden"}>
{sections.map((sec, secIdx) => (
<div key={secIdx} className="bg-black/40 border border-white/10 p-6 rounded-2xl relative">
<button type="button" onClick={() => setSections(sections.filter((_, i) => i !== secIdx))} className="absolute top-4 right-4 text-[#86868B] hover:text-red-400"><Trash2 size={16}/></button>
<div className="flex gap-4 mb-4">
<div className="flex-1"><input value={sec.title} onChange={e => { const n = [...sections]; n[secIdx].title = e.target.value; setSections(n); }} placeholder="Section Title" className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm text-white focus:border-purple-500 outline-none" /></div>
<label className="flex items-center gap-2 text-sm text-white mt-2 cursor-pointer"><input type="checkbox" checked={sec.isMainTech} onChange={e => { const n = [...sections]; n[secIdx].isMainTech = e.target.checked; setSections(n); }} className="accent-purple-500" /> Main Tech</label>
</div>
<div className="pl-4 border-l-2 border-white/10 space-y-3">
{sec.items.map((item: any, itemIdx: number) => (
<div key={itemIdx} className="flex gap-2">
<input value={item.label} onChange={e => { const n = [...sections]; n[secIdx].items[itemIdx].label = e.target.value; setSections(n); }} placeholder="Label" className="w-1/3 bg-black/40 border border-white/10 rounded-lg p-2 text-xs text-white outline-none" />
<input value={item.content} onChange={e => { const n = [...sections]; n[secIdx].items[itemIdx].content = e.target.value; setSections(n); }} placeholder="Content" className="flex-1 bg-black/40 border border-white/10 rounded-lg p-2 text-xs text-white outline-none" />
<button type="button" onClick={() => { const n = [...sections]; n[secIdx].items.splice(itemIdx, 1); setSections(n); }} className="text-[#86868B] hover:text-red-400 p-2"><Trash2 size={14}/></button>
</div>
))}
<button type="button" onClick={() => { const n = [...sections]; n[secIdx].items.push({ label: "", content: "" }); setSections(n); }} className="text-xs text-purple-400 hover:text-white flex items-center gap-1 mt-2"><Plus size={12}/> Add Row</button>
</div>
</div>
))}
<button type="button" onClick={() => setSections([...sections, { title: "", isMainTech: false, items: [] }])} className="w-full border border-dashed border-white/20 text-[#86868B] hover:text-white py-3 rounded-xl flex items-center justify-center gap-2"><Plus size={16}/> Add Section</button>
</div>
<div className={activeTab === "dashboard" ? "space-y-4 animate-in fade-in" : "hidden"}>
{dashboardMetrics.map((metric, idx) => (
<div key={idx} className="bg-black/40 border border-white/10 p-4 rounded-xl relative grid grid-cols-3 gap-3">
<button type="button" onClick={() => setDashboardMetrics(dashboardMetrics.filter((_, i) => i !== idx))} className="absolute -top-2 -right-2 text-[#86868B] hover:text-red-400 bg-black rounded-full p-1"><Trash2 size={14}/></button>
<input value={metric.label} onChange={e => { const n = [...dashboardMetrics]; n[idx].label = e.target.value; setDashboardMetrics(n); }} placeholder="Label" className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm text-white outline-none" />
<input value={metric.value} onChange={e => { const n = [...dashboardMetrics]; n[idx].value = e.target.value; setDashboardMetrics(n); }} placeholder="Value (e.g. 5kW)" className="w-full bg-black/40 border border-[#00F0FF]/30 rounded-lg p-2 text-sm text-[#00F0FF] outline-none" />
<input value={metric.subtext} onChange={e => { const n = [...dashboardMetrics]; n[idx].subtext = e.target.value; setDashboardMetrics(n); }} placeholder="Subtext" className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm text-[#86868B] outline-none" />
</div>
))}
<button type="button" onClick={() => setDashboardMetrics([...dashboardMetrics, { label: "", value: "", subtext: "" }])} className="w-full border border-dashed border-white/20 text-[#86868B] hover:text-white py-3 rounded-xl flex items-center justify-center gap-2"><Plus size={16}/> Add Metric</button>
</div>
<div className="bg-gradient-to-r from-purple-500/10 to-transparent border border-purple-500/20 p-4 rounded-xl flex items-center justify-between mt-10">
<div className="flex items-center gap-3"><div className="p-2 bg-purple-500/20 rounded-lg text-purple-400"><Sparkles size={18} /></div><div><p className="text-sm text-white font-medium">Flux AI JSON Translation</p><p className="text-[10px] text-[#86868B] uppercase tracking-widest mt-0.5">Translates Markdown & Complex Data Arrays</p></div></div>
<label className="relative inline-flex items-center cursor-pointer"><input type="checkbox" name="autoTranslate" defaultChecked className="sr-only peer" /><div className="w-11 h-6 bg-black/50 border border-white/10 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-500"></div></label>
</div>
</form>
<div className="p-6 border-t border-white/10 bg-[#111] rounded-b-[2rem] flex justify-end gap-3 shrink-0">
<button type="button" onClick={() => setEditingApp(null)} className="px-5 py-3 text-sm font-medium text-[#86868B] hover:text-white">Cancel</button>
<button onClick={() => (document.getElementById("edit-app-form") as HTMLFormElement)?.requestSubmit()} disabled={isSubmitting} className="flex items-center gap-2 bg-purple-500 text-white px-8 py-3 rounded-xl text-sm font-semibold hover:bg-purple-400 disabled:opacity-50 transition-colors shadow-[0_0_15px_rgba(168,85,247,0.3)]">{isSubmitting ? <><Loader2 size={18} className="animate-spin" /> Processing AI...</> : "Deploy Changes"}</button>
</div>
</div>
</div>
)}
</div>
);
}