From f3e6a9c885e4bfc3c5368caab560dc1e13636225 Mon Sep 17 00:00:00 2001 From: jlightner Date: Tue, 31 Mar 2026 08:56:16 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Created=20useDocumentTitle=20hook=20and?= =?UTF-8?q?=20wired=20descriptive,=20route-specif=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "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 --- frontend/src/hooks/useDocumentTitle.ts | 22 ++++++++++++++++++++++ frontend/src/pages/About.tsx | 2 ++ frontend/src/pages/AdminPipeline.tsx | 2 ++ frontend/src/pages/AdminReports.tsx | 2 ++ frontend/src/pages/CreatorDetail.tsx | 3 +++ frontend/src/pages/CreatorsBrowse.tsx | 2 ++ frontend/src/pages/Home.tsx | 2 ++ frontend/src/pages/SearchResults.tsx | 3 +++ frontend/src/pages/SubTopicPage.tsx | 7 +++++++ frontend/src/pages/TechniquePage.tsx | 3 +++ frontend/src/pages/TopicsBrowse.tsx | 2 ++ frontend/tsconfig.app.tsbuildinfo | 2 +- 12 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 frontend/src/hooks/useDocumentTitle.ts diff --git a/frontend/src/hooks/useDocumentTitle.ts b/frontend/src/hooks/useDocumentTitle.ts new file mode 100644 index 0000000..fec45a0 --- /dev/null +++ b/frontend/src/hooks/useDocumentTitle.ts @@ -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; + }; + }, []); +} diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index 4830581..f83b092 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx @@ -1,6 +1,8 @@ import { Link } from "react-router-dom"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; export default function About() { + useDocumentTitle("About — Chrysopedia"); return (
diff --git a/frontend/src/pages/AdminPipeline.tsx b/frontend/src/pages/AdminPipeline.tsx index f3f02f6..36a88b0 100644 --- a/frontend/src/pages/AdminPipeline.tsx +++ b/frontend/src/pages/AdminPipeline.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useSearchParams } from "react-router-dom"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { fetchPipelineVideos, fetchPipelineEvents, @@ -466,6 +467,7 @@ function StatusFilter({ // ── Main Page ──────────────────────────────────────────────────────────────── export default function AdminPipeline() { + useDocumentTitle("Pipeline Management — Chrysopedia"); const [searchParams] = useSearchParams(); const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(true); diff --git a/frontend/src/pages/AdminReports.tsx b/frontend/src/pages/AdminReports.tsx index a91deb8..9b18cec 100644 --- a/frontend/src/pages/AdminReports.tsx +++ b/frontend/src/pages/AdminReports.tsx @@ -11,6 +11,7 @@ import { updateReport, type ContentReport, } from "../api/public-client"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; const STATUS_OPTIONS = [ { value: "", label: "All" }, @@ -49,6 +50,7 @@ function reportTypeLabel(rt: string): string { } export default function AdminReports() { + useDocumentTitle("Content Reports — Chrysopedia"); const [reports, setReports] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); diff --git a/frontend/src/pages/CreatorDetail.tsx b/frontend/src/pages/CreatorDetail.tsx index ced4bca..e18c338 100644 --- a/frontend/src/pages/CreatorDetail.tsx +++ b/frontend/src/pages/CreatorDetail.tsx @@ -16,6 +16,7 @@ import { import CreatorAvatar from "../components/CreatorAvatar"; import { catSlug } from "../utils/catSlug"; import TagList from "../components/TagList"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; export default function CreatorDetail() { const { slug } = useParams<{ slug: string }>(); @@ -25,6 +26,8 @@ export default function CreatorDetail() { const [notFound, setNotFound] = useState(false); const [error, setError] = useState(null); + useDocumentTitle(creator ? `${creator.name} — Chrysopedia` : "Chrysopedia"); + useEffect(() => { if (!slug) return; diff --git a/frontend/src/pages/CreatorsBrowse.tsx b/frontend/src/pages/CreatorsBrowse.tsx index 9cb0256..9450b16 100644 --- a/frontend/src/pages/CreatorsBrowse.tsx +++ b/frontend/src/pages/CreatorsBrowse.tsx @@ -15,6 +15,7 @@ import { type CreatorBrowseItem, } from "../api/public-client"; import CreatorAvatar from "../components/CreatorAvatar"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; const GENRES = [ "Bass music", @@ -41,6 +42,7 @@ const SORT_OPTIONS: { value: SortMode; label: string }[] = [ ]; export default function CreatorsBrowse() { + useDocumentTitle("Creators — Chrysopedia"); const [creators, setCreators] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 6371f0f..1a323df 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -10,6 +10,7 @@ import SearchAutocomplete from "../components/SearchAutocomplete"; import TagList from "../components/TagList"; import { useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { fetchTechniques, fetchTopics, @@ -18,6 +19,7 @@ import { } from "../api/public-client"; export default function Home() { + useDocumentTitle("Chrysopedia — Production Knowledge, Distilled"); const [featured, setFeatured] = useState(null); const [recent, setRecent] = useState([]); const [recentLoading, setRecentLoading] = useState(true); diff --git a/frontend/src/pages/SearchResults.tsx b/frontend/src/pages/SearchResults.tsx index 26cb3dd..5713990 100644 --- a/frontend/src/pages/SearchResults.tsx +++ b/frontend/src/pages/SearchResults.tsx @@ -12,12 +12,15 @@ import { searchApi, type SearchResultItem } from "../api/public-client"; import { catSlug } from "../utils/catSlug"; import SearchAutocomplete from "../components/SearchAutocomplete"; import TagList from "../components/TagList"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; export default function SearchResults() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const q = searchParams.get("q") ?? ""; + useDocumentTitle(q ? `Search: ${q} — Chrysopedia` : "Search — Chrysopedia"); + const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); diff --git a/frontend/src/pages/SubTopicPage.tsx b/frontend/src/pages/SubTopicPage.tsx index c21b7cf..c0b4fd9 100644 --- a/frontend/src/pages/SubTopicPage.tsx +++ b/frontend/src/pages/SubTopicPage.tsx @@ -13,6 +13,7 @@ import { } from "../api/public-client"; import { catSlug } from "../utils/catSlug"; import TagList from "../components/TagList"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; /** Convert a URL slug to a display name: replace hyphens with spaces, title-case. */ function slugToDisplayName(slug: string): string { @@ -51,6 +52,12 @@ export default function SubTopicPage() { const categoryDisplay = category ? slugToDisplayName(category) : ""; const subtopicDisplay = subtopic ? slugToDisplayName(subtopic) : ""; + useDocumentTitle( + subtopicDisplay && categoryDisplay + ? `${subtopicDisplay} — ${categoryDisplay} — Chrysopedia` + : "Chrysopedia", + ); + useEffect(() => { if (!category || !subtopic) return; diff --git a/frontend/src/pages/TechniquePage.tsx b/frontend/src/pages/TechniquePage.tsx index c670987..dd2bbd9 100644 --- a/frontend/src/pages/TechniquePage.tsx +++ b/frontend/src/pages/TechniquePage.tsx @@ -19,6 +19,7 @@ import { import ReportIssueModal from "../components/ReportIssueModal"; import CopyLinkButton from "../components/CopyLinkButton"; import CreatorAvatar from "../components/CreatorAvatar"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; function formatTime(seconds: number): string { const m = Math.floor(seconds / 60); @@ -73,6 +74,8 @@ export default function TechniquePage() { const [error, setError] = useState(null); const [showReport, setShowReport] = useState(false); + useDocumentTitle(technique ? `${technique.title} — Chrysopedia` : "Chrysopedia"); + // Version switching const [versions, setVersions] = useState([]); const [selectedVersion, setSelectedVersion] = useState("current"); diff --git a/frontend/src/pages/TopicsBrowse.tsx b/frontend/src/pages/TopicsBrowse.tsx index eea0412..5cb140a 100644 --- a/frontend/src/pages/TopicsBrowse.tsx +++ b/frontend/src/pages/TopicsBrowse.tsx @@ -14,10 +14,12 @@ import { Link } from "react-router-dom"; import { fetchTopics, type TopicCategory } from "../api/public-client"; import { CATEGORY_ICON } from "../components/CategoryIcons"; import { catSlug } from "../utils/catSlug"; +import { useDocumentTitle } from "../hooks/useDocumentTitle"; export default function TopicsBrowse() { + useDocumentTitle("Topics — Chrysopedia"); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index be7f618..7844bb1 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file