Fix Party component interface and remove unstable_cache
- Update NewPartyClient and PartyPageClient to use correct Party props - Remove unstable_cache from all data fetching functions - Fix viewport metadata configuration in App Router - Restore ToastViewport component in layout - Fix import paths from 'types' to '~types' in Party components - Add comprehensive PRD documenting the fixes This addresses the interface mismatch between Party component and its client wrappers that occurred during the App Router migration.
This commit is contained in:
parent
1775f3f5b6
commit
218a524b55
12 changed files with 410 additions and 231 deletions
5
.env.local
Normal file
5
.env.local
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
NEXT_PUBLIC_SIERO_API_URL=http://127.0.0.1:3000/api/v1
|
||||||
|
NEXT_PUBLIC_SIERO_OAUTH_URL=http://127.0.0.1:3000/oauth
|
||||||
|
NEXT_INTL_CONFIG_PATH=i18n/request.ts
|
||||||
|
DEBUG_API_URL=1
|
||||||
|
DEBUG_API_BODY=1
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Metadata } from 'next'
|
import { Metadata, Viewport } from 'next'
|
||||||
import localFont from 'next/font/local'
|
import localFont from 'next/font/local'
|
||||||
import { Viewport } from '@radix-ui/react-toast'
|
import { Viewport as ToastViewport } from '@radix-ui/react-toast'
|
||||||
|
|
||||||
import '../styles/globals.scss'
|
import '../styles/globals.scss'
|
||||||
|
|
||||||
|
|
@ -13,7 +13,13 @@ import UpdateToastClient from './components/UpdateToastClient'
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'granblue.team',
|
title: 'granblue.team',
|
||||||
description: 'Create, save, and share Granblue Fantasy party compositions',
|
description: 'Create, save, and share Granblue Fantasy party compositions',
|
||||||
viewport: 'viewport-fit=cover, width=device-width, initial-scale=1.0',
|
}
|
||||||
|
|
||||||
|
// Viewport configuration (Next.js 13+ requires separate export)
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: 'device-width',
|
||||||
|
initialScale: 1,
|
||||||
|
viewportFit: 'cover',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Font
|
// Font
|
||||||
|
|
@ -35,7 +41,7 @@ export default function RootLayout({
|
||||||
<Header />
|
<Header />
|
||||||
<UpdateToastClient />
|
<UpdateToastClient />
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
<Viewport className="ToastViewport" />
|
<ToastViewport className="ToastViewport" />
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { unstable_cache } from 'next/cache';
|
|
||||||
import { fetchFromApi } from './api-utils';
|
import { fetchFromApi } from './api-utils';
|
||||||
|
|
||||||
// Cached server-side data fetching functions
|
// Server-side data fetching functions
|
||||||
// These are wrapped with React's cache function to deduplicate requests
|
// Next.js automatically deduplicates requests within the same render
|
||||||
|
|
||||||
// Get teams with optional filters
|
// Get teams with optional filters
|
||||||
export async function getTeams({
|
export async function getTeams({
|
||||||
|
|
@ -18,16 +17,6 @@ export async function getTeams({
|
||||||
page?: number;
|
page?: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
}) {
|
}) {
|
||||||
const key = [
|
|
||||||
'getTeams',
|
|
||||||
String(element ?? ''),
|
|
||||||
String(raid ?? ''),
|
|
||||||
String(recency ?? ''),
|
|
||||||
String(page ?? 1),
|
|
||||||
String(username ?? ''),
|
|
||||||
];
|
|
||||||
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
const queryParams: Record<string, string> = {};
|
const queryParams: Record<string, string> = {};
|
||||||
if (element) queryParams.element = element.toString();
|
if (element) queryParams.element = element.toString();
|
||||||
if (raid) queryParams.raid_id = raid;
|
if (raid) queryParams.raid_id = raid;
|
||||||
|
|
@ -49,15 +38,10 @@ export async function getTeams({
|
||||||
console.error('Failed to fetch teams', error);
|
console.error('Failed to fetch teams', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 60 });
|
|
||||||
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a single team by shortcode
|
// Get a single team by shortcode
|
||||||
export async function getTeam(shortcode: string) {
|
export async function getTeam(shortcode: string) {
|
||||||
const key = ['getTeam', String(shortcode)];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchFromApi(`/parties/${shortcode}`);
|
const data = await fetchFromApi(`/parties/${shortcode}`);
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -65,14 +49,10 @@ export async function getTeam(shortcode: string) {
|
||||||
console.error(`Failed to fetch team with shortcode ${shortcode}`, error);
|
console.error(`Failed to fetch team with shortcode ${shortcode}`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 60 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user info
|
// Get user info
|
||||||
export async function getUserInfo(username: string) {
|
export async function getUserInfo(username: string) {
|
||||||
const key = ['getUserInfo', String(username)];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchFromApi(`/users/info/${username}`);
|
const data = await fetchFromApi(`/users/info/${username}`);
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -80,14 +60,10 @@ export async function getUserInfo(username: string) {
|
||||||
console.error(`Failed to fetch user info for ${username}`, error);
|
console.error(`Failed to fetch user info for ${username}`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 60 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get raid groups
|
// Get raid groups
|
||||||
export async function getRaidGroups() {
|
export async function getRaidGroups() {
|
||||||
const key = ['getRaidGroups'];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchFromApi('/raids/groups');
|
const data = await fetchFromApi('/raids/groups');
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -95,14 +71,10 @@ export async function getRaidGroups() {
|
||||||
console.error('Failed to fetch raid groups', error);
|
console.error('Failed to fetch raid groups', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 300 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get version info
|
// Get version info
|
||||||
export async function getVersion() {
|
export async function getVersion() {
|
||||||
const key = ['getVersion'];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchFromApi('/version');
|
const data = await fetchFromApi('/version');
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -110,14 +82,10 @@ export async function getVersion() {
|
||||||
console.error('Failed to fetch version info', error);
|
console.error('Failed to fetch version info', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 300 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's favorites/saved teams
|
// Get user's favorites/saved teams
|
||||||
export async function getFavorites() {
|
export async function getFavorites() {
|
||||||
const key = ['getFavorites'];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchFromApi('/parties/favorites');
|
const data = await fetchFromApi('/parties/favorites');
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -125,14 +93,10 @@ export async function getFavorites() {
|
||||||
console.error('Failed to fetch favorites', error);
|
console.error('Failed to fetch favorites', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 60 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all jobs
|
// Get all jobs
|
||||||
export async function getJobs(element?: number) {
|
export async function getJobs(element?: number) {
|
||||||
const key = ['getJobs', String(element ?? '')];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const queryParams: Record<string, string> = {};
|
const queryParams: Record<string, string> = {};
|
||||||
if (element) queryParams.element = element.toString();
|
if (element) queryParams.element = element.toString();
|
||||||
|
|
@ -147,14 +111,10 @@ export async function getJobs(element?: number) {
|
||||||
console.error('Failed to fetch jobs', error);
|
console.error('Failed to fetch jobs', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 300 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get job by ID
|
// Get job by ID
|
||||||
export async function getJob(jobId: string) {
|
export async function getJob(jobId: string) {
|
||||||
const key = ['getJob', String(jobId)];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchFromApi(`/jobs/${jobId}`);
|
const data = await fetchFromApi(`/jobs/${jobId}`);
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -162,14 +122,10 @@ export async function getJob(jobId: string) {
|
||||||
console.error(`Failed to fetch job with ID ${jobId}`, error);
|
console.error(`Failed to fetch job with ID ${jobId}`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 300 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get job skills
|
// Get job skills
|
||||||
export async function getJobSkills(jobId?: string) {
|
export async function getJobSkills(jobId?: string) {
|
||||||
const key = ['getJobSkills', String(jobId ?? '')];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const endpoint = jobId ? `/jobs/${jobId}/skills` : '/jobs/skills';
|
const endpoint = jobId ? `/jobs/${jobId}/skills` : '/jobs/skills';
|
||||||
const data = await fetchFromApi(endpoint);
|
const data = await fetchFromApi(endpoint);
|
||||||
|
|
@ -178,14 +134,10 @@ export async function getJobSkills(jobId?: string) {
|
||||||
console.error('Failed to fetch job skills', error);
|
console.error('Failed to fetch job skills', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 300 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get job accessories
|
// Get job accessories
|
||||||
export async function getJobAccessories(jobId: string) {
|
export async function getJobAccessories(jobId: string) {
|
||||||
const key = ['getJobAccessories', String(jobId)];
|
|
||||||
const run = unstable_cache(async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await fetchFromApi(`/jobs/${jobId}/accessories`);
|
const data = await fetchFromApi(`/jobs/${jobId}/accessories`);
|
||||||
return data;
|
return data;
|
||||||
|
|
@ -193,6 +145,4 @@ export async function getJobAccessories(jobId: string) {
|
||||||
console.error(`Failed to fetch accessories for job ${jobId}`, error);
|
console.error(`Failed to fetch accessories for job ${jobId}`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, key, { revalidate: 300 });
|
|
||||||
return run();
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import NewHead from '~/components/head/NewHead'
|
|
||||||
import Party from '~/components/party/Party'
|
import Party from '~/components/party/Party'
|
||||||
import ErrorSection from '~/components/ErrorSection'
|
import ErrorSection from '~/components/ErrorSection'
|
||||||
|
|
||||||
|
|
@ -13,6 +12,7 @@ import ErrorSection from '~/components/ErrorSection'
|
||||||
import { appState, initialAppState } from '~/utils/appState'
|
import { appState, initialAppState } from '~/utils/appState'
|
||||||
import { accountState } from '~/utils/accountState'
|
import { accountState } from '~/utils/accountState'
|
||||||
import clonedeep from 'lodash.clonedeep'
|
import clonedeep from 'lodash.clonedeep'
|
||||||
|
import { GridType } from '~/utils/enums'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
raidGroups: any[]; // Replace with proper RaidGroup type
|
raidGroups: any[]; // Replace with proper RaidGroup type
|
||||||
|
|
@ -26,6 +26,9 @@ const NewPartyClient: React.FC<Props> = ({
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// State for tab management
|
||||||
|
const [selectedTab, setSelectedTab] = useState<GridType>(GridType.Weapon)
|
||||||
|
|
||||||
// Initialize app state for a new party
|
// Initialize app state for a new party
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset app state for new party
|
// Reset app state for new party
|
||||||
|
|
@ -40,40 +43,17 @@ const NewPartyClient: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
}, [raidGroups])
|
}, [raidGroups])
|
||||||
|
|
||||||
// Handle save action
|
// Handle tab change
|
||||||
async function handleSave(shouldNavigate = true) {
|
const handleTabChanged = (value: string) => {
|
||||||
try {
|
const tabType = parseInt(value) as GridType
|
||||||
// Prepare party data
|
setSelectedTab(tabType)
|
||||||
const party = {
|
|
||||||
name: appState.parties[0]?.name || '',
|
|
||||||
description: appState.parties[0]?.description || '',
|
|
||||||
visibility: appState.parties[0]?.visibility || 'public',
|
|
||||||
element: appState.parties[0]?.element || 1, // Default to Wind
|
|
||||||
raid_id: appState.parties[0]?.raid?.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the party
|
// Navigation helper for Party component
|
||||||
const response = await fetch('/api/parties', {
|
const pushHistory = (path: string) => {
|
||||||
method: 'POST',
|
router.push(path)
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ party })
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (data && data.shortcode && shouldNavigate) {
|
|
||||||
// Navigate to the new party page
|
|
||||||
router.push(`/p/${data.shortcode}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving party', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -87,14 +67,13 @@ const NewPartyClient: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<NewHead />
|
|
||||||
<Party
|
<Party
|
||||||
party={appState.parties[0] || { name: t('new_party'), element: 1 }}
|
new={true}
|
||||||
isNew={true}
|
selectedTab={selectedTab}
|
||||||
onSave={handleSave}
|
raidGroups={raidGroups}
|
||||||
|
handleTabChanged={handleTabChanged}
|
||||||
|
pushHistory={pushHistory}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'next-i18next'
|
import { useTranslation } from 'next-i18next'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { appState } from '~/utils/appState'
|
import { appState } from '~/utils/appState'
|
||||||
|
import { GridType } from '~/utils/enums'
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Party from '~/components/party/Party'
|
import Party from '~/components/party/Party'
|
||||||
|
|
@ -21,6 +22,9 @@ const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
|
// State for tab management
|
||||||
|
const [selectedTab, setSelectedTab] = useState<GridType>(GridType.Weapon)
|
||||||
|
|
||||||
// Initialize app state
|
// Initialize app state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (party) {
|
if (party) {
|
||||||
|
|
@ -29,45 +33,18 @@ const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
||||||
}
|
}
|
||||||
}, [party, raidGroups])
|
}, [party, raidGroups])
|
||||||
|
|
||||||
// Handle remix action
|
// Handle tab change
|
||||||
async function handleRemix() {
|
const handleTabChanged = (value: string) => {
|
||||||
if (!party || !party.shortcode) return
|
const tabType = parseInt(value) as GridType
|
||||||
|
setSelectedTab(tabType)
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/parties/${party.shortcode}/remix`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (data && data.shortcode) {
|
|
||||||
// Navigate to the new remixed party
|
|
||||||
router.push(`/p/${data.shortcode}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error remixing party', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle deletion action
|
// Navigation helper (not used for existing parties but required by interface)
|
||||||
async function handleDelete() {
|
const pushHistory = (path: string) => {
|
||||||
if (!party || !party.shortcode) return
|
router.push(path)
|
||||||
|
|
||||||
try {
|
|
||||||
await fetch(`/api/parties/${party.shortcode}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Navigate to teams page after deletion
|
|
||||||
router.push('/teams')
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting party', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Error case
|
// Error case
|
||||||
if (!party) {
|
if (!party) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -83,9 +60,11 @@ const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Party
|
<Party
|
||||||
party={party}
|
team={party}
|
||||||
onRemix={handleRemix}
|
selectedTab={selectedTab}
|
||||||
onDelete={handleDelete}
|
raidGroups={raidGroups}
|
||||||
|
handleTabChanged={handleTabChanged}
|
||||||
|
pushHistory={pushHistory}
|
||||||
/>
|
/>
|
||||||
<PartyFooter party={party} />
|
<PartyFooter party={party} />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import SwitchTableField from '~components/common/SwitchTableField'
|
||||||
import TableField from '~components/common/TableField'
|
import TableField from '~components/common/TableField'
|
||||||
|
|
||||||
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
|
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
|
||||||
import type { DetailsObject } from 'types'
|
import type { DetailsObject } from '~types'
|
||||||
import type { DialogProps } from '@radix-ui/react-dialog'
|
import type { DialogProps } from '@radix-ui/react-dialog'
|
||||||
import type { JSONContent } from '@tiptap/core'
|
import type { JSONContent } from '@tiptap/core'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import api from '~utils/api'
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
import { youtube } from '~utils/youtube'
|
import { youtube } from '~utils/youtube'
|
||||||
|
|
||||||
import type { DetailsObject } from 'types'
|
import type { DetailsObject } from '~types'
|
||||||
|
|
||||||
import RemixIcon from '~public/icons/Remix.svg'
|
import RemixIcon from '~public/icons/Remix.svg'
|
||||||
import EditIcon from '~public/icons/Edit.svg'
|
import EditIcon from '~public/icons/Edit.svg'
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import SaveIcon from '~public/icons/Save.svg'
|
||||||
import PrivateIcon from '~public/icons/Private.svg'
|
import PrivateIcon from '~public/icons/Private.svg'
|
||||||
import UnlistedIcon from '~public/icons/Unlisted.svg'
|
import UnlistedIcon from '~public/icons/Unlisted.svg'
|
||||||
|
|
||||||
import type { DetailsObject } from 'types'
|
import type { DetailsObject } from '~types'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import DialogHeader from '~components/common/DialogHeader'
|
||||||
import DialogFooter from '~components/common/DialogFooter'
|
import DialogFooter from '~components/common/DialogFooter'
|
||||||
import DialogContent from '~components/common/DialogContent'
|
import DialogContent from '~components/common/DialogContent'
|
||||||
|
|
||||||
import type { DetailsObject } from 'types'
|
import type { DetailsObject } from '~types'
|
||||||
import type { DialogProps } from '@radix-ui/react-dialog'
|
import type { DialogProps } from '@radix-ui/react-dialog'
|
||||||
|
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
|
|
|
||||||
17
i18n/request.ts
Normal file
17
i18n/request.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import {getRequestConfig} from 'next-intl/server'
|
||||||
|
import {locales, defaultLocale, type Locale} from '../i18n.config'
|
||||||
|
|
||||||
|
// next-intl v4: global request config used by getMessages()
|
||||||
|
export default getRequestConfig(async ({requestLocale}) => {
|
||||||
|
let locale = (await requestLocale) as Locale | null;
|
||||||
|
if (!locale || !locales.includes(locale)) {
|
||||||
|
locale = defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load only i18n namespaces; exclude content data with dotted keys
|
||||||
|
const common = (await import(`../public/locales/${locale}/common.json`)).default;
|
||||||
|
const about = (await import(`../public/locales/${locale}/about.json`)).default;
|
||||||
|
const messages = {common, about} as const;
|
||||||
|
|
||||||
|
return {locale, messages};
|
||||||
|
});
|
||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
|
|
@ -3,4 +3,4 @@
|
||||||
/// <reference types="next/navigation-types/compat/navigation" />
|
/// <reference types="next/navigation-types/compat/navigation" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
|
|
|
||||||
243
prd/fix-party-component-interface.md
Normal file
243
prd/fix-party-component-interface.md
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
# Product Requirements Document: Fix Party Component Interface Mismatch
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
During the App Router migration, the client components (`NewPartyClient` and `PartyPageClient`) were created with an incorrect interface for the `Party` component. This causes an "Element type is invalid" error preventing pages from loading.
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
### What Went Wrong
|
||||||
|
1. **Interface Mismatch**: The Party component expects props like `selectedTab`, `raidGroups`, and `handleTabChanged`, but client components are passing `party`, `isNew`, and `onSave`
|
||||||
|
2. **Incomplete Migration**: During the Pages-to-App Router migration, new client wrapper components were created without matching the existing Party component's interface
|
||||||
|
3. **Missing Context**: The migration didn't account for the Party component being a complete, self-contained system that handles its own state management
|
||||||
|
|
||||||
|
### Current Party Component Architecture
|
||||||
|
The Party component is a comprehensive system that:
|
||||||
|
- Manages its own save/create/update logic via internal functions (`createParty()` and `updateParty()`)
|
||||||
|
- Uses global `appState` from Valtio for state management
|
||||||
|
- Handles tab switching between Character/Weapon/Summon grids
|
||||||
|
- Manages edit permissions and authentication
|
||||||
|
- Contains PartyHeader, PartySegmentedControl, Grid components, and PartyFooter
|
||||||
|
|
||||||
|
### Expected Interface
|
||||||
|
```typescript
|
||||||
|
interface Props {
|
||||||
|
new?: boolean
|
||||||
|
team?: Party
|
||||||
|
selectedTab: GridType
|
||||||
|
raidGroups: RaidGroup[]
|
||||||
|
handleTabChanged: (value: string) => void
|
||||||
|
pushHistory?: (path: string) => void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current (Incorrect) Usage
|
||||||
|
```typescript
|
||||||
|
// In NewPartyClient.tsx
|
||||||
|
<Party
|
||||||
|
party={appState.parties[0] || { name: t('new_party'), element: 1 }}
|
||||||
|
isNew={true}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// In PartyPageClient.tsx
|
||||||
|
<Party
|
||||||
|
party={party}
|
||||||
|
onRemix={handleRemix}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solution Options
|
||||||
|
|
||||||
|
### Option 1: Fix Client Components (Recommended)
|
||||||
|
Update the client wrapper components to match the Party component's expected interface.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Maintains existing Party component architecture
|
||||||
|
- Minimal changes required
|
||||||
|
- Preserves all existing functionality
|
||||||
|
- Lower risk of breaking existing features
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Client components need to manage additional state
|
||||||
|
|
||||||
|
### Option 2: Create Adapter Components
|
||||||
|
Create intermediate adapter components that translate between the two interfaces.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Keeps existing client component logic
|
||||||
|
- Provides flexibility for future changes
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Adds complexity
|
||||||
|
- Extra layer of abstraction
|
||||||
|
- Performance overhead
|
||||||
|
|
||||||
|
### Option 3: Refactor Party Component
|
||||||
|
Change the Party component to accept the simpler interface the client components expect.
|
||||||
|
|
||||||
|
**Pros:**
|
||||||
|
- Simpler client components
|
||||||
|
- More explicit prop passing
|
||||||
|
|
||||||
|
**Cons:**
|
||||||
|
- Major refactoring required
|
||||||
|
- Risk of breaking existing functionality
|
||||||
|
- Would need to extract and relocate save/update logic
|
||||||
|
- Affects multiple components
|
||||||
|
|
||||||
|
## Implementation Plan (Option 1 - Recommended)
|
||||||
|
|
||||||
|
### Phase 1: Fix Critical Props Interface
|
||||||
|
|
||||||
|
#### Task List
|
||||||
|
- [x] Remove unstable_cache from data.ts (completed)
|
||||||
|
- [x] Fix viewport metadata in layout.tsx (completed)
|
||||||
|
- [x] Fix Radix Toast import (completed)
|
||||||
|
- [ ] Fix NewPartyClient props interface
|
||||||
|
- [ ] Fix PartyPageClient props interface
|
||||||
|
- [ ] Add selectedTab state management to both components
|
||||||
|
- [ ] Implement handleTabChanged functions
|
||||||
|
- [ ] Add pushHistory navigation wrapper
|
||||||
|
- [ ] Ensure raidGroups are passed correctly
|
||||||
|
- [ ] Remove next/head usage from Head components
|
||||||
|
|
||||||
|
#### Files to Modify
|
||||||
|
|
||||||
|
##### Critical Files (Must Change)
|
||||||
|
```typescript
|
||||||
|
/app/new/NewPartyClient.tsx
|
||||||
|
- Add: useState for selectedTab (default: GridType.Weapon)
|
||||||
|
- Add: handleTabChanged function
|
||||||
|
- Change: party → team prop
|
||||||
|
- Change: isNew → new prop
|
||||||
|
- Remove: onSave prop (Party handles internally)
|
||||||
|
- Add: raidGroups prop from parent
|
||||||
|
- Add: pushHistory function using router.push
|
||||||
|
|
||||||
|
/app/p/[party]/PartyPageClient.tsx
|
||||||
|
- Add: useState for selectedTab (default: GridType.Weapon)
|
||||||
|
- Add: handleTabChanged function
|
||||||
|
- Change: party → team prop
|
||||||
|
- Remove: onRemix and onDelete props (Party handles internally)
|
||||||
|
- Add: raidGroups prop from parent
|
||||||
|
- Add: pushHistory function using router.push
|
||||||
|
|
||||||
|
/app/new/page.tsx
|
||||||
|
- Verify: raidGroups are fetched correctly
|
||||||
|
- Ensure: raidGroups are passed to NewPartyClient
|
||||||
|
|
||||||
|
/app/p/[party]/page.tsx
|
||||||
|
- Add: Fetch raidGroups data
|
||||||
|
- Pass: raidGroups to PartyPageClient
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Head Component Cleanup
|
||||||
|
```typescript
|
||||||
|
/components/head/NewHead/index.tsx
|
||||||
|
- Remove entirely (metadata handled in page.tsx)
|
||||||
|
|
||||||
|
/components/head/ProfileHead/index.tsx
|
||||||
|
- Remove entirely (metadata handled in page.tsx)
|
||||||
|
|
||||||
|
/components/head/SavedHead/index.tsx
|
||||||
|
- Remove entirely (metadata handled in page.tsx)
|
||||||
|
|
||||||
|
/components/head/TeamsHead/index.tsx
|
||||||
|
- Remove entirely (metadata handled in page.tsx)
|
||||||
|
|
||||||
|
/components/party/PartyHead/index.tsx
|
||||||
|
- Refactor to not use next/head
|
||||||
|
- Or remove if not needed in App Router
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Restore Toast Viewport
|
||||||
|
```typescript
|
||||||
|
/app/layout.tsx
|
||||||
|
- Uncomment and fix ToastViewport component
|
||||||
|
- Ensure proper import from @radix-ui/react-toast
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
### Functional Tests
|
||||||
|
1. **Page Loading**
|
||||||
|
- [ ] /new page loads without errors
|
||||||
|
- [ ] /p/[party] pages load without errors
|
||||||
|
- [ ] No "Element type is invalid" errors in console
|
||||||
|
|
||||||
|
2. **Party Creation**
|
||||||
|
- [ ] Can create a new party
|
||||||
|
- [ ] Party name can be edited
|
||||||
|
- [ ] Party description can be added
|
||||||
|
- [ ] Save functionality works
|
||||||
|
|
||||||
|
3. **Party Editing**
|
||||||
|
- [ ] Can edit existing party
|
||||||
|
- [ ] Changes persist after save
|
||||||
|
- [ ] Edit permissions work correctly
|
||||||
|
|
||||||
|
4. **Tab Navigation**
|
||||||
|
- [ ] Can switch between Characters tab
|
||||||
|
- [ ] Can switch to Weapons tab
|
||||||
|
- [ ] Can switch to Summons tab
|
||||||
|
- [ ] Tab state persists during editing
|
||||||
|
|
||||||
|
5. **State Management**
|
||||||
|
- [ ] appState updates correctly
|
||||||
|
- [ ] Valtio state management works
|
||||||
|
- [ ] No state inconsistencies
|
||||||
|
|
||||||
|
### Browser Testing
|
||||||
|
- [ ] Chrome/Edge
|
||||||
|
- [ ] Firefox
|
||||||
|
- [ ] Safari
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- No runtime errors on party pages
|
||||||
|
- All party creation/editing functionality works
|
||||||
|
- Tab navigation functions correctly
|
||||||
|
- Pages load successfully in development and production
|
||||||
|
- Metadata is properly rendered for SEO
|
||||||
|
- No console warnings about invalid props
|
||||||
|
|
||||||
|
## Risk Mitigation
|
||||||
|
1. **Testing Strategy**
|
||||||
|
- Test each change incrementally
|
||||||
|
- Verify functionality after each component update
|
||||||
|
- Use development server for immediate feedback
|
||||||
|
|
||||||
|
2. **Rollback Plan**
|
||||||
|
- Create focused commits for each component
|
||||||
|
- Keep changes minimal and isolated
|
||||||
|
- Document any discovered issues
|
||||||
|
|
||||||
|
3. **Communication**
|
||||||
|
- Document any unexpected behavior
|
||||||
|
- Note any additional required changes
|
||||||
|
- Update PRD if scope changes
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
- **Phase 1**: Immediate (blocking issue)
|
||||||
|
- Estimated: 1-2 hours
|
||||||
|
- Priority: Critical
|
||||||
|
|
||||||
|
- **Phase 2**: Follow-up
|
||||||
|
- Estimated: 30 minutes
|
||||||
|
- Priority: Medium
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- GridType enum from utils/enums.tsx
|
||||||
|
- RaidGroup type definitions
|
||||||
|
- Valtio state management
|
||||||
|
- Next.js App Router navigation
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
1. Should PartyHead component be refactored or removed?
|
||||||
|
2. Are there other pages using the Party component that need updates?
|
||||||
|
3. Should we add TypeScript interfaces for better type safety?
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The Party component is well-designed as a self-contained system
|
||||||
|
- The issue is purely an interface mismatch, not a design flaw
|
||||||
|
- This pattern (self-contained components) might be worth documenting for future migrations
|
||||||
Loading…
Reference in a new issue