From a57504021721f19f91c93dee0ac17e528d8e7bd4 Mon Sep 17 00:00:00 2001 From: jmartin Date: Fri, 24 Jul 2026 11:47:46 -0400 Subject: [PATCH] Add edit flow for portfolio views in admin Load existing views into the form to change name, slug, description, category priority order, show-others, and active flags. --- client/src/pages/admin/AdminViews.tsx | 199 +++++++++++++++++++++----- 1 file changed, 167 insertions(+), 32 deletions(-) diff --git a/client/src/pages/admin/AdminViews.tsx b/client/src/pages/admin/AdminViews.tsx index 7143f4a..a74b831 100644 --- a/client/src/pages/admin/AdminViews.tsx +++ b/client/src/pages/admin/AdminViews.tsx @@ -1,14 +1,22 @@ import { useEffect, useState } from "react"; import { api, type Category, type PortfolioView } from "../../lib/api"; +import { usePageTitle } from "../../lib/pageTitle"; export function AdminViews() { const [views, setViews] = useState([]); const [categories, setCategories] = useState([]); + const [editingId, setEditingId] = useState(null); const [name, setName] = useState(""); const [slug, setSlug] = useState(""); const [description, setDescription] = useState(""); const [selected, setSelected] = useState([]); + const [showOthers, setShowOthers] = useState(true); + const [isActive, setIsActive] = useState(true); const [msg, setMsg] = useState(null); + const [err, setErr] = useState(null); + const [saving, setSaving] = useState(false); + + usePageTitle(editingId ? "Edit view · Admin" : "Portfolio views · Admin"); function load() { api.adminViews().then(setViews); @@ -19,27 +27,34 @@ export function AdminViews() { load(); }, []); - async function create(e: React.FormEvent) { - e.preventDefault(); - await api.createView({ - name, - slug: slug || undefined, - description, - showOthers: true, - categories: selected.map((categoryId, priority) => ({ categoryId, priority })), - }); + function resetForm() { + setEditingId(null); setName(""); setSlug(""); setDescription(""); setSelected([]); - setMsg("View created"); - load(); + setShowOthers(true); + setIsActive(true); } - async function remove(id: string, n: string) { - if (!confirm(`Delete view “${n}”?`)) return; - await api.deleteView(id); - load(); + function startEdit(v: PortfolioView) { + setErr(null); + setMsg(null); + setEditingId(v.id); + setName(v.name); + setSlug(v.slug); + setDescription(v.description || ""); + setShowOthers(v.showOthers !== false); + setIsActive(v.isActive !== false); + const ordered = [...(v.categoryPriorities || [])].sort( + (a, b) => (a.priority ?? 0) - (b.priority ?? 0) + ); + setSelected( + ordered + .map((cp) => cp.categoryId || cp.category?.id) + .filter((id): id is string => !!id) + ); + window.scrollTo({ top: 0, behavior: "smooth" }); } function toggleCat(id: string) { @@ -60,34 +75,97 @@ export function AdminViews() { }); } + async function save(e: React.FormEvent) { + e.preventDefault(); + setSaving(true); + setErr(null); + setMsg(null); + const body = { + name: name.trim(), + slug: slug.trim() || undefined, + description: description.trim() || null, + showOthers, + isActive, + categories: selected.map((categoryId, priority) => ({ categoryId, priority })), + }; + try { + if (editingId) { + await api.updateView(editingId, body); + setMsg(`Updated “${body.name}”`); + } else { + await api.createView(body); + setMsg(`Created “${body.name}”`); + } + resetForm(); + load(); + } catch (e) { + setErr((e as Error).message); + } finally { + setSaving(false); + } + } + + async function remove(id: string, n: string) { + if (!confirm(`Delete view “${n}”? This cannot be undone.`)) return; + setErr(null); + try { + await api.deleteView(id); + if (editingId === id) resetForm(); + setMsg(`Deleted “${n}”`); + load(); + } catch (e) { + setErr((e as Error).message); + } + } + return (

Portfolio views

Human-readable URLs like /portfolio/game-dev that prioritize categories for - different audiences. + different audiences. Category chips (e.g. /portfolio/photography) still + hard-filter by category name — views are for multi-category audience ordering.

{msg &&
{msg}
} + {err &&
{err}
} -
+ +

+ {editingId ? "Edit view" : "Create view"} +

- - setName(e.target.value)} /> + + setName(e.target.value)} + />
- + setSlug(e.target.value)} /> + {slug && ( + + Live URL: /portfolio/{slug} + + )}
- -