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 { 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">
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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