refactor: migrate App Router pages to locale segments
- Move all App Router pages under [locale] dynamic segment - Update layout to handle locale params and server-side version fetch - Remove duplicate pages from root app directory - Add generateStaticParams for static generation of locale routes - Update Header component for locale-aware navigation - Update about page to use next-intl hooks 🤖 Generated with Claude Code https://claude.ai/code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b7bf44498b
commit
727549db6b
21 changed files with 154 additions and 102 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslations } from 'next-intl'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from '~/i18n/navigation'
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
|
|
@ -60,7 +60,7 @@ const ProfilePageClient: React.FC<Props> = ({
|
||||||
initialRaid,
|
initialRaid,
|
||||||
initialRecency
|
initialRecency
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('common')
|
const t = useTranslations('common')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
|
@ -234,4 +234,4 @@ const ProfilePageClient: React.FC<Props> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProfilePageClient
|
export default ProfilePageClient
|
||||||
80
app/[locale]/layout.tsx
Normal file
80
app/[locale]/layout.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { Metadata, Viewport } from 'next'
|
||||||
|
import localFont from 'next/font/local'
|
||||||
|
import { NextIntlClientProvider } from 'next-intl'
|
||||||
|
import { getMessages } from 'next-intl/server'
|
||||||
|
import { Viewport as ToastViewport } from '@radix-ui/react-toast'
|
||||||
|
import { locales } from '../../i18n.config'
|
||||||
|
|
||||||
|
import '../../styles/globals.scss'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import Providers from '../components/Providers'
|
||||||
|
import Header from '../components/Header'
|
||||||
|
import UpdateToastClient from '../components/UpdateToastClient'
|
||||||
|
import VersionHydrator from '../components/VersionHydrator'
|
||||||
|
|
||||||
|
// Generate static params for all locales
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return locales.map((locale) => ({ locale }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'granblue.team',
|
||||||
|
description: 'Create, save, and share Granblue Fantasy party compositions',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viewport configuration (Next.js 13+ requires separate export)
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: 'device-width',
|
||||||
|
initialScale: 1,
|
||||||
|
viewportFit: 'cover',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font
|
||||||
|
const goalking = localFont({
|
||||||
|
src: '../../pages/fonts/gk-variable.woff2',
|
||||||
|
fallback: ['system-ui', 'inter', 'helvetica neue', 'sans-serif'],
|
||||||
|
variable: '--font-goalking',
|
||||||
|
})
|
||||||
|
|
||||||
|
export default async function LocaleLayout({
|
||||||
|
children,
|
||||||
|
params: { locale }
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
params: { locale: string }
|
||||||
|
}) {
|
||||||
|
// Load messages for the locale
|
||||||
|
const messages = await getMessages()
|
||||||
|
|
||||||
|
// Fetch version data on the server
|
||||||
|
let version = null
|
||||||
|
try {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_URL || 'http://localhost:1234'
|
||||||
|
const res = await fetch(`${baseUrl}/api/version`, {
|
||||||
|
cache: 'no-store'
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
version = await res.json()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch version data:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang={locale} className={goalking.variable}>
|
||||||
|
<body className={goalking.className}>
|
||||||
|
<NextIntlClientProvider messages={messages}>
|
||||||
|
<Providers>
|
||||||
|
<Header />
|
||||||
|
<VersionHydrator version={version} />
|
||||||
|
<UpdateToastClient initialVersion={version} />
|
||||||
|
<main>{children}</main>
|
||||||
|
<ToastViewport className="ToastViewport" />
|
||||||
|
</Providers>
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslations } from 'next-intl'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from '~/i18n/navigation'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
|
|
@ -24,7 +24,7 @@ const NewPartyClient: React.FC<Props> = ({
|
||||||
raidGroups,
|
raidGroups,
|
||||||
error = false
|
error = false
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('common')
|
const t = useTranslations('common')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// State for tab management
|
// State for tab management
|
||||||
|
|
@ -76,4 +76,4 @@ const NewPartyClient: React.FC<Props> = ({
|
||||||
return <PartyWrapper raidGroups={raidGroups} />
|
return <PartyWrapper raidGroups={raidGroups} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NewPartyClient
|
export default NewPartyClient
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import Link from 'next/link'
|
import { Link } from '~/i18n/navigation'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { getTranslations } from 'next-intl/server'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Page not found / granblue.team',
|
title: 'Page not found / granblue.team',
|
||||||
description: 'The page you were looking for could not be found'
|
description: 'The page you were looking for could not be found'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NotFound() {
|
export default async function NotFound() {
|
||||||
|
const t = await getTranslations('common')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="error-container">
|
<div className="error-container">
|
||||||
<div className="error-content">
|
<div className="error-content">
|
||||||
|
|
@ -24,4 +26,4 @@ export default function NotFound() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslations } from 'next-intl'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from '~/i18n/navigation'
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { appState } from '~/utils/appState'
|
import { appState } from '~/utils/appState'
|
||||||
|
|
@ -20,7 +20,7 @@ interface Props {
|
||||||
|
|
||||||
const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useTranslation('common')
|
const t = useTranslations('common')
|
||||||
|
|
||||||
// State for tab management
|
// State for tab management
|
||||||
const [selectedTab, setSelectedTab] = useState<GridType>(GridType.Weapon)
|
const [selectedTab, setSelectedTab] = useState<GridType>(GridType.Weapon)
|
||||||
|
|
@ -71,4 +71,4 @@ const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PartyPageClient
|
export default PartyPageClient
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslations } from 'next-intl'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from '~/i18n/navigation'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import FilterBar from '~/components/filters/FilterBar'
|
import FilterBar from '~/components/filters/FilterBar'
|
||||||
|
|
@ -43,7 +43,7 @@ const SavedPageClient: React.FC<Props> = ({
|
||||||
initialRecency,
|
initialRecency,
|
||||||
error = false
|
error = false
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('common')
|
const t = useTranslations('common')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
|
@ -201,4 +201,4 @@ const SavedPageClient: React.FC<Props> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SavedPageClient
|
export default SavedPageClient
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslations } from 'next-intl'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from '~/i18n/navigation'
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
|
|
@ -55,7 +55,7 @@ const TeamsPageClient: React.FC<Props> = ({
|
||||||
initialRecency,
|
initialRecency,
|
||||||
error = false
|
error = false
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('common')
|
const t = useTranslations('common')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
|
@ -245,4 +245,4 @@ const TeamsPageClient: React.FC<Props> = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TeamsPageClient
|
export default TeamsPageClient
|
||||||
|
|
@ -2,11 +2,12 @@
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { deleteCookie } from 'cookies-next'
|
import { deleteCookie } from 'cookies-next'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from '~/i18n/navigation'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslations } from 'next-intl'
|
||||||
|
import { useLocale } from 'next-intl'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import clonedeep from 'lodash.clonedeep'
|
import clonedeep from 'lodash.clonedeep'
|
||||||
import Link from 'next/link'
|
import { Link } from '~/i18n/navigation'
|
||||||
|
|
||||||
import { accountState, initialAccountState } from '~/utils/accountState'
|
import { accountState, initialAccountState } from '~/utils/accountState'
|
||||||
import { appState, initialAppState } from '~/utils/appState'
|
import { appState, initialAppState } from '~/utils/appState'
|
||||||
|
|
@ -37,11 +38,11 @@ import styles from '~/components/Header/index.module.scss'
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
// Localization
|
// Localization
|
||||||
const { t } = useTranslation('common')
|
const t = useTranslations('common')
|
||||||
|
const locale = useLocale()
|
||||||
|
|
||||||
// Router
|
// Router
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const locale = 'en' // TODO: Update when implementing internationalization with App Router
|
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
const [alertOpen, setAlertOpen] = useState(false)
|
const [alertOpen, setAlertOpen] = useState(false)
|
||||||
|
|
@ -402,4 +403,4 @@ const Header = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Header
|
export default Header
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,8 @@
|
||||||
import { Metadata, Viewport } from 'next'
|
// Minimal root layout - all content is handled in [locale]/layout.tsx
|
||||||
import localFont from 'next/font/local'
|
|
||||||
import { Viewport as ToastViewport } from '@radix-ui/react-toast'
|
|
||||||
|
|
||||||
import '../styles/globals.scss'
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import Providers from './components/Providers'
|
|
||||||
import Header from './components/Header'
|
|
||||||
import UpdateToastClient from './components/UpdateToastClient'
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: 'granblue.team',
|
|
||||||
description: 'Create, save, and share Granblue Fantasy party compositions',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Viewport configuration (Next.js 13+ requires separate export)
|
|
||||||
export const viewport: Viewport = {
|
|
||||||
width: 'device-width',
|
|
||||||
initialScale: 1,
|
|
||||||
viewportFit: 'cover',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Font
|
|
||||||
const goalking = localFont({
|
|
||||||
src: '../pages/fonts/gk-variable.woff2',
|
|
||||||
fallback: ['system-ui', 'inter', 'helvetica neue', 'sans-serif'],
|
|
||||||
variable: '--font-goalking',
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return children
|
||||||
<html lang="en" className={goalking.variable}>
|
|
||||||
<body className={goalking.className}>
|
|
||||||
<Providers>
|
|
||||||
<Header />
|
|
||||||
<UpdateToastClient />
|
|
||||||
<main>{children}</main>
|
|
||||||
<ToastViewport className="ToastViewport" />
|
|
||||||
</Providers>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import Head from 'next/head'
|
||||||
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
|
import { NextIntlClientProvider } from 'next-intl'
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||||
|
|
||||||
import { AboutTabs } from '~utils/enums'
|
import { AboutTabs } from '~utils/enums'
|
||||||
|
|
@ -19,6 +20,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
page?: string
|
page?: string
|
||||||
|
messages: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AboutRoute: React.FC<Props> = (props: Props) => {
|
const AboutRoute: React.FC<Props> = (props: Props) => {
|
||||||
|
|
@ -100,35 +102,37 @@ const AboutRoute: React.FC<Props> = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<div id="About">
|
<div id="About">
|
||||||
{pageHead()}
|
{pageHead()}
|
||||||
<section>
|
<NextIntlClientProvider messages={props.messages}>
|
||||||
<SegmentedControl blended={true}>
|
<section>
|
||||||
<Segment
|
<SegmentedControl blended={true}>
|
||||||
groupName="about"
|
<Segment
|
||||||
name="about"
|
groupName="about"
|
||||||
selected={currentTab == AboutTabs.About}
|
name="about"
|
||||||
onClick={handleTabClicked}
|
selected={currentTab == AboutTabs.About}
|
||||||
>
|
onClick={handleTabClicked}
|
||||||
{t('about.segmented_control.about')}
|
>
|
||||||
</Segment>
|
{t('about.segmented_control.about')}
|
||||||
<Segment
|
</Segment>
|
||||||
groupName="about"
|
<Segment
|
||||||
name="updates"
|
groupName="about"
|
||||||
selected={currentTab == AboutTabs.Updates}
|
name="updates"
|
||||||
onClick={handleTabClicked}
|
selected={currentTab == AboutTabs.Updates}
|
||||||
>
|
onClick={handleTabClicked}
|
||||||
{t('about.segmented_control.updates')}
|
>
|
||||||
</Segment>
|
{t('about.segmented_control.updates')}
|
||||||
<Segment
|
</Segment>
|
||||||
groupName="about"
|
<Segment
|
||||||
name="roadmap"
|
groupName="about"
|
||||||
selected={currentTab == AboutTabs.Roadmap}
|
name="roadmap"
|
||||||
onClick={handleTabClicked}
|
selected={currentTab == AboutTabs.Roadmap}
|
||||||
>
|
onClick={handleTabClicked}
|
||||||
{t('about.segmented_control.roadmap')}
|
>
|
||||||
</Segment>
|
{t('about.segmented_control.roadmap')}
|
||||||
</SegmentedControl>
|
</Segment>
|
||||||
{currentSection()}
|
</SegmentedControl>
|
||||||
</section>
|
{currentSection()}
|
||||||
|
</section>
|
||||||
|
</NextIntlClientProvider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -149,6 +153,12 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
||||||
props: {
|
props: {
|
||||||
page: req.url?.slice(1),
|
page: req.url?.slice(1),
|
||||||
...(await serverSideTranslations(locale, ['common', 'about', 'updates'])),
|
...(await serverSideTranslations(locale, ['common', 'about', 'updates'])),
|
||||||
|
// Provide next-intl messages for child components using next-intl
|
||||||
|
messages: {
|
||||||
|
common: (await import(`../public/locales/${locale}/common.json`)).default,
|
||||||
|
about: (await import(`../public/locales/${locale}/about.json`)).default,
|
||||||
|
updates: (await import(`../public/locales/${locale}/updates.json`)).default,
|
||||||
|
},
|
||||||
// Will be passed to the page component as props
|
// Will be passed to the page component as props
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue