diff --git a/client/src/lib/pageTitle.ts b/client/src/lib/pageTitle.ts new file mode 100644 index 0000000..2a76bc4 --- /dev/null +++ b/client/src/lib/pageTitle.ts @@ -0,0 +1,16 @@ +import { useEffect } from "react"; + +export const SITE_TITLE = "jmartgraphix"; + +/** Set browser tab title: "Page · jmartgraphix" or just site name. */ +export function setPageTitle(page?: string | null) { + const part = page?.trim(); + document.title = part ? `${part} · ${SITE_TITLE}` : SITE_TITLE; +} + +/** React hook — updates document.title whenever `page` changes. */ +export function usePageTitle(page?: string | null) { + useEffect(() => { + setPageTitle(page); + }, [page]); +} diff --git a/client/src/pages/AboutPage.tsx b/client/src/pages/AboutPage.tsx index 60c4550..0c06e74 100644 --- a/client/src/pages/AboutPage.tsx +++ b/client/src/pages/AboutPage.tsx @@ -1,11 +1,14 @@ import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { api, type SiteSettings } from "../lib/api"; +import { usePageTitle } from "../lib/pageTitle"; import "./AboutPage.scss"; export function AboutPage() { const [site, setSite] = useState(null); + usePageTitle("About"); + useEffect(() => { api.site().then(setSite).catch(() => {}); }, []); diff --git a/client/src/pages/HomePage.tsx b/client/src/pages/HomePage.tsx index 1272c65..ca58ba1 100644 --- a/client/src/pages/HomePage.tsx +++ b/client/src/pages/HomePage.tsx @@ -2,6 +2,7 @@ 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 { usePageTitle } from "../lib/pageTitle"; import "./HomePage.scss"; export function HomePage() { @@ -10,6 +11,8 @@ export function HomePage() { const [recent, setRecent] = useState([]); const [categories, setCategories] = useState([]); + usePageTitle(null); // site name only + useEffect(() => { Promise.all([ api.site(), diff --git a/client/src/pages/PortfolioPage.tsx b/client/src/pages/PortfolioPage.tsx index 897f1e9..469d554 100644 --- a/client/src/pages/PortfolioPage.tsx +++ b/client/src/pages/PortfolioPage.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useParams, useSearchParams, Link } from "react-router-dom"; import { api, type Project, type Category, type ListMeta } from "../lib/api"; import { ProjectCard } from "../components/ProjectCard"; +import { usePageTitle } from "../lib/pageTitle"; import "./PortfolioPage.scss"; export function PortfolioPage() { @@ -57,6 +58,15 @@ export function PortfolioPage() { meta?.view?.description || "Browse projects by category, tag, or search. Share tailored views with clients and collaborators."; + const tabTitle = viewSlug + ? meta?.view?.name || viewSlug + : category + ? categories.find((c) => c.slug === category)?.name || category + : q + ? `Search: ${q}` + : "Portfolio"; + usePageTitle(tabTitle); + return (
diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx index 2ae4b19..8cf3e51 100644 --- a/client/src/pages/ProjectPage.tsx +++ b/client/src/pages/ProjectPage.tsx @@ -3,6 +3,7 @@ import { Link, useParams } from "react-router-dom"; import Lightbox from "yet-another-react-lightbox"; import "yet-another-react-lightbox/styles.css"; import { api, type Project, mediaSrc } from "../lib/api"; +import { usePageTitle } from "../lib/pageTitle"; import "./ProjectPage.scss"; export function ProjectPage() { @@ -11,13 +12,16 @@ export function ProjectPage() { const [error, setError] = useState(null); const [lbIndex, setLbIndex] = useState(-1); + usePageTitle(project?.title || (error ? "Not found" : "Project")); + useEffect(() => { if (!slug) return; + setProject(null); + setError(null); api .project(slug) .then((p) => { setProject(p); - document.title = `${p.title} · jmartgraphix`; // Open Graph-ish meta updates const setMeta = (prop: string, content: string) => { let el = document.querySelector(`meta[property="${prop}"]`); diff --git a/client/src/pages/ResumePage.tsx b/client/src/pages/ResumePage.tsx index 03d42cd..3c2efb7 100644 --- a/client/src/pages/ResumePage.tsx +++ b/client/src/pages/ResumePage.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { api, type Resume } from "../lib/api"; +import { usePageTitle } from "../lib/pageTitle"; import "./ResumePage.scss"; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -9,12 +10,15 @@ export function ResumePage() { const [resume, setResume] = useState(null); const [error, setError] = useState(null); + usePageTitle( + resume ? `${resume.fullName} — Resume` : error ? "Resume" : "Resume" + ); + useEffect(() => { api .resume() .then((r) => { setResume(r); - document.title = `${r.fullName} — Resume · jmartgraphix`; }) .catch((e) => setError(e.message)); }, []); diff --git a/client/src/pages/admin/AdminLayout.tsx b/client/src/pages/admin/AdminLayout.tsx index 22dc4ff..c7afdd7 100644 --- a/client/src/pages/admin/AdminLayout.tsx +++ b/client/src/pages/admin/AdminLayout.tsx @@ -1,12 +1,28 @@ import { useEffect, useState } from "react"; -import { Link, NavLink, Outlet, useNavigate } from "react-router-dom"; +import { Link, NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { api } from "../../lib/api"; +import { usePageTitle } from "../../lib/pageTitle"; import "./Admin.scss"; +function adminTitleFromPath(pathname: string): string { + if (pathname.endsWith("/projects/new")) return "New project · Admin"; + if (/\/projects\/[^/]+$/.test(pathname) && !pathname.endsWith("/projects")) + return "Edit project · Admin"; + if (pathname.includes("/projects")) return "Projects · Admin"; + if (pathname.includes("/categories")) return "Categories · Admin"; + if (pathname.includes("/views")) return "Portfolio views · Admin"; + if (pathname.includes("/resume")) return "Resume · Admin"; + if (pathname.includes("/settings")) return "Settings · Admin"; + return "Admin"; +} + export function AdminLayout() { const [user, setUser] = useState<{ username: string; name?: string } | null>(null); const [error, setError] = useState(null); const navigate = useNavigate(); + const location = useLocation(); + + usePageTitle(adminTitleFromPath(location.pathname)); useEffect(() => { api