From 2f7540d43f04978c13e2bb54e488a41be6cd8efe Mon Sep 17 00:00:00 2001
From: Foysal Ahamed <cfaion341@gmail.com>
Date: Mon, 29 Jan 2024 16:33:56 +0100
Subject: [PATCH 1/2] :sparkles: Add dynamic page title to all pages

---
 app/communication-template/[id]/edit/page.tsx |  6 ++++
 app/communication-template/create/page.tsx    |  6 ++++
 app/communication-template/page.tsx           |  7 ++++-
 app/events/[id]/page.tsx                      | 19 ++++++++++---
 app/events/page-content.tsx                   |  5 ++++
 app/reports/page-content.tsx                  | 28 ++++++++++++++++++-
 app/repositories/[id]/[...record]/page.tsx    | 25 +++++++++++++++++
 app/repositories/[id]/page-content.tsx        | 25 +++++++++++++++++
 app/repositories/page-content.tsx             | 13 +++++++++
 app/subject-status/page.tsx                   | 11 ++++++++
 app/surprise-me/page.tsx                      |  6 +++-
 components/shell/LoginModal.tsx               |  7 +++++
 12 files changed, 151 insertions(+), 7 deletions(-)

diff --git a/app/communication-template/[id]/edit/page.tsx b/app/communication-template/[id]/edit/page.tsx
index 6b004e30..621337dd 100644
--- a/app/communication-template/[id]/edit/page.tsx
+++ b/app/communication-template/[id]/edit/page.tsx
@@ -1,8 +1,14 @@
 'use client'
 
 import { CommunicationTemplateForm } from 'components/communication-template/form'
+import { useEffect } from 'react'
 
 export default function CommunicationTemplateEditPage({ params: { id } }) {
+  // Change page title dynamically
+  useEffect(() => {
+    document.title = `Edit Communication Templates #${id}`
+  }, [id])
+
   return (
     <div className="w-5/6 md:w-2/3 lg:w-1/2 mx-auto">
       <h2 className="text-gray-600 font-semibold mb-3 pb-2 mt-4 border-b border-gray-300">
diff --git a/app/communication-template/create/page.tsx b/app/communication-template/create/page.tsx
index 430fb34f..9408404a 100644
--- a/app/communication-template/create/page.tsx
+++ b/app/communication-template/create/page.tsx
@@ -1,8 +1,14 @@
 'use client'
 
 import { CommunicationTemplateForm } from 'components/communication-template/form'
+import { useEffect } from 'react'
 
 export default function CommunicationTemplateCreatePage() {
+  // Change page title dynamically
+  useEffect(() => {
+    document.title = `Create Communication Templates`
+  }, [])
+
   return (
     <div className="w-5/6 md:w-2/3 lg:w-1/2 mx-auto">
       <h2 className="text-gray-600 font-semibold mb-3 pb-2 mt-4 border-b border-gray-300">
diff --git a/app/communication-template/page.tsx b/app/communication-template/page.tsx
index aa69b781..4c999351 100644
--- a/app/communication-template/page.tsx
+++ b/app/communication-template/page.tsx
@@ -1,7 +1,7 @@
 'use client'
 
 import format from 'date-fns/format'
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
 import Link from 'next/link'
 import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/20/solid'
 
@@ -17,6 +17,11 @@ export default function CommunicationTemplatePage() {
     string | undefined
   >()
 
+  // Change page title dynamically
+  useEffect(() => {
+    document.title = `Communication Templates`
+  }, [])
+
   if (isLoading) {
     return <Loading message="Loading templates" />
   }
diff --git a/app/events/[id]/page.tsx b/app/events/[id]/page.tsx
index 390527ec..d9ee124b 100644
--- a/app/events/[id]/page.tsx
+++ b/app/events/[id]/page.tsx
@@ -3,13 +3,12 @@ import { useQuery } from '@tanstack/react-query'
 import client from '@/lib/client'
 import { Loading, LoadingFailed } from '@/common/Loader'
 import { EventView } from '@/mod-event/View'
+import { useEffect } from 'react'
+import { MOD_EVENT_TITLES } from '@/mod-event/constants'
 
 export default function EventViewPage({ params }: { params: { id: string } }) {
   const id = decodeURIComponent(params.id)
-  const {
-    data: event,
-    error,
-  } = useQuery({
+  const { data: event, error } = useQuery({
     queryKey: ['event', { id }],
     queryFn: async () => {
       const { data } = await client.api.com.atproto.admin.getModerationEvent(
@@ -20,6 +19,18 @@ export default function EventViewPage({ params }: { params: { id: string } }) {
     },
   })
 
+  // Change page title dynamically
+  // Use a human-readable event name once event details are fetched
+  useEffect(() => {
+    if (event) {
+      const eventTitle =
+        MOD_EVENT_TITLES[event.event.$type as string] || 'Moderation'
+      document.title = `${eventTitle} Event #${id}`
+    } else {
+      document.title = `Moderation Event #${id}`
+    }
+  }, [id, event])
+
   if (error) {
     return <LoadingFailed error={error} />
   }
diff --git a/app/events/page-content.tsx b/app/events/page-content.tsx
index d1490aaf..7a2b5841 100644
--- a/app/events/page-content.tsx
+++ b/app/events/page-content.tsx
@@ -3,6 +3,7 @@ import { emitEvent } from '@/mod-event/helpers/emitEvent'
 import { ComAtprotoAdminEmitModerationEvent } from '@atproto/api'
 import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'
 import { usePathname, useRouter, useSearchParams } from 'next/navigation'
+import { useEffect } from 'react'
 
 export default function EventListPageContent() {
   const searchParams = useSearchParams()
@@ -19,6 +20,10 @@ export default function EventListPageContent() {
     router.push((pathname ?? '') + '?' + newParams.toString())
   }
 
+  useEffect(() => {
+    document.title = `Moderation Events`
+  }, [])
+
   return (
     <div>
       <div className="w-5/6 sm:w-3/4 md:w-2/3 lg:w-1/2 mx-auto my-4">
diff --git a/app/reports/page-content.tsx b/app/reports/page-content.tsx
index 186838f4..b19425ed 100644
--- a/app/reports/page-content.tsx
+++ b/app/reports/page-content.tsx
@@ -1,5 +1,5 @@
 'use client'
-import { useContext, useCallback, Suspense } from 'react'
+import { useContext, useCallback, Suspense, useEffect } from 'react'
 import {
   ReadonlyURLSearchParams,
   usePathname,
@@ -197,6 +197,32 @@ export const ReportsPageContent = () => {
     ),
   )
 
+  useEffect(() => {
+    const titleFromTab =
+      currentTab === 'all'
+        ? `All subjects`
+        : `${currentTab[0].toUpperCase()}${currentTab.slice(1)}`
+    const additionalFragments: string[] = []
+
+    if (takendown) {
+      additionalFragments.push('Taken Down')
+    }
+
+    if (includeMuted) {
+      additionalFragments.push('Include Muted')
+    }
+
+    if (appealed) {
+      additionalFragments.push('Appealed')
+    }
+
+    const additionalTitle = additionalFragments.length
+      ? ` (${additionalFragments.join(', ')})`
+      : ''
+    const title = `Queue - ${titleFromTab}${additionalTitle}`
+    document.title = title
+  }, [currentTab, takendown, includeMuted, appealed])
+
   return (
     <>
       <SectionHeader title="Queue" tabs={TABS} current={currentTab}>
diff --git a/app/repositories/[id]/[...record]/page.tsx b/app/repositories/[id]/[...record]/page.tsx
index 55ee3694..5769f180 100644
--- a/app/repositories/[id]/[...record]/page.tsx
+++ b/app/repositories/[id]/[...record]/page.tsx
@@ -116,6 +116,31 @@ export default function Record({
     }
   }, [data, reportUri])
 
+  // Change title dynamically
+  // Show the collection name
+  // Once we retrieve the profile/repo details, show the handle
+  useEffect(() => {
+    let title = `Record Details`
+
+    if (collection) {
+      const titleFromCollection = collection.split('.').pop()
+      if (titleFromCollection) {
+        title =
+          titleFromCollection[0].toUpperCase() + titleFromCollection.slice(1)
+      }
+    }
+
+    if (data?.record?.repo) {
+      title += ` - ${data.record.repo.handle}`
+    }
+
+    if (rkey) {
+      title += ` - ${rkey}`
+    }
+
+    document.title = title
+  }, [data, collection])
+
   if (error) {
     return <LoadingFailed error={error} />
   }
diff --git a/app/repositories/[id]/page-content.tsx b/app/repositories/[id]/page-content.tsx
index abfe7c93..469d3c6e 100644
--- a/app/repositories/[id]/page-content.tsx
+++ b/app/repositories/[id]/page-content.tsx
@@ -6,6 +6,7 @@ import { ComAtprotoAdminEmitModerationEvent } from '@atproto/api'
 import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'
 import { usePathname, useRouter, useSearchParams } from 'next/navigation'
 import { emitEvent } from '@/mod-event/helpers/emitEvent'
+import { useEffect } from 'react'
 
 export function RepositoryViewPageContent({ id }: { id: string }) {
   const {
@@ -27,6 +28,30 @@ export function RepositoryViewPageContent({ id }: { id: string }) {
     }
     router.push((pathname ?? '') + '?' + newParams.toString())
   }
+  const tab = searchParams.get('tab')
+
+  // Change title dynamically
+  // Once we retrieve the profile/repo details, show the handle
+  // Show the current tab name from account view
+  useEffect(() => {
+    let title = `Repository Details`
+    const titleFragments: string[] = [title]
+    const titleFromTab = tab ? tab[0].toUpperCase() + tab.slice(1) : ''
+
+    if (titleFromTab) {
+      titleFragments.unshift(titleFromTab)
+    }
+
+    if (profile) {
+      titleFragments.unshift(profile.handle)
+    } else if (repo) {
+      titleFragments.unshift(repo.handle)
+    } else {
+      titleFragments.unshift(id)
+    }
+
+    document.title = titleFragments.join(' - ')
+  }, [id, repo, profile, tab])
 
   return (
     <>
diff --git a/app/repositories/page-content.tsx b/app/repositories/page-content.tsx
index 9de8d7cf..e5e08916 100644
--- a/app/repositories/page-content.tsx
+++ b/app/repositories/page-content.tsx
@@ -3,6 +3,7 @@ import { RepositoriesTable } from '@/repositories/RepositoriesTable'
 import { useSearchParams } from 'next/navigation'
 import { useInfiniteQuery } from '@tanstack/react-query'
 import client from '@/lib/client'
+import { useEffect } from 'react'
 
 export default function RepositoriesListPage() {
   const params = useSearchParams()
@@ -22,6 +23,18 @@ export default function RepositoriesListPage() {
     },
     getNextPageParam: (lastPage) => lastPage.cursor,
   })
+
+  // Change title dynamically, if there's a search term, include that
+  useEffect(() => {
+    let title = `Repositories`
+
+    if (term) {
+      title += ` - ${term}`
+    }
+
+    document.title = title
+  }, [term])
+
   const repos = data?.pages.flatMap((page) => page.repos) ?? []
   return (
     <>
diff --git a/app/subject-status/page.tsx b/app/subject-status/page.tsx
index 9c0ace5a..d7397cac 100644
--- a/app/subject-status/page.tsx
+++ b/app/subject-status/page.tsx
@@ -4,6 +4,7 @@ import client from '@/lib/client'
 import { Loading, LoadingFailed } from '@/common/Loader'
 import { useSearchParams } from 'next/navigation'
 import { SubjectStatusView } from '@/subject/StatusView'
+import { useEffect } from 'react'
 
 export default function SubjectStatus() {
   const params = useSearchParams()
@@ -21,6 +22,16 @@ export default function SubjectStatus() {
     },
   })
 
+  useEffect(() => {
+    let title = `Subject Status`
+
+    if (data?.subjectStatuses[0]) {
+      title = `${data.subjectStatuses[0].subjectRepoHandle} - ${title}`
+    }
+
+    document.title = title
+  }, [data])
+
   if (status === 'loading') {
     return <Loading />
   }
diff --git a/app/surprise-me/page.tsx b/app/surprise-me/page.tsx
index cb74bc46..a6e71e7a 100644
--- a/app/surprise-me/page.tsx
+++ b/app/surprise-me/page.tsx
@@ -1,5 +1,5 @@
 'use client'
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
 import { useInterval } from 'react-use'
 import dynamic from 'next/dynamic'
 
@@ -38,6 +38,10 @@ const Timer = () => {
 // Right now, we only serve the tetris game here, in the future, we want to rotate
 // between a few games/fun activities which is why it's named "surprise me"
 export default function SurpriseMePage() {
+  useEffect(() => {
+    document.title = `Take a break!`
+  }, [])
+
   return (
     <>
       {/* This is valid jsx but because of a known bug, typescript is confused */}
diff --git a/components/shell/LoginModal.tsx b/components/shell/LoginModal.tsx
index 92986d0e..d165b05e 100644
--- a/components/shell/LoginModal.tsx
+++ b/components/shell/LoginModal.tsx
@@ -43,6 +43,13 @@ export function LoginModal() {
     }
   }, [])
 
+  useEffect(() => {
+    const title = `Ozone - Authenticate`
+    if (!isLoggedIn) {
+      document.title = title
+    }
+  }, [isLoggedIn])
+
   const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
     e.preventDefault()
     e.stopPropagation()

From 73af95fb7249a15019a5e22ff33faa70a897b62d Mon Sep 17 00:00:00 2001
From: Foysal Ahamed <cfaion341@gmail.com>
Date: Tue, 6 Feb 2024 20:25:24 +0100
Subject: [PATCH 2/2] :recycle: Refactor titles to useTitle hook

---
 app/communication-template/[id]/edit/page.tsx |  7 +-
 app/communication-template/create/page.tsx    |  7 +-
 app/communication-template/page.tsx           |  8 +--
 app/events/[id]/page.tsx                      | 20 +++---
 app/events/page-content.tsx                   |  6 +-
 app/reports/page-content.tsx                  | 69 ++++++++++++-------
 app/repositories/[id]/[...record]/page.tsx    | 60 +++++++++-------
 app/repositories/[id]/page-content.tsx        | 50 +++++++-------
 app/repositories/page-content.tsx             | 15 ++--
 app/subject-status/page.tsx                   | 13 ++--
 app/surprise-me/page.tsx                      |  6 +-
 11 files changed, 138 insertions(+), 123 deletions(-)

diff --git a/app/communication-template/[id]/edit/page.tsx b/app/communication-template/[id]/edit/page.tsx
index 621337dd..bd06f4e2 100644
--- a/app/communication-template/[id]/edit/page.tsx
+++ b/app/communication-template/[id]/edit/page.tsx
@@ -1,13 +1,10 @@
 'use client'
 
+import { useTitle } from 'react-use'
 import { CommunicationTemplateForm } from 'components/communication-template/form'
-import { useEffect } from 'react'
 
 export default function CommunicationTemplateEditPage({ params: { id } }) {
-  // Change page title dynamically
-  useEffect(() => {
-    document.title = `Edit Communication Templates #${id}`
-  }, [id])
+  useTitle(`Edit Communication Templates #${id}`)
 
   return (
     <div className="w-5/6 md:w-2/3 lg:w-1/2 mx-auto">
diff --git a/app/communication-template/create/page.tsx b/app/communication-template/create/page.tsx
index 9408404a..2efeed2e 100644
--- a/app/communication-template/create/page.tsx
+++ b/app/communication-template/create/page.tsx
@@ -1,13 +1,10 @@
 'use client'
 
+import { useTitle } from 'react-use'
 import { CommunicationTemplateForm } from 'components/communication-template/form'
-import { useEffect } from 'react'
 
 export default function CommunicationTemplateCreatePage() {
-  // Change page title dynamically
-  useEffect(() => {
-    document.title = `Create Communication Templates`
-  }, [])
+  useTitle(`Create Communication Templates`)
 
   return (
     <div className="w-5/6 md:w-2/3 lg:w-1/2 mx-auto">
diff --git a/app/communication-template/page.tsx b/app/communication-template/page.tsx
index 4c999351..cc38a0f2 100644
--- a/app/communication-template/page.tsx
+++ b/app/communication-template/page.tsx
@@ -1,9 +1,10 @@
 'use client'
 
 import format from 'date-fns/format'
-import { useEffect, useState } from 'react'
+import { useState } from 'react'
 import Link from 'next/link'
 import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/20/solid'
+import { useTitle } from 'react-use'
 
 import { LabelChip } from '@/common/labels'
 import { Loading, LoadingFailed } from '@/common/Loader'
@@ -17,10 +18,7 @@ export default function CommunicationTemplatePage() {
     string | undefined
   >()
 
-  // Change page title dynamically
-  useEffect(() => {
-    document.title = `Communication Templates`
-  }, [])
+  useTitle(`Communication Templates`)
 
   if (isLoading) {
     return <Loading message="Loading templates" />
diff --git a/app/events/[id]/page.tsx b/app/events/[id]/page.tsx
index d9ee124b..dbda8284 100644
--- a/app/events/[id]/page.tsx
+++ b/app/events/[id]/page.tsx
@@ -1,9 +1,9 @@
 'use client'
 import { useQuery } from '@tanstack/react-query'
+import { useTitle } from 'react-use'
 import client from '@/lib/client'
 import { Loading, LoadingFailed } from '@/common/Loader'
 import { EventView } from '@/mod-event/View'
-import { useEffect } from 'react'
 import { MOD_EVENT_TITLES } from '@/mod-event/constants'
 
 export default function EventViewPage({ params }: { params: { id: string } }) {
@@ -19,17 +19,13 @@ export default function EventViewPage({ params }: { params: { id: string } }) {
     },
   })
 
-  // Change page title dynamically
-  // Use a human-readable event name once event details are fetched
-  useEffect(() => {
-    if (event) {
-      const eventTitle =
-        MOD_EVENT_TITLES[event.event.$type as string] || 'Moderation'
-      document.title = `${eventTitle} Event #${id}`
-    } else {
-      document.title = `Moderation Event #${id}`
-    }
-  }, [id, event])
+  let pageTitle = `Moderation Event #${id}`
+  if (event) {
+    const eventTitle =
+      MOD_EVENT_TITLES[event.event.$type as string] || 'Moderation'
+    pageTitle = `${eventTitle} Event #${id}`
+  }
+  useTitle(pageTitle)
 
   if (error) {
     return <LoadingFailed error={error} />
diff --git a/app/events/page-content.tsx b/app/events/page-content.tsx
index 7a2b5841..07f55904 100644
--- a/app/events/page-content.tsx
+++ b/app/events/page-content.tsx
@@ -1,9 +1,9 @@
+import { useTitle } from 'react-use'
 import { ModEventList } from '@/mod-event/EventList'
 import { emitEvent } from '@/mod-event/helpers/emitEvent'
 import { ComAtprotoAdminEmitModerationEvent } from '@atproto/api'
 import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'
 import { usePathname, useRouter, useSearchParams } from 'next/navigation'
-import { useEffect } from 'react'
 
 export default function EventListPageContent() {
   const searchParams = useSearchParams()
@@ -20,9 +20,7 @@ export default function EventListPageContent() {
     router.push((pathname ?? '') + '?' + newParams.toString())
   }
 
-  useEffect(() => {
-    document.title = `Moderation Events`
-  }, [])
+  useTitle(`Moderation Events`)
 
   return (
     <div>
diff --git a/app/reports/page-content.tsx b/app/reports/page-content.tsx
index b19425ed..7161a7aa 100644
--- a/app/reports/page-content.tsx
+++ b/app/reports/page-content.tsx
@@ -22,6 +22,7 @@ import { AuthContext } from '@/shell/AuthContext'
 import { ButtonGroup } from '@/common/buttons'
 import { useFluentReportSearch } from '@/reports/useFluentReportSearch'
 import { SubjectTable } from 'components/subject/table'
+import { useTitle } from 'react-use'
 
 const TABS = [
   {
@@ -48,6 +49,42 @@ const TABS = [
   { key: 'all', name: 'All', href: '/reports' },
 ]
 
+const buildPageTitle = ({
+  currentTab,
+  takendown,
+  includeMuted,
+  appealed,
+}: {
+  currentTab: string
+  takendown: boolean
+  includeMuted: boolean
+  appealed: boolean
+}) => {
+  const titleFromTab =
+    currentTab === 'all'
+      ? `All subjects`
+      : `${currentTab[0].toUpperCase()}${currentTab.slice(1)}`
+  const additionalFragments: string[] = []
+
+  if (takendown) {
+    additionalFragments.push('Taken Down')
+  }
+
+  if (includeMuted) {
+    additionalFragments.push('Include Muted')
+  }
+
+  if (appealed) {
+    additionalFragments.push('Appealed')
+  }
+
+  const additionalTitle = additionalFragments.length
+    ? ` (${additionalFragments.join(', ')})`
+    : ''
+  const title = `Queue - ${titleFromTab}${additionalTitle}`
+  return title
+}
+
 const ResolvedFilters = () => {
   const router = useRouter()
   const pathname = usePathname()
@@ -197,31 +234,13 @@ export const ReportsPageContent = () => {
     ),
   )
 
-  useEffect(() => {
-    const titleFromTab =
-      currentTab === 'all'
-        ? `All subjects`
-        : `${currentTab[0].toUpperCase()}${currentTab.slice(1)}`
-    const additionalFragments: string[] = []
-
-    if (takendown) {
-      additionalFragments.push('Taken Down')
-    }
-
-    if (includeMuted) {
-      additionalFragments.push('Include Muted')
-    }
-
-    if (appealed) {
-      additionalFragments.push('Appealed')
-    }
-
-    const additionalTitle = additionalFragments.length
-      ? ` (${additionalFragments.join(', ')})`
-      : ''
-    const title = `Queue - ${titleFromTab}${additionalTitle}`
-    document.title = title
-  }, [currentTab, takendown, includeMuted, appealed])
+  const pageTitle = buildPageTitle({
+    currentTab,
+    takendown,
+    includeMuted,
+    appealed,
+  })
+  useTitle(pageTitle)
 
   return (
     <>
diff --git a/app/repositories/[id]/[...record]/page.tsx b/app/repositories/[id]/[...record]/page.tsx
index 5769f180..ad81ea5a 100644
--- a/app/repositories/[id]/[...record]/page.tsx
+++ b/app/repositories/[id]/[...record]/page.tsx
@@ -15,6 +15,36 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
 import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'
 import { emitEvent } from '@/mod-event/helpers/emitEvent'
 import { useEffect } from 'react'
+import { useTitle } from 'react-use'
+
+const buildPageTitle = ({
+  handle,
+  collection,
+  rkey,
+}: {
+  handle?: string
+  collection?: string
+  rkey?: string
+}) => {
+  let title = `Record Details`
+
+  if (collection) {
+    const titleFromCollection = collection.split('.').pop()
+    if (titleFromCollection) {
+      title =
+        titleFromCollection[0].toUpperCase() + titleFromCollection.slice(1)
+    }
+  }
+
+  if (handle) {
+    title += ` - ${handle}`
+  }
+
+  if (rkey) {
+    title += ` - ${rkey}`
+  }
+  return title
+}
 
 export default function Record({
   params,
@@ -116,30 +146,12 @@ export default function Record({
     }
   }, [data, reportUri])
 
-  // Change title dynamically
-  // Show the collection name
-  // Once we retrieve the profile/repo details, show the handle
-  useEffect(() => {
-    let title = `Record Details`
-
-    if (collection) {
-      const titleFromCollection = collection.split('.').pop()
-      if (titleFromCollection) {
-        title =
-          titleFromCollection[0].toUpperCase() + titleFromCollection.slice(1)
-      }
-    }
-
-    if (data?.record?.repo) {
-      title += ` - ${data.record.repo.handle}`
-    }
-
-    if (rkey) {
-      title += ` - ${rkey}`
-    }
-
-    document.title = title
-  }, [data, collection])
+  const pageTitle = buildPageTitle({
+    handle: data?.record?.repo.handle,
+    rkey,
+    collection,
+  })
+  useTitle(pageTitle)
 
   if (error) {
     return <LoadingFailed error={error} />
diff --git a/app/repositories/[id]/page-content.tsx b/app/repositories/[id]/page-content.tsx
index 469d3c6e..2a8f29b2 100644
--- a/app/repositories/[id]/page-content.tsx
+++ b/app/repositories/[id]/page-content.tsx
@@ -6,8 +6,29 @@ import { ComAtprotoAdminEmitModerationEvent } from '@atproto/api'
 import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction'
 import { usePathname, useRouter, useSearchParams } from 'next/navigation'
 import { emitEvent } from '@/mod-event/helpers/emitEvent'
-import { useEffect } from 'react'
+import { useTitle } from 'react-use'
 
+const buildPageTitle = ({
+  handle,
+  tab,
+}: {
+  handle: string
+  tab: string | null
+}) => {
+  let title = `Repository Details`
+  const titleFragments: string[] = [title]
+  const titleFromTab = tab ? tab[0].toUpperCase() + tab.slice(1) : ''
+
+  if (titleFromTab) {
+    titleFragments.unshift(titleFromTab)
+  }
+
+  if (handle) {
+    titleFragments.unshift(handle)
+  }
+
+  return titleFragments.join(' - ')
+}
 export function RepositoryViewPageContent({ id }: { id: string }) {
   const {
     error,
@@ -30,28 +51,11 @@ export function RepositoryViewPageContent({ id }: { id: string }) {
   }
   const tab = searchParams.get('tab')
 
-  // Change title dynamically
-  // Once we retrieve the profile/repo details, show the handle
-  // Show the current tab name from account view
-  useEffect(() => {
-    let title = `Repository Details`
-    const titleFragments: string[] = [title]
-    const titleFromTab = tab ? tab[0].toUpperCase() + tab.slice(1) : ''
-
-    if (titleFromTab) {
-      titleFragments.unshift(titleFromTab)
-    }
-
-    if (profile) {
-      titleFragments.unshift(profile.handle)
-    } else if (repo) {
-      titleFragments.unshift(repo.handle)
-    } else {
-      titleFragments.unshift(id)
-    }
-
-    document.title = titleFragments.join(' - ')
-  }, [id, repo, profile, tab])
+  const pageTitle = buildPageTitle({
+    handle: profile?.handle || repo?.handle || id,
+    tab,
+  })
+  useTitle(pageTitle)
 
   return (
     <>
diff --git a/app/repositories/page-content.tsx b/app/repositories/page-content.tsx
index e5e08916..11d6bb39 100644
--- a/app/repositories/page-content.tsx
+++ b/app/repositories/page-content.tsx
@@ -4,6 +4,7 @@ import { useSearchParams } from 'next/navigation'
 import { useInfiniteQuery } from '@tanstack/react-query'
 import client from '@/lib/client'
 import { useEffect } from 'react'
+import { useTitle } from 'react-use'
 
 export default function RepositoriesListPage() {
   const params = useSearchParams()
@@ -24,16 +25,12 @@ export default function RepositoriesListPage() {
     getNextPageParam: (lastPage) => lastPage.cursor,
   })
 
-  // Change title dynamically, if there's a search term, include that
-  useEffect(() => {
-    let title = `Repositories`
+  let pageTitle = `Repositories`
+  if (term) {
+    pageTitle += ` - ${term}`
+  }
 
-    if (term) {
-      title += ` - ${term}`
-    }
-
-    document.title = title
-  }, [term])
+  useTitle(pageTitle)
 
   const repos = data?.pages.flatMap((page) => page.repos) ?? []
   return (
diff --git a/app/subject-status/page.tsx b/app/subject-status/page.tsx
index d7397cac..86247abe 100644
--- a/app/subject-status/page.tsx
+++ b/app/subject-status/page.tsx
@@ -5,6 +5,7 @@ import { Loading, LoadingFailed } from '@/common/Loader'
 import { useSearchParams } from 'next/navigation'
 import { SubjectStatusView } from '@/subject/StatusView'
 import { useEffect } from 'react'
+import { useTitle } from 'react-use'
 
 export default function SubjectStatus() {
   const params = useSearchParams()
@@ -22,15 +23,13 @@ export default function SubjectStatus() {
     },
   })
 
-  useEffect(() => {
-    let title = `Subject Status`
+  let pageTitle = `Subject Status`
 
-    if (data?.subjectStatuses[0]) {
-      title = `${data.subjectStatuses[0].subjectRepoHandle} - ${title}`
-    }
+  if (data?.subjectStatuses[0]) {
+    pageTitle = `${data.subjectStatuses[0].subjectRepoHandle} - ${pageTitle}`
+  }
 
-    document.title = title
-  }, [data])
+  useTitle(pageTitle)
 
   if (status === 'loading') {
     return <Loading />
diff --git a/app/surprise-me/page.tsx b/app/surprise-me/page.tsx
index a6e71e7a..11892276 100644
--- a/app/surprise-me/page.tsx
+++ b/app/surprise-me/page.tsx
@@ -1,6 +1,6 @@
 'use client'
 import { useEffect, useState } from 'react'
-import { useInterval } from 'react-use'
+import { useInterval, useTitle } from 'react-use'
 import dynamic from 'next/dynamic'
 
 // The game package uses some client only code so we can't really import and use it here because that breaks SSR for some reason
@@ -38,9 +38,7 @@ const Timer = () => {
 // Right now, we only serve the tetris game here, in the future, we want to rotate
 // between a few games/fun activities which is why it's named "surprise me"
 export default function SurpriseMePage() {
-  useEffect(() => {
-    document.title = `Take a break!`
-  }, [])
+  useTitle(`Take a break!`)
 
   return (
     <>