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:
parent
6845f5c349
commit
f3e6a9c885
12 changed files with 51 additions and 1 deletions
22
frontend/src/hooks/useDocumentTitle.ts
Normal file
22
frontend/src/hooks/useDocumentTitle.ts
Normal 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;
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import { useDocumentTitle } from "../hooks/useDocumentTitle";
|
||||
|
||||
export default function About() {
|
||||
useDocumentTitle("About — Chrysopedia");
|
||||
return (
|
||||
<div className="about">
|
||||
<section className="about-hero">
|
||||
|
|
|
|||
|
|
@ -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<PipelineVideoItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
|
|||
|
|
@ -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<ContentReport[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
|
||||
useDocumentTitle(creator ? `${creator.name} — Chrysopedia` : "Chrysopedia");
|
||||
|
||||
useEffect(() => {
|
||||
if (!slug) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<CreatorBrowseItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
|
|||
|
|
@ -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<TechniqueListItem | null>(null);
|
||||
const [recent, setRecent] = useState<TechniqueListItem[]>([]);
|
||||
const [recentLoading, setRecentLoading] = useState(true);
|
||||
|
|
|
|||
|
|
@ -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<SearchResultItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
const [showReport, setShowReport] = useState(false);
|
||||
|
||||
useDocumentTitle(technique ? `${technique.title} — Chrysopedia` : "Chrysopedia");
|
||||
|
||||
// Version switching
|
||||
const [versions, setVersions] = useState<TechniquePageVersionSummary[]>([]);
|
||||
const [selectedVersion, setSelectedVersion] = useState<string>("current");
|
||||
|
|
|
|||
|
|
@ -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<TopicCategory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
Loading…
Add table
Reference in a new issue