Initial portfolio CMS: Fastify API + Vite SPA

Single-container app with Postgres catalog, Typesense search,
Authelia Remote-User admin, portfolio views, resume/PDF, media
uploads, and ArtStation import tooling.
This commit is contained in:
2026-07-23 17:01:34 -04:00
commit 43928f7a49
61 changed files with 13218 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import { useEffect, useState } from "react";
import { Link, NavLink, Outlet, useNavigate } from "react-router-dom";
import { api } from "../../lib/api";
import "./Admin.scss";
export function AdminLayout() {
const [user, setUser] = useState<{ username: string; name?: string } | null>(null);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
api
.me()
.then(setUser)
.catch((e) => setError(e.message));
}, []);
if (error) {
return (
<div className="admin-auth-error">
<h1>Admin access required</h1>
<p>
This area is protected by Authelia SSO via the <code>Remote-User</code> header.
</p>
<p className="admin-auth-error__detail">{error}</p>
<p>
Sign in at{" "}
<a href="https://auth.jmartgraphix.com">auth.jmartgraphix.com</a>, then return here.
</p>
<Link to="/" className="btn btn--ghost">
Back to site
</Link>
</div>
);
}
if (!user) {
return (
<div className="admin-auth-error">
<p>Checking authentication</p>
</div>
);
}
return (
<div className="admin-shell">
<aside className="admin-nav">
<div className="admin-nav__brand">
<Link to="/admin">jmartgraphix</Link>
<span>Admin</span>
</div>
<nav>
<NavLink to="/admin" end>
Dashboard
</NavLink>
<NavLink to="/admin/projects">Projects</NavLink>
<NavLink to="/admin/categories">Categories & Tags</NavLink>
<NavLink to="/admin/views">Portfolio Views</NavLink>
<NavLink to="/admin/resume">Resume</NavLink>
<NavLink to="/admin/settings">Settings</NavLink>
</nav>
<div className="admin-nav__user">
<span>{user.name || user.username}</span>
<button type="button" className="btn btn--ghost btn--sm" onClick={() => navigate("/")}>
View site
</button>
</div>
</aside>
<div className="admin-content">
<Outlet />
</div>
</div>
);
}