feat: Created useDocumentTitle hook and wired descriptive, route-specif…

- "frontend/src/hooks/useDocumentTitle.ts"
- "frontend/src/pages/Home.tsx"
- "frontend/src/pages/TopicsBrowse.tsx"
- "frontend/src/pages/SubTopicPage.tsx"
- "frontend/src/pages/CreatorsBrowse.tsx"
- "frontend/src/pages/CreatorDetail.tsx"
- "frontend/src/pages/TechniquePage.tsx"
- "frontend/src/pages/SearchResults.tsx"

GSD-Task: S04/T02
This commit is contained in:
jlightner 2026-03-31 08:56:16 +00:00
parent 6845f5c349
commit f3e6a9c885
12 changed files with 51 additions and 1 deletions

View file

@ -0,0 +1,22 @@
import { useEffect, useRef } from "react";
const DEFAULT_TITLE = "Chrysopedia";
/**
* Sets `document.title` to the given value. Resets to the default
* title on unmount so navigating away doesn't leave a stale tab name.
*/
export function useDocumentTitle(title: string): void {
const prevTitle = useRef(document.title);
useEffect(() => {
document.title = title || DEFAULT_TITLE;
}, [title]);
useEffect(() => {
const fallback = prevTitle.current;
return () => {
document.title = fallback || DEFAULT_TITLE;
};
}, []);
}

View file

@ -1,6 +1,8 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
export default function About() { export default function About() {
useDocumentTitle("About — Chrysopedia");
return ( return (
<div className="about"> <div className="about">
<section className="about-hero"> <section className="about-hero">

View file

@ -5,6 +5,7 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
import { import {
fetchPipelineVideos, fetchPipelineVideos,
fetchPipelineEvents, fetchPipelineEvents,
@ -466,6 +467,7 @@ function StatusFilter({
// ── Main Page ──────────────────────────────────────────────────────────────── // ── Main Page ────────────────────────────────────────────────────────────────
export default function AdminPipeline() { export default function AdminPipeline() {
useDocumentTitle("Pipeline Management — Chrysopedia");
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [videos, setVideos] = useState<PipelineVideoItem[]>([]); const [videos, setVideos] = useState<PipelineVideoItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);

View file

@ -11,6 +11,7 @@ import {
updateReport, updateReport,
type ContentReport, type ContentReport,
} from "../api/public-client"; } from "../api/public-client";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
{ value: "", label: "All" }, { value: "", label: "All" },
@ -49,6 +50,7 @@ function reportTypeLabel(rt: string): string {
} }
export default function AdminReports() { export default function AdminReports() {
useDocumentTitle("Content Reports — Chrysopedia");
const [reports, setReports] = useState<ContentReport[]>([]); const [reports, setReports] = useState<ContentReport[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);

View file

@ -16,6 +16,7 @@ import {
import CreatorAvatar from "../components/CreatorAvatar"; import CreatorAvatar from "../components/CreatorAvatar";
import { catSlug } from "../utils/catSlug"; import { catSlug } from "../utils/catSlug";
import TagList from "../components/TagList"; import TagList from "../components/TagList";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
export default function CreatorDetail() { export default function CreatorDetail() {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
@ -25,6 +26,8 @@ export default function CreatorDetail() {
const [notFound, setNotFound] = useState(false); const [notFound, setNotFound] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useDocumentTitle(creator ? `${creator.name} — Chrysopedia` : "Chrysopedia");
useEffect(() => { useEffect(() => {
if (!slug) return; if (!slug) return;

View file

@ -15,6 +15,7 @@ import {
type CreatorBrowseItem, type CreatorBrowseItem,
} from "../api/public-client"; } from "../api/public-client";
import CreatorAvatar from "../components/CreatorAvatar"; import CreatorAvatar from "../components/CreatorAvatar";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
const GENRES = [ const GENRES = [
"Bass music", "Bass music",
@ -41,6 +42,7 @@ const SORT_OPTIONS: { value: SortMode; label: string }[] = [
]; ];
export default function CreatorsBrowse() { export default function CreatorsBrowse() {
useDocumentTitle("Creators — Chrysopedia");
const [creators, setCreators] = useState<CreatorBrowseItem[]>([]); const [creators, setCreators] = useState<CreatorBrowseItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);

View file

@ -10,6 +10,7 @@ import SearchAutocomplete from "../components/SearchAutocomplete";
import TagList from "../components/TagList"; import TagList from "../components/TagList";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
import { import {
fetchTechniques, fetchTechniques,
fetchTopics, fetchTopics,
@ -18,6 +19,7 @@ import {
} from "../api/public-client"; } from "../api/public-client";
export default function Home() { export default function Home() {
useDocumentTitle("Chrysopedia — Production Knowledge, Distilled");
const [featured, setFeatured] = useState<TechniqueListItem | null>(null); const [featured, setFeatured] = useState<TechniqueListItem | null>(null);
const [recent, setRecent] = useState<TechniqueListItem[]>([]); const [recent, setRecent] = useState<TechniqueListItem[]>([]);
const [recentLoading, setRecentLoading] = useState(true); const [recentLoading, setRecentLoading] = useState(true);

View file

@ -12,12 +12,15 @@ import { searchApi, type SearchResultItem } from "../api/public-client";
import { catSlug } from "../utils/catSlug"; import { catSlug } from "../utils/catSlug";
import SearchAutocomplete from "../components/SearchAutocomplete"; import SearchAutocomplete from "../components/SearchAutocomplete";
import TagList from "../components/TagList"; import TagList from "../components/TagList";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
export default function SearchResults() { export default function SearchResults() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
const q = searchParams.get("q") ?? ""; const q = searchParams.get("q") ?? "";
useDocumentTitle(q ? `Search: ${q} — Chrysopedia` : "Search — Chrysopedia");
const [results, setResults] = useState<SearchResultItem[]>([]); const [results, setResults] = useState<SearchResultItem[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);

View file

@ -13,6 +13,7 @@ import {
} from "../api/public-client"; } from "../api/public-client";
import { catSlug } from "../utils/catSlug"; import { catSlug } from "../utils/catSlug";
import TagList from "../components/TagList"; import TagList from "../components/TagList";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
/** Convert a URL slug to a display name: replace hyphens with spaces, title-case. */ /** Convert a URL slug to a display name: replace hyphens with spaces, title-case. */
function slugToDisplayName(slug: string): string { function slugToDisplayName(slug: string): string {
@ -51,6 +52,12 @@ export default function SubTopicPage() {
const categoryDisplay = category ? slugToDisplayName(category) : ""; const categoryDisplay = category ? slugToDisplayName(category) : "";
const subtopicDisplay = subtopic ? slugToDisplayName(subtopic) : ""; const subtopicDisplay = subtopic ? slugToDisplayName(subtopic) : "";
useDocumentTitle(
subtopicDisplay && categoryDisplay
? `${subtopicDisplay}${categoryDisplay} — Chrysopedia`
: "Chrysopedia",
);
useEffect(() => { useEffect(() => {
if (!category || !subtopic) return; if (!category || !subtopic) return;

View file

@ -19,6 +19,7 @@ import {
import ReportIssueModal from "../components/ReportIssueModal"; import ReportIssueModal from "../components/ReportIssueModal";
import CopyLinkButton from "../components/CopyLinkButton"; import CopyLinkButton from "../components/CopyLinkButton";
import CreatorAvatar from "../components/CreatorAvatar"; import CreatorAvatar from "../components/CreatorAvatar";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
function formatTime(seconds: number): string { function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60); const m = Math.floor(seconds / 60);
@ -73,6 +74,8 @@ export default function TechniquePage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showReport, setShowReport] = useState(false); const [showReport, setShowReport] = useState(false);
useDocumentTitle(technique ? `${technique.title} — Chrysopedia` : "Chrysopedia");
// Version switching // Version switching
const [versions, setVersions] = useState<TechniquePageVersionSummary[]>([]); const [versions, setVersions] = useState<TechniquePageVersionSummary[]>([]);
const [selectedVersion, setSelectedVersion] = useState<string>("current"); const [selectedVersion, setSelectedVersion] = useState<string>("current");

View file

@ -14,10 +14,12 @@ import { Link } from "react-router-dom";
import { fetchTopics, type TopicCategory } from "../api/public-client"; import { fetchTopics, type TopicCategory } from "../api/public-client";
import { CATEGORY_ICON } from "../components/CategoryIcons"; import { CATEGORY_ICON } from "../components/CategoryIcons";
import { catSlug } from "../utils/catSlug"; import { catSlug } from "../utils/catSlug";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
export default function TopicsBrowse() { export default function TopicsBrowse() {
useDocumentTitle("Topics — Chrysopedia");
const [categories, setCategories] = useState<TopicCategory[]>([]); const [categories, setCategories] = useState<TopicCategory[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);

View file

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/TagList.tsx","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts"],"version":"5.6.3"} {"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/TagList.tsx","./src/hooks/useDocumentTitle.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts"],"version":"5.6.3"}