"use client"; import { useEffect, useState, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { useRouter } from "next/navigation"; import { PROJECT_SIZES, SIZE_LABELS, type ProjectSize } from "../../types"; import { MarkdownContent } from "@/components/MarkdownContent"; type Resource = "projects" | "experience" | "affiliates" | "blogs"; const RESOURCES: { key: Resource; label: string }[] = [ { key: "projects", label: "Projects" }, { key: "experience", label: "Work Experience" }, { key: "affiliates", label: "Affiliates" }, { key: "blogs", label: "Blogs" }, ]; // A record being edited; `_new` marks an unsaved draft (POST vs PUT). type Row = Record & { id?: string; _new?: boolean }; const TEMPLATES: Record Row> = { projects: () => ({ _new: true, name: "", label: "", size: "MediumSized", imageUrl: "", link: "", linkIsDemo: false, sourceCode: "", description: "", why: "", note: "", tags: [], loc: null, locEndpoint: "", }), experience: () => ({ _new: true, company: "", role: "", fromDate: "", toDate: "", url: "", iconUrl: "", summary: "", tags: [], sortIndex: 0, }), affiliates: () => ({ _new: true, name: "", link: "", icon: "", location: "", provides: [], good: [], bad: [], sortIndex: 0, }), blogs: () => ({ _new: true, title: "", slug: "", excerpt: "", coverImageUrl: "", content: "# New post\n\nStart writing here.", isPublished: true, publishedAt: "", views: 0, }), }; const inputCls = "w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm text-gray-100 outline-none focus:border-blue-500/50"; const labelCls = "text-xs font-bold text-gray-500 uppercase"; function toDateInput(value: unknown): string { if (!value) return ""; const d = new Date(String(value)); if (Number.isNaN(d.getTime())) return ""; return d.toISOString().slice(0, 10); } function toDateTimeInput(value: unknown): string { if (!value) return ""; const d = new Date(String(value)); if (Number.isNaN(d.getTime())) return ""; return new Date(d.getTime() - d.getTimezoneOffset() * 60_000).toISOString().slice(0, 16); } export default function AdminPage() { const router = useRouter(); const [authChecked, setAuthChecked] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false); const [password, setPassword] = useState(""); const [loginError, setLoginError] = useState(""); const [loggingIn, setLoggingIn] = useState(false); const [resource, setResource] = useState("projects"); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(false); const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null); const [savingId, setSavingId] = useState(null); const showToast = (type: "success" | "error", message: string) => { setToast({ type, message }); setTimeout(() => setToast(null), 3500); }; // --- auth --- useEffect(() => { fetch("/api/admin/session") .then((r) => r.json()) .then((d) => setIsAuthenticated(Boolean(d.authenticated))) .catch(() => setIsAuthenticated(false)) .finally(() => setAuthChecked(true)); }, []); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setLoggingIn(true); setLoginError(""); try { const res = await fetch("/api/admin/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }), }); if (res.ok) { setIsAuthenticated(true); setPassword(""); } else { setLoginError("Invalid password"); } } catch { setLoginError("Network error"); } finally { setLoggingIn(false); } }; const handleLogout = async () => { await fetch("/api/admin/logout", { method: "POST" }); setIsAuthenticated(false); }; // --- data --- const loadRows = useCallback(async (res: Resource) => { setLoading(true); try { const r = await fetch(`/api/admin/${res}`); if (r.ok) { const json = await r.json(); const normalized = Array.isArray(json) ? json.map((item: Row) => res === "experience" ? { ...item, fromDate: toDateInput(item.fromDate), toDate: toDateInput(item.toDate) } : res === "blogs" ? { ...item, publishedAt: toDateTimeInput(item.publishedAt) } : item, ) : []; setRows(normalized); } else if (r.status === 401) { setIsAuthenticated(false); } else { showToast("error", `Failed to load (${r.status})`); } } catch { showToast("error", "Failed to load data"); } finally { setLoading(false); } }, []); useEffect(() => { if (isAuthenticated) loadRows(resource); }, [isAuthenticated, resource, loadRows]); const update = (idx: number, field: string, value: unknown) => { setRows((prev) => prev.map((row, i) => (i === idx ? { ...row, [field]: value } : row))); }; const addRow = () => setRows((prev) => [TEMPLATES[resource](), ...prev]); const saveRow = async (idx: number) => { const row = rows[idx]; const isNew = row._new || !row.id; setSavingId(row.id || `new-${idx}`); try { const url = isNew ? `/api/admin/${resource}` : `/api/admin/${resource}/${row.id}`; const payload = { ...row }; delete payload._new; const res = await fetch(url, { method: isNew ? "POST" : "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (res.ok) { showToast("success", "Saved"); await loadRows(resource); } else { const err = await res.json().catch(() => ({})); showToast("error", err.error || `Save failed (${res.status})`); } } catch { showToast("error", "Network error"); } finally { setSavingId(null); } }; const deleteRow = async (idx: number) => { const row = rows[idx]; if (row._new || !row.id) { setRows((prev) => prev.filter((_, i) => i !== idx)); return; } if (!confirm("Delete this entry permanently?")) return; try { const res = await fetch(`/api/admin/${resource}/${row.id}`, { method: "DELETE" }); if (res.ok) { showToast("success", "Deleted"); setRows((prev) => prev.filter((_, i) => i !== idx)); } else { showToast("error", `Delete failed (${res.status})`); } } catch { showToast("error", "Network error"); } }; // --- login screen --- if (!authChecked) { return (
Checking session…
); } if (!isAuthenticated) { return (

ADMIN ACCESS

setPassword(e.target.value)} placeholder="Password" className="w-full bg-black/50 border border-white/10 rounded-xl px-4 py-3 focus:ring-2 focus:ring-purple-500/50 outline-none" autoFocus /> {loginError &&

{loginError}

}
); } // --- panel --- return (

PORTFOLIO CMS

Manage your projects, experience, affiliates & blog posts

{RESOURCES.find((r) => r.key === resource)?.label}

{loading ? (
Loading…
) : rows.length === 0 ? (
Nothing here yet. Add a new entry.
) : ( {rows.map((row, idx) => (
{row._new ? "NEW" : (row.id as string)?.slice(0, 8)}
{resource === "projects" && } {resource === "experience" && } {resource === "affiliates" && } {resource === "blogs" && }
))}
)}
{toast && ( {toast.message} )}
); } // ---------- field group components ---------- type FieldProps = { row: Row; idx: number; update: (idx: number, field: string, value: unknown) => void }; function Text({ row, idx, update, field, label, placeholder }: FieldProps & { field: string; label: string; placeholder?: string }) { return (
update(idx, field, e.target.value)} className={inputCls} placeholder={placeholder} />
); } function Area({ row, idx, update, field, label }: FieldProps & { field: string; label: string }) { return (