diff --git a/spa/src/hooks/use-document-title.ts b/spa/src/hooks/use-document-title.ts
new file mode 100644
index 0000000..1d2006a
--- /dev/null
+++ b/spa/src/hooks/use-document-title.ts
@@ -0,0 +1,12 @@
+import { useEffect } from "react"
+
+const BASE_TITLE = "Movies Diary"
+
+export function useDocumentTitle(title?: string) {
+ useEffect(() => {
+ document.title = title ? `${title} — ${BASE_TITLE}` : BASE_TITLE
+ return () => {
+ document.title = BASE_TITLE
+ }
+ }, [title])
+}
diff --git a/spa/src/routes/_app/diary.tsx b/spa/src/routes/_app/diary.tsx
index 3161d21..fde59db 100644
--- a/spa/src/routes/_app/diary.tsx
+++ b/spa/src/routes/_app/diary.tsx
@@ -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" })
diff --git a/spa/src/routes/_app/movies.$id.tsx b/spa/src/routes/_app/movies.$id.tsx
index a4bf900..0732a59 100644
--- a/spa/src/routes/_app/movies.$id.tsx
+++ b/spa/src/routes/_app/movies.$id.tsx
@@ -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 (
diff --git a/spa/src/routes/_app/people.$id.tsx b/spa/src/routes/_app/people.$id.tsx
index 00a11d9..ca6c13a 100644
--- a/spa/src/routes/_app/people.$id.tsx
+++ b/spa/src/routes/_app/people.$id.tsx
@@ -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 (
diff --git a/spa/src/routes/_app/profile.tsx b/spa/src/routes/_app/profile.tsx
index 694092d..618a289 100644
--- a/spa/src/routes/_app/profile.tsx
+++ b/spa/src/routes/_app/profile.tsx
@@ -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",
})
diff --git a/spa/src/routes/_app/search.tsx b/spa/src/routes/_app/search.tsx
index c17d6a0..e36e8fc 100644
--- a/spa/src/routes/_app/search.tsx
+++ b/spa/src/routes/_app/search.tsx
@@ -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)
diff --git a/spa/src/routes/_app/settings/blocked.tsx b/spa/src/routes/_app/settings/blocked.tsx
index d8c6dbd..02039c3 100644
--- a/spa/src/routes/_app/settings/blocked.tsx
+++ b/spa/src/routes/_app/settings/blocked.tsx
@@ -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 (
diff --git a/spa/src/routes/_app/settings/edit-profile.tsx b/spa/src/routes/_app/settings/edit-profile.tsx
index 04c43af..de81005 100644
--- a/spa/src/routes/_app/settings/edit-profile.tsx
+++ b/spa/src/routes/_app/settings/edit-profile.tsx
@@ -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()
diff --git a/spa/src/routes/_app/settings/import.tsx b/spa/src/routes/_app/settings/import.tsx
index dd47088..b5eb5d1 100644
--- a/spa/src/routes/_app/settings/import.tsx
+++ b/spa/src/routes/_app/settings/import.tsx
@@ -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") },
diff --git a/spa/src/routes/_app/settings/index.tsx b/spa/src/routes/_app/settings/index.tsx
index 449e979..a789409 100644
--- a/spa/src/routes/_app/settings/index.tsx
+++ b/spa/src/routes/_app/settings/index.tsx
@@ -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()
diff --git a/spa/src/routes/_app/settings/webhooks.tsx b/spa/src/routes/_app/settings/webhooks.tsx
index eb7f5e3..b01e078 100644
--- a/spa/src/routes/_app/settings/webhooks.tsx
+++ b/spa/src/routes/_app/settings/webhooks.tsx
@@ -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()
diff --git a/spa/src/routes/_app/settings/wrapup.tsx b/spa/src/routes/_app/settings/wrapup.tsx
index 060c89d..3b0f467 100644
--- a/spa/src/routes/_app/settings/wrapup.tsx
+++ b/spa/src/routes/_app/settings/wrapup.tsx
@@ -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()
diff --git a/spa/src/routes/_app/social.tsx b/spa/src/routes/_app/social.tsx
index c3f50a7..b5fbc82 100644
--- a/spa/src/routes/_app/social.tsx
+++ b/spa/src/routes/_app/social.tsx
@@ -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
diff --git a/spa/src/routes/_app/users.$id.tsx b/spa/src/routes/_app/users.$id.tsx
index 88c5f04..dd3c83c 100644
--- a/spa/src/routes/_app/users.$id.tsx
+++ b/spa/src/routes/_app/users.$id.tsx
@@ -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
if (!data) return null
+ useDocumentTitle(data?.username)
const isSelf = auth?.user_id === id
const isFollowing = followingData?.actors.some((a) => a.handle === data.username) ?? false
diff --git a/spa/src/routes/_app/wrapup.$id.tsx b/spa/src/routes/_app/wrapup.$id.tsx
index 6ff8a8b..c41b75a 100644
--- a/spa/src/routes/_app/wrapup.$id.tsx
+++ b/spa/src/routes/_app/wrapup.$id.tsx
@@ -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
if (!report) return null