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'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useRouter, useSearchParams } from '~/i18n/navigation'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
|
||||
// Components
|
||||
|
|
@ -60,7 +60,7 @@ const ProfilePageClient: React.FC<Props> = ({
|
|||
initialRaid,
|
||||
initialRecency
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const t = useTranslations('common')
|
||||
const router = useRouter()
|
||||
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'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useRouter } from '~/i18n/navigation'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
// Components
|
||||
|
|
@ -24,7 +24,7 @@ const NewPartyClient: React.FC<Props> = ({
|
|||
raidGroups,
|
||||
error = false
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const t = useTranslations('common')
|
||||
const router = useRouter()
|
||||
|
||||
// State for tab management
|
||||
|
|
@ -76,4 +76,4 @@ const NewPartyClient: React.FC<Props> = ({
|
|||
return <PartyWrapper raidGroups={raidGroups} />
|
||||
}
|
||||
|
||||
export default NewPartyClient
|
||||
export default NewPartyClient
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { Link } from '~/i18n/navigation'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page not found / granblue.team',
|
||||
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 (
|
||||
<div className="error-container">
|
||||
<div className="error-content">
|
||||
|
|
@ -24,4 +26,4 @@ export default function NotFound() {
|
|||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useRouter } from '~/i18n/navigation'
|
||||
|
||||
// Utils
|
||||
import { appState } from '~/utils/appState'
|
||||
|
|
@ -20,7 +20,7 @@ interface Props {
|
|||
|
||||
const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
const t = useTranslations('common')
|
||||
|
||||
// State for tab management
|
||||
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'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useRouter, useSearchParams } from '~/i18n/navigation'
|
||||
|
||||
// Components
|
||||
import FilterBar from '~/components/filters/FilterBar'
|
||||
|
|
@ -43,7 +43,7 @@ const SavedPageClient: React.FC<Props> = ({
|
|||
initialRecency,
|
||||
error = false
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const t = useTranslations('common')
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
|
|
@ -201,4 +201,4 @@ const SavedPageClient: React.FC<Props> = ({
|
|||
)
|
||||
}
|
||||
|
||||
export default SavedPageClient
|
||||
export default SavedPageClient
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useRouter, useSearchParams } from '~/i18n/navigation'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
|
||||
// Hooks
|
||||
|
|
@ -55,7 +55,7 @@ const TeamsPageClient: React.FC<Props> = ({
|
|||
initialRecency,
|
||||
error = false
|
||||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
const t = useTranslations('common')
|
||||
const router = useRouter()
|
||||
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 { deleteCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from '~/i18n/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useLocale } from 'next-intl'
|
||||
import classNames from 'classnames'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import Link from 'next/link'
|
||||
import { Link } from '~/i18n/navigation'
|
||||
|
||||
import { accountState, initialAccountState } from '~/utils/accountState'
|
||||
import { appState, initialAppState } from '~/utils/appState'
|
||||
|
|
@ -37,11 +38,11 @@ import styles from '~/components/Header/index.module.scss'
|
|||
|
||||
const Header = () => {
|
||||
// Localization
|
||||
const { t } = useTranslation('common')
|
||||
const t = useTranslations('common')
|
||||
const locale = useLocale()
|
||||
|
||||
// Router
|
||||
const router = useRouter()
|
||||
const locale = 'en' // TODO: Update when implementing internationalization with App Router
|
||||
|
||||
// State management
|
||||
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'
|
||||
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',
|
||||
})
|
||||
|
||||
// Minimal root layout - all content is handled in [locale]/layout.tsx
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={goalking.variable}>
|
||||
<body className={goalking.className}>
|
||||
<Providers>
|
||||
<Header />
|
||||
<UpdateToastClient />
|
||||
<main>{children}</main>
|
||||
<ToastViewport className="ToastViewport" />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
return children
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import Head from 'next/head'
|
|||
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { NextIntlClientProvider } from 'next-intl'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
|
||||
import { AboutTabs } from '~utils/enums'
|
||||
|
|
@ -19,6 +20,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
|
|||
|
||||
interface Props {
|
||||
page?: string
|
||||
messages: Record<string, any>
|
||||
}
|
||||
|
||||
const AboutRoute: React.FC<Props> = (props: Props) => {
|
||||
|
|
@ -100,35 +102,37 @@ const AboutRoute: React.FC<Props> = (props: Props) => {
|
|||
return (
|
||||
<div id="About">
|
||||
{pageHead()}
|
||||
<section>
|
||||
<SegmentedControl blended={true}>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="about"
|
||||
selected={currentTab == AboutTabs.About}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.about')}
|
||||
</Segment>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="updates"
|
||||
selected={currentTab == AboutTabs.Updates}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.updates')}
|
||||
</Segment>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="roadmap"
|
||||
selected={currentTab == AboutTabs.Roadmap}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.roadmap')}
|
||||
</Segment>
|
||||
</SegmentedControl>
|
||||
{currentSection()}
|
||||
</section>
|
||||
<NextIntlClientProvider messages={props.messages}>
|
||||
<section>
|
||||
<SegmentedControl blended={true}>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="about"
|
||||
selected={currentTab == AboutTabs.About}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.about')}
|
||||
</Segment>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="updates"
|
||||
selected={currentTab == AboutTabs.Updates}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.updates')}
|
||||
</Segment>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="roadmap"
|
||||
selected={currentTab == AboutTabs.Roadmap}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.roadmap')}
|
||||
</Segment>
|
||||
</SegmentedControl>
|
||||
{currentSection()}
|
||||
</section>
|
||||
</NextIntlClientProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -149,6 +153,12 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
props: {
|
||||
page: req.url?.slice(1),
|
||||
...(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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue