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 { Viewport } from '@radix-ui/react-toast'
|
||||
import { Viewport as ToastViewport } from '@radix-ui/react-toast'
|
||||
|
||||
import '../styles/globals.scss'
|
||||
|
||||
|
|
@ -13,7 +13,13 @@ import UpdateToastClient from './components/UpdateToastClient'
|
|||
export const metadata: Metadata = {
|
||||
title: 'granblue.team',
|
||||
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
|
||||
|
|
@ -35,7 +41,7 @@ export default function RootLayout({
|
|||
<Header />
|
||||
<UpdateToastClient />
|
||||
<main>{children}</main>
|
||||
<Viewport className="ToastViewport" />
|
||||
<ToastViewport className="ToastViewport" />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
230
app/lib/data.ts
230
app/lib/data.ts
|
|
@ -1,8 +1,7 @@
|
|||
import { unstable_cache } from 'next/cache';
|
||||
import { fetchFromApi } from './api-utils';
|
||||
|
||||
// Cached server-side data fetching functions
|
||||
// These are wrapped with React's cache function to deduplicate requests
|
||||
// Server-side data fetching functions
|
||||
// Next.js automatically deduplicates requests within the same render
|
||||
|
||||
// Get teams with optional filters
|
||||
export async function getTeams({
|
||||
|
|
@ -18,181 +17,132 @@ export async function getTeams({
|
|||
page?: number;
|
||||
username?: string;
|
||||
}) {
|
||||
const key = [
|
||||
'getTeams',
|
||||
String(element ?? ''),
|
||||
String(raid ?? ''),
|
||||
String(recency ?? ''),
|
||||
String(page ?? 1),
|
||||
String(username ?? ''),
|
||||
];
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (element) queryParams.element = element.toString();
|
||||
if (raid) queryParams.raid_id = raid;
|
||||
if (recency) queryParams.recency = recency;
|
||||
if (page) queryParams.page = page.toString();
|
||||
|
||||
const run = unstable_cache(async () => {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (element) queryParams.element = element.toString();
|
||||
if (raid) queryParams.raid_id = raid;
|
||||
if (recency) queryParams.recency = recency;
|
||||
if (page) queryParams.page = page.toString();
|
||||
let endpoint = '/parties';
|
||||
if (username) {
|
||||
endpoint = `/users/${username}/parties`;
|
||||
}
|
||||
|
||||
let endpoint = '/parties';
|
||||
if (username) {
|
||||
endpoint = `/users/${username}/parties`;
|
||||
}
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
if (queryString) endpoint += `?${queryString}`;
|
||||
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
if (queryString) endpoint += `?${queryString}`;
|
||||
|
||||
try {
|
||||
const data = await fetchFromApi(endpoint);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch teams', error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 60 });
|
||||
|
||||
return run();
|
||||
try {
|
||||
const data = await fetchFromApi(endpoint);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch teams', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get a single team by shortcode
|
||||
export async function getTeam(shortcode: string) {
|
||||
const key = ['getTeam', String(shortcode)];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const data = await fetchFromApi(`/parties/${shortcode}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch team with shortcode ${shortcode}`, error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 60 });
|
||||
return run();
|
||||
try {
|
||||
const data = await fetchFromApi(`/parties/${shortcode}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch team with shortcode ${shortcode}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get user info
|
||||
export async function getUserInfo(username: string) {
|
||||
const key = ['getUserInfo', String(username)];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const data = await fetchFromApi(`/users/info/${username}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch user info for ${username}`, error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 60 });
|
||||
return run();
|
||||
try {
|
||||
const data = await fetchFromApi(`/users/info/${username}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch user info for ${username}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get raid groups
|
||||
export async function getRaidGroups() {
|
||||
const key = ['getRaidGroups'];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const data = await fetchFromApi('/raids/groups');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch raid groups', error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 300 });
|
||||
return run();
|
||||
try {
|
||||
const data = await fetchFromApi('/raids/groups');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch raid groups', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get version info
|
||||
export async function getVersion() {
|
||||
const key = ['getVersion'];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const data = await fetchFromApi('/version');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch version info', error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 300 });
|
||||
return run();
|
||||
try {
|
||||
const data = await fetchFromApi('/version');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch version info', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get user's favorites/saved teams
|
||||
export async function getFavorites() {
|
||||
const key = ['getFavorites'];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const data = await fetchFromApi('/parties/favorites');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch favorites', error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 60 });
|
||||
return run();
|
||||
try {
|
||||
const data = await fetchFromApi('/parties/favorites');
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch favorites', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all jobs
|
||||
export async function getJobs(element?: number) {
|
||||
const key = ['getJobs', String(element ?? '')];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (element) queryParams.element = element.toString();
|
||||
try {
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (element) queryParams.element = element.toString();
|
||||
|
||||
let endpoint = '/jobs';
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
if (queryString) endpoint += `?${queryString}`;
|
||||
let endpoint = '/jobs';
|
||||
const queryString = new URLSearchParams(queryParams).toString();
|
||||
if (queryString) endpoint += `?${queryString}`;
|
||||
|
||||
const data = await fetchFromApi(endpoint);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch jobs', error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 300 });
|
||||
return run();
|
||||
const data = await fetchFromApi(endpoint);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch jobs', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get job by ID
|
||||
export async function getJob(jobId: string) {
|
||||
const key = ['getJob', String(jobId)];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const data = await fetchFromApi(`/jobs/${jobId}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch job with ID ${jobId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 300 });
|
||||
return run();
|
||||
try {
|
||||
const data = await fetchFromApi(`/jobs/${jobId}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch job with ID ${jobId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get job skills
|
||||
export async function getJobSkills(jobId?: string) {
|
||||
const key = ['getJobSkills', String(jobId ?? '')];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const endpoint = jobId ? `/jobs/${jobId}/skills` : '/jobs/skills';
|
||||
const data = await fetchFromApi(endpoint);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch job skills', error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 300 });
|
||||
return run();
|
||||
try {
|
||||
const endpoint = jobId ? `/jobs/${jobId}/skills` : '/jobs/skills';
|
||||
const data = await fetchFromApi(endpoint);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch job skills', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get job accessories
|
||||
export async function getJobAccessories(jobId: string) {
|
||||
const key = ['getJobAccessories', String(jobId)];
|
||||
const run = unstable_cache(async () => {
|
||||
try {
|
||||
const data = await fetchFromApi(`/jobs/${jobId}/accessories`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch accessories for job ${jobId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}, key, { revalidate: 300 });
|
||||
return run();
|
||||
}
|
||||
try {
|
||||
const data = await fetchFromApi(`/jobs/${jobId}/accessories`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch accessories for job ${jobId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
// Components
|
||||
import NewHead from '~/components/head/NewHead'
|
||||
import Party from '~/components/party/Party'
|
||||
import ErrorSection from '~/components/ErrorSection'
|
||||
|
||||
|
|
@ -13,6 +12,7 @@ import ErrorSection from '~/components/ErrorSection'
|
|||
import { appState, initialAppState } from '~/utils/appState'
|
||||
import { accountState } from '~/utils/accountState'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import { GridType } from '~/utils/enums'
|
||||
|
||||
interface Props {
|
||||
raidGroups: any[]; // Replace with proper RaidGroup type
|
||||
|
|
@ -26,6 +26,9 @@ const NewPartyClient: React.FC<Props> = ({
|
|||
const { t } = useTranslation('common')
|
||||
const router = useRouter()
|
||||
|
||||
// State for tab management
|
||||
const [selectedTab, setSelectedTab] = useState<GridType>(GridType.Weapon)
|
||||
|
||||
// Initialize app state for a new party
|
||||
useEffect(() => {
|
||||
// Reset app state for new party
|
||||
|
|
@ -40,41 +43,18 @@ const NewPartyClient: React.FC<Props> = ({
|
|||
}
|
||||
}, [raidGroups])
|
||||
|
||||
// Handle save action
|
||||
async function handleSave(shouldNavigate = true) {
|
||||
try {
|
||||
// Prepare party data
|
||||
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
|
||||
const response = await fetch('/api/parties', {
|
||||
method: 'POST',
|
||||
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
|
||||
}
|
||||
// Handle tab change
|
||||
const handleTabChanged = (value: string) => {
|
||||
const tabType = parseInt(value) as GridType
|
||||
setSelectedTab(tabType)
|
||||
}
|
||||
|
||||
// Navigation helper for Party component
|
||||
const pushHistory = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorSection
|
||||
|
|
@ -87,14 +67,13 @@ const NewPartyClient: React.FC<Props> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewHead />
|
||||
<Party
|
||||
party={appState.parties[0] || { name: t('new_party'), element: 1 }}
|
||||
isNew={true}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</>
|
||||
<Party
|
||||
new={true}
|
||||
selectedTab={selectedTab}
|
||||
raidGroups={raidGroups}
|
||||
handleTabChanged={handleTabChanged}
|
||||
pushHistory={pushHistory}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
'use client'
|
||||
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
// Utils
|
||||
import { appState } from '~/utils/appState'
|
||||
import { GridType } from '~/utils/enums'
|
||||
|
||||
// Components
|
||||
import Party from '~/components/party/Party'
|
||||
|
|
@ -21,6 +22,9 @@ const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
|||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// State for tab management
|
||||
const [selectedTab, setSelectedTab] = useState<GridType>(GridType.Weapon)
|
||||
|
||||
// Initialize app state
|
||||
useEffect(() => {
|
||||
if (party) {
|
||||
|
|
@ -29,45 +33,18 @@ const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
|||
}
|
||||
}, [party, raidGroups])
|
||||
|
||||
// Handle remix action
|
||||
async function handleRemix() {
|
||||
if (!party || !party.shortcode) return
|
||||
|
||||
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 tab change
|
||||
const handleTabChanged = (value: string) => {
|
||||
const tabType = parseInt(value) as GridType
|
||||
setSelectedTab(tabType)
|
||||
}
|
||||
|
||||
// Handle deletion action
|
||||
async function handleDelete() {
|
||||
if (!party || !party.shortcode) return
|
||||
|
||||
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)
|
||||
}
|
||||
// Navigation helper (not used for existing parties but required by interface)
|
||||
const pushHistory = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
|
||||
// Error case
|
||||
if (!party) {
|
||||
return (
|
||||
|
|
@ -83,9 +60,11 @@ const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
|
|||
return (
|
||||
<>
|
||||
<Party
|
||||
party={party}
|
||||
onRemix={handleRemix}
|
||||
onDelete={handleDelete}
|
||||
team={party}
|
||||
selectedTab={selectedTab}
|
||||
raidGroups={raidGroups}
|
||||
handleTabChanged={handleTabChanged}
|
||||
pushHistory={pushHistory}
|
||||
/>
|
||||
<PartyFooter party={party} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import SwitchTableField from '~components/common/SwitchTableField'
|
|||
import TableField from '~components/common/TableField'
|
||||
|
||||
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 { JSONContent } from '@tiptap/core'
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import api from '~utils/api'
|
|||
import { appState } from '~utils/appState'
|
||||
import { youtube } from '~utils/youtube'
|
||||
|
||||
import type { DetailsObject } from 'types'
|
||||
import type { DetailsObject } from '~types'
|
||||
|
||||
import RemixIcon from '~public/icons/Remix.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 UnlistedIcon from '~public/icons/Unlisted.svg'
|
||||
|
||||
import type { DetailsObject } from 'types'
|
||||
import type { DetailsObject } from '~types'
|
||||
|
||||
import styles from './index.module.scss'
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import DialogHeader from '~components/common/DialogHeader'
|
|||
import DialogFooter from '~components/common/DialogFooter'
|
||||
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 { 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" />
|
||||
|
||||
// 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