feat: dynamic page titles across SPA
useDocumentTitle hook sets document.title per page. Dynamic: movie name, person name, username, wrapup year. Static: diary, profile, search, social, all settings pages.
This commit is contained in:
@@ -10,6 +10,7 @@ import { VirtualList } from "@/components/virtual-list"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useInfiniteDiary, useDeleteReview } from "@/hooks/use-diary"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import type { DiaryEntryDto } from "@/lib/api/common"
|
||||
|
||||
export const Route = createFileRoute("/_app/diary")({
|
||||
@@ -27,6 +28,7 @@ function groupByDate(items: DiaryEntryDto[]) {
|
||||
|
||||
function DiaryPage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("diary.title"))
|
||||
const [month, setMonth] = useState(() => startOfMonth(new Date()))
|
||||
const { data, isPending, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteDiary({ sort_by: "desc" })
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { posterUrl, tmdbProfileUrl } from "@/lib/api/client"
|
||||
import { timeAgo, shortDate } from "@/lib/date"
|
||||
import { useMovie, useMovieHistory, useMovieProfile } from "@/hooks/use-movies"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import {
|
||||
useWatchlistStatus,
|
||||
useAddToWatchlist,
|
||||
@@ -34,6 +35,7 @@ function MovieDetailPage() {
|
||||
if (!data) return null
|
||||
|
||||
const { movie, stats, reviews } = data
|
||||
useDocumentTitle(movie.title)
|
||||
const hasStats = profile && (profile.budget_usd != null || profile.revenue_usd != null || profile.vote_average != null)
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EmptyState } from "@/components/empty-state"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { tmdbProfileUrl } from "@/lib/api/client"
|
||||
import { usePersonCredits } from "@/hooks/use-search"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
|
||||
export const Route = createFileRoute("/_app/people/$id")({
|
||||
component: PersonDetailPage,
|
||||
@@ -21,6 +22,7 @@ function PersonDetailPage() {
|
||||
if (!data) return null
|
||||
|
||||
const { person, cast, crew } = data
|
||||
useDocumentTitle(person.name)
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useDeleteGoal } from "@/hooks/use-goals"
|
||||
import { GoalCard } from "@/components/goal-card"
|
||||
import { GoalSheet } from "@/components/goal-sheet"
|
||||
import { toast } from "sonner"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import type { GoalDto } from "@/lib/api/users"
|
||||
|
||||
export const Route = createFileRoute("/_app/profile")({
|
||||
@@ -20,6 +21,7 @@ export const Route = createFileRoute("/_app/profile")({
|
||||
function ProfilePage() {
|
||||
const { t } = useTranslation()
|
||||
const { auth } = useAuth()
|
||||
useDocumentTitle(t("profile.title"))
|
||||
const { data, isPending } = useUserProfile(auth?.user_id ?? "", {
|
||||
view: "trends",
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import { InfiniteScroll } from "@/components/infinite-scroll"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useInfiniteSearch } from "@/hooks/use-search"
|
||||
import { useDebounce } from "@/hooks/use-debounce"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import { useAddToWatchlist } from "@/hooks/use-watchlist"
|
||||
import { toast } from "sonner"
|
||||
|
||||
@@ -20,6 +21,7 @@ export const Route = createFileRoute("/_app/search")({
|
||||
|
||||
function SearchPage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("search.placeholder"))
|
||||
const addToWatchlist = useAddToWatchlist()
|
||||
const [query, setQuery] = useState("")
|
||||
const debouncedQuery = useDebounce(query, 300)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useAddBlockedDomain,
|
||||
useRemoveBlockedDomain,
|
||||
} from "@/hooks/use-social"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
|
||||
export const Route = createFileRoute("/_app/settings/blocked")({
|
||||
component: BlockedPage,
|
||||
@@ -22,6 +23,7 @@ export const Route = createFileRoute("/_app/settings/blocked")({
|
||||
|
||||
function BlockedPage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("blocked.title"))
|
||||
const isAdmin = useIsAdmin()
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Separator } from "@/components/ui/separator"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useProfile, useUpdateProfile, useUpdateProfileFields } from "@/hooks/use-users"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
|
||||
export const Route = createFileRoute("/_app/settings/edit-profile")({
|
||||
component: EditProfilePage,
|
||||
@@ -17,6 +18,7 @@ export const Route = createFileRoute("/_app/settings/edit-profile")({
|
||||
|
||||
function EditProfilePage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("editProfile.title"))
|
||||
const { data, isPending } = useProfile()
|
||||
const update = useUpdateProfile()
|
||||
const updateFields = useUpdateProfileFields()
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
useConfirmImport,
|
||||
useImportPreview,
|
||||
} from "@/hooks/use-imports"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import type { SessionCreatedResponse } from "@/lib/api/imports"
|
||||
|
||||
export const Route = createFileRoute("/_app/settings/import")({
|
||||
@@ -36,6 +37,7 @@ export const Route = createFileRoute("/_app/settings/import")({
|
||||
|
||||
function ImportPage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("import.title"))
|
||||
|
||||
const DOMAIN_FIELDS = [
|
||||
{ value: "title", label: t("import.fieldTitle") },
|
||||
|
||||
@@ -22,6 +22,7 @@ import { API_URL } from "@/lib/api/client"
|
||||
import { getToken } from "@/lib/auth"
|
||||
import { reindexSearch } from "@/lib/api/users"
|
||||
import { useSettings, useUpdateSettings } from "@/hooks/use-goals"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
|
||||
export const Route = createFileRoute("/_app/settings/")({
|
||||
component: SettingsPage,
|
||||
@@ -36,6 +37,7 @@ type SettingsItem = {
|
||||
|
||||
function SettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("settings.title"))
|
||||
const { logout } = useAuth()
|
||||
const isAdmin = useIsAdmin()
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
useDeleteToken,
|
||||
} from "@/hooks/use-webhooks"
|
||||
import { API_URL } from "@/lib/api/client"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
|
||||
export const Route = createFileRoute("/_app/settings/webhooks")({
|
||||
component: WebhooksPage,
|
||||
@@ -35,6 +36,7 @@ export const Route = createFileRoute("/_app/settings/webhooks")({
|
||||
|
||||
function WebhooksPage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("webhooks.title"))
|
||||
const { data: tokens, isPending } = useWebhookTokens()
|
||||
const generate = useGenerateToken()
|
||||
const remove = useDeleteToken()
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
useDeleteWrapUp,
|
||||
} from "@/hooks/use-wrapup"
|
||||
import { useUsers } from "@/hooks/use-users"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
|
||||
export const Route = createFileRoute("/_app/settings/wrapup")({
|
||||
component: WrapupPage,
|
||||
@@ -30,6 +31,7 @@ export const Route = createFileRoute("/_app/settings/wrapup")({
|
||||
|
||||
function WrapupPage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("wrapup.title"))
|
||||
const { auth } = useAuth()
|
||||
const isAdmin = useIsAdmin()
|
||||
const { data, isPending } = useWrapUps()
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
useUserFollowing,
|
||||
useUserFollowers,
|
||||
} from "@/hooks/use-social"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import type { RemoteActorDto } from "@/lib/api/social"
|
||||
|
||||
type SearchParams = { user?: string }
|
||||
@@ -36,6 +37,7 @@ export const Route = createFileRoute("/_app/social")({
|
||||
|
||||
function SocialPage() {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t("social.title"))
|
||||
const { user: viewUserId } = Route.useSearch()
|
||||
const { auth } = useAuth()
|
||||
const isSelf = !viewUserId || viewUserId === auth?.user_id
|
||||
|
||||
@@ -9,6 +9,7 @@ import { GoalCard } from "@/components/goal-card"
|
||||
import { useAuth } from "@/components/auth-provider"
|
||||
import { useUserProfile } from "@/hooks/use-users"
|
||||
import { useFollow, useUnfollow, useFollowing } from "@/hooks/use-social"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
|
||||
export const Route = createFileRoute("/_app/users/$id")({
|
||||
component: UserProfilePage,
|
||||
@@ -28,6 +29,7 @@ function UserProfilePage() {
|
||||
if (isPending) return <ProfileSkeleton />
|
||||
if (!data) return null
|
||||
|
||||
useDocumentTitle(data?.username)
|
||||
const isSelf = auth?.user_id === id
|
||||
const isFollowing = followingData?.actors.some((a) => a.handle === data.username) ?? false
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { RankCard } from "@/components/wrapup-rank-card"
|
||||
import { posterUrl } from "@/lib/api/client"
|
||||
import { fmtUsd } from "@/lib/format"
|
||||
import { useWrapUpReport } from "@/hooks/use-wrapup"
|
||||
import { useDocumentTitle } from "@/hooks/use-document-title"
|
||||
import type { MovieRef } from "@/lib/api/wrapup"
|
||||
|
||||
const WrapUpShareCard = lazy(() => import("@/components/wrapup-share-card").then((m) => ({ default: m.WrapUpShareCard })))
|
||||
@@ -39,6 +40,7 @@ function WrapUpReportPage() {
|
||||
const { data: report, isPending } = useWrapUpReport(id)
|
||||
const [showShare, setShowShare] = useState(false)
|
||||
|
||||
useDocumentTitle(report ? `${report.date_range.start.slice(0, 4)} Wrap-Up` : undefined)
|
||||
if (isPending) return <ReportSkeleton />
|
||||
if (!report) return null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user