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:
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api, type Project, type SiteSettings, type Category } from "../lib/api";
|
||||
import { ProjectCard } from "../components/ProjectCard";
|
||||
import "./HomePage.scss";
|
||||
|
||||
export function HomePage() {
|
||||
const [site, setSite] = useState<SiteSettings | null>(null);
|
||||
const [featured, setFeatured] = useState<Project[]>([]);
|
||||
const [recent, setRecent] = useState<Project[]>([]);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
api.site(),
|
||||
api.featured().catch(() => [] as Project[]),
|
||||
api.projects({ sort: "date", perPage: 6 }),
|
||||
api.categories(),
|
||||
]).then(([s, f, r, c]) => {
|
||||
setSite(s);
|
||||
setFeatured(f);
|
||||
setRecent(r.data);
|
||||
setCategories(c);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const heroTitle = site?.heroTitle || site?.name || "jmartgraphix";
|
||||
const heroSub = site?.heroSubtitle || "3D · Design · Motion · Craft";
|
||||
|
||||
return (
|
||||
<div className="home page-enter">
|
||||
<section className="home-hero">
|
||||
<div className="container">
|
||||
<p className="home-hero__eyebrow">Portfolio</p>
|
||||
<h1>
|
||||
<span className="home-hero__serif">{heroTitle}</span>
|
||||
</h1>
|
||||
<p className="home-hero__sub">{heroSub}</p>
|
||||
<p className="home-hero__about">
|
||||
{site?.about ||
|
||||
"A curated collection of 3D modeling, CAD, animation, and visual design."}
|
||||
</p>
|
||||
<div className="home-hero__actions">
|
||||
<Link to="/portfolio" className="btn btn--primary">
|
||||
View portfolio
|
||||
</Link>
|
||||
<Link to="/resume" className="btn btn--ghost">
|
||||
Resume
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="home-cats container">
|
||||
<div className="home-cats__list">
|
||||
{categories.map((c) => (
|
||||
<Link key={c.id} to={`/portfolio/${c.slug}`} className="home-cats__chip">
|
||||
{c.name}
|
||||
{typeof c.projectCount === "number" && (
|
||||
<span>{c.projectCount}</span>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
<Link to="/portfolio/game-dev" className="home-cats__chip home-cats__chip--view">
|
||||
Game Dev view
|
||||
</Link>
|
||||
<Link to="/portfolio/engineering" className="home-cats__chip home-cats__chip--view">
|
||||
Engineering view
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{featured.length > 0 && (
|
||||
<section className="container home-section">
|
||||
<div className="home-section__head">
|
||||
<h2>Featured</h2>
|
||||
<Link to="/portfolio">All work →</Link>
|
||||
</div>
|
||||
<div className="project-grid">
|
||||
{featured.map((p, i) => (
|
||||
<ProjectCard key={p.id} project={p} index={i} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="container home-section">
|
||||
<div className="home-section__head">
|
||||
<h2>Recent</h2>
|
||||
</div>
|
||||
{recent.length === 0 ? (
|
||||
<p className="home-empty">
|
||||
Projects will appear here once published. Use the{" "}
|
||||
<Link to="/admin">admin panel</Link> to add work, or import from ArtStation.
|
||||
</p>
|
||||
) : (
|
||||
<div className="project-grid">
|
||||
{recent.map((p, i) => (
|
||||
<ProjectCard key={p.id} project={p} index={i} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user