Remove PRD files and add prd/ to .gitignore
This commit is contained in:
parent
df7d81ee1d
commit
953dba50bf
9 changed files with 3 additions and 1616 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -87,3 +87,6 @@ typings/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
codebase.md
|
codebase.md
|
||||||
|
|
||||||
|
# PRDs
|
||||||
|
prd/
|
||||||
|
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
# App Router Migration PRD
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Complete migration from Next.js Pages Router to App Router by updating all components to use `next/navigation` instead of `next/router`.
|
|
||||||
|
|
||||||
## Migration Requirements
|
|
||||||
|
|
||||||
### Router API Changes
|
|
||||||
- `useRouter()` from `next/router` → `useRouter()` from `next/navigation`
|
|
||||||
- `router.query` → `useSearchParams()` hook
|
|
||||||
- `router.pathname` → `usePathname()` hook
|
|
||||||
- `router.asPath` → combination of `usePathname()` + `useSearchParams()`
|
|
||||||
- `router.push()` → stays the same
|
|
||||||
- `router.replace()` → stays the same
|
|
||||||
- `router.back()` → stays the same
|
|
||||||
- `router.reload()` → `router.refresh()`
|
|
||||||
- `router.prefetch()` → stays the same
|
|
||||||
- `router.beforePopState()` → not available in App Router (needs alternative)
|
|
||||||
- `router.events` → not available in App Router (needs alternative)
|
|
||||||
|
|
||||||
### Import Updates Required
|
|
||||||
All imports need to change from:
|
|
||||||
```tsx
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
```
|
|
||||||
To:
|
|
||||||
```tsx
|
|
||||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files to Migrate
|
|
||||||
|
|
||||||
### App Directory Files (Already using next/navigation - 6 files)
|
|
||||||
These files already use `next/navigation` but should be verified:
|
|
||||||
- [ ] `/app/teams/TeamsPageClient.tsx`
|
|
||||||
- [ ] `/app/saved/SavedPageClient.tsx`
|
|
||||||
- [ ] `/app/p/[party]/PartyPageClient.tsx`
|
|
||||||
- [ ] `/app/new/NewPartyClient.tsx`
|
|
||||||
- [ ] `/app/components/Header.tsx`
|
|
||||||
- [ ] `/app/[username]/ProfilePageClient.tsx`
|
|
||||||
|
|
||||||
### Pages Directory Files (1 file)
|
|
||||||
- [ ] `/pages/about.tsx` - Needs migration to app router or update imports
|
|
||||||
|
|
||||||
### Component Files - Auth (3 files)
|
|
||||||
- [x] `/components/auth/LoginModal/index.tsx`
|
|
||||||
- [x] `/components/auth/SignupModal/index.tsx`
|
|
||||||
- [x] `/components/auth/AccountModal/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Character (5 files)
|
|
||||||
- [x] `/components/character/CharacterUnit/index.tsx`
|
|
||||||
- [x] `/components/character/CharacterModal/index.tsx`
|
|
||||||
- [x] `/components/character/CharacterHovercard/index.tsx`
|
|
||||||
- [x] `/components/character/CharacterConflictModal/index.tsx`
|
|
||||||
- [x] `/components/character/CharacterResult/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Weapon (7 files)
|
|
||||||
- [x] `/components/weapon/WeaponUnit/index.tsx`
|
|
||||||
- [x] `/components/weapon/WeaponModal/index.tsx`
|
|
||||||
- [x] `/components/weapon/WeaponKeySelect/index.tsx`
|
|
||||||
- [x] `/components/weapon/WeaponHovercard/index.tsx`
|
|
||||||
- [x] `/components/weapon/WeaponConflictModal/index.tsx`
|
|
||||||
- [x] `/components/weapon/WeaponResult/index.tsx`
|
|
||||||
- [x] `/components/weapon/WeaponLabelIcon/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Summon (3 files)
|
|
||||||
- [x] `/components/summon/SummonUnit/index.tsx`
|
|
||||||
- [x] `/components/summon/SummonHovercard/index.tsx`
|
|
||||||
- [x] `/components/summon/SummonResult/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Party (5 files)
|
|
||||||
- [x] `/components/party/PartyHeader/index.tsx`
|
|
||||||
- [x] `/components/party/PartyHead/index.tsx`
|
|
||||||
- [x] `/components/party/PartyFooter/index.tsx`
|
|
||||||
- [x] `/components/party/PartyDropdown/index.tsx`
|
|
||||||
- [x] `/components/party/Party/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Job (7 files)
|
|
||||||
- [x] `/components/job/JobSkillItem/index.tsx`
|
|
||||||
- [x] `/components/job/JobSection/index.tsx`
|
|
||||||
- [x] `/components/job/JobDropdown/index.tsx`
|
|
||||||
- [x] `/components/job/JobAccessoryPopover/index.tsx`
|
|
||||||
- [x] `/components/job/JobAccessoryItem/index.tsx`
|
|
||||||
- [x] `/components/job/JobSkillResult/index.tsx`
|
|
||||||
- [x] `/components/job/JobImage/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Mastery (3 files)
|
|
||||||
- [x] `/components/mastery/ExtendedMasterySelect/index.tsx`
|
|
||||||
- [x] `/components/mastery/AxSelect/index.tsx`
|
|
||||||
- [x] `/components/mastery/AwakeningSelectWithInput/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Reps (4 files)
|
|
||||||
- [x] `/components/reps/GridRep/index.tsx`
|
|
||||||
- [x] `/components/reps/CharacterRep/index.tsx`
|
|
||||||
- [x] `/components/reps/WeaponRep/index.tsx`
|
|
||||||
- [x] `/components/reps/SummonRep/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Common (2 files)
|
|
||||||
- [x] `/components/common/SelectWithInput/index.tsx`
|
|
||||||
- [x] `/components/common/Editor/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Extra (2 files)
|
|
||||||
- [x] `/components/extra/GuidebookUnit/index.tsx`
|
|
||||||
- [x] `/components/extra/GuidebookResult/index.tsx`
|
|
||||||
|
|
||||||
### Component Files - Other (11 files)
|
|
||||||
- [x] `/components/toasts/UpdateToast/index.tsx`
|
|
||||||
- [x] `/components/search/SearchModal/index.tsx`
|
|
||||||
- [x] `/components/raids/RaidCombobox/index.tsx`
|
|
||||||
- [x] `/components/filters/FilterModal/index.tsx`
|
|
||||||
- [x] `/components/MentionList/index.tsx`
|
|
||||||
- [x] `/components/Layout/index.tsx`
|
|
||||||
- [x] `/components/LanguageSwitch/index.tsx`
|
|
||||||
- [x] `/components/Header/index.tsx`
|
|
||||||
- [x] `/components/ElementToggle/index.tsx`
|
|
||||||
- [x] `/components/about/ChangelogUnit/index.tsx`
|
|
||||||
- [x] `/components/HovercardHeader/index.tsx`
|
|
||||||
|
|
||||||
### Utility Files (1 file)
|
|
||||||
- [x] `/utils/changeLanguage.tsx`
|
|
||||||
|
|
||||||
## Total Files to Migrate: 59
|
|
||||||
|
|
||||||
## Migration Steps for Each File
|
|
||||||
|
|
||||||
1. **Analyze current usage**
|
|
||||||
- Check how `router` is being used
|
|
||||||
- Identify which properties/methods are accessed
|
|
||||||
- Note any router.events listeners
|
|
||||||
|
|
||||||
2. **Update imports**
|
|
||||||
- Change from `next/router` to `next/navigation`
|
|
||||||
- Add additional hooks if needed (`usePathname`, `useSearchParams`)
|
|
||||||
|
|
||||||
3. **Update router usage**
|
|
||||||
- Replace `router.query` with `useSearchParams()`
|
|
||||||
- Replace `router.pathname` with `usePathname()`
|
|
||||||
- Update any other router property access
|
|
||||||
|
|
||||||
4. **Handle edge cases**
|
|
||||||
- Router events (need alternative approach)
|
|
||||||
- beforePopState (need alternative approach)
|
|
||||||
- Dynamic route params (use `useParams()`)
|
|
||||||
|
|
||||||
5. **Test the component**
|
|
||||||
- Ensure navigation still works
|
|
||||||
- Check query parameter handling
|
|
||||||
- Verify dynamic routes
|
|
||||||
|
|
||||||
## Special Considerations
|
|
||||||
|
|
||||||
### Router Events
|
|
||||||
Files using `router.events` will need special handling as this is not available in App Router. Common patterns:
|
|
||||||
- Route change start/complete events → Use loading.tsx or Suspense
|
|
||||||
- Route change error → Use error boundaries
|
|
||||||
|
|
||||||
### Query Parameters
|
|
||||||
When migrating `router.query`:
|
|
||||||
```tsx
|
|
||||||
// Old (Pages Router)
|
|
||||||
const { id, filter } = router.query
|
|
||||||
|
|
||||||
// New (App Router)
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const id = searchParams.get('id')
|
|
||||||
const filter = searchParams.get('filter')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dynamic Route Parameters
|
|
||||||
For dynamic segments like `[id]`:
|
|
||||||
```tsx
|
|
||||||
// Old (Pages Router)
|
|
||||||
const { id } = router.query
|
|
||||||
|
|
||||||
// New (App Router)
|
|
||||||
import { useParams } from 'next/navigation'
|
|
||||||
const params = useParams()
|
|
||||||
const id = params.id
|
|
||||||
```
|
|
||||||
|
|
||||||
### Programmatic Navigation
|
|
||||||
```tsx
|
|
||||||
// Both routers (mostly the same)
|
|
||||||
router.push('/path')
|
|
||||||
router.replace('/path')
|
|
||||||
router.back()
|
|
||||||
|
|
||||||
// Refresh (different)
|
|
||||||
// Old: router.reload()
|
|
||||||
// New: router.refresh()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Post-Migration Cleanup
|
|
||||||
|
|
||||||
After all files are migrated:
|
|
||||||
1. [ ] Remove `/pages` directory (except `/pages/api` if still needed)
|
|
||||||
2. [ ] Remove Pages Router specific configuration
|
|
||||||
3. [ ] Update `_app.tsx` dependencies if any remain
|
|
||||||
4. [ ] Test all routes thoroughly
|
|
||||||
5. [ ] Update any documentation
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
- [x] All components use `next/navigation` imports
|
|
||||||
- [x] No references to `next/router` remain in components (only in pages/about.tsx which still needs migration)
|
|
||||||
- [x] All navigation functionality works as before
|
|
||||||
- [x] No console errors about "NextRouter was not mounted"
|
|
||||||
- [x] App runs successfully with `npm run dev`
|
|
||||||
- [ ] Build completes with `npm run build` (to be tested)
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
# Fix App Router Caching Issues PRD
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Fix critical runtime errors in the App Router implementation related to viewport metadata configuration and dynamic data access within cached functions. These fixes are designed to work with Next.js 14 while preparing for migration to Next.js 15.
|
|
||||||
|
|
||||||
## Current Issues
|
|
||||||
|
|
||||||
### Issue 1: Viewport Metadata Warning
|
|
||||||
**Error:** "Unsupported metadata viewport is configured in metadata export. Please move it to viewport export instead."
|
|
||||||
|
|
||||||
**Root Cause:**
|
|
||||||
- Viewport configuration is incorrectly placed in the `metadata` export in `app/layout.tsx`
|
|
||||||
- Next.js 13+ App Router requires viewport to be a separate export
|
|
||||||
|
|
||||||
**Impact:** Warning on every page load, potential SEO and mobile rendering issues
|
|
||||||
|
|
||||||
### Issue 2: Dynamic Data in Cached Functions
|
|
||||||
**Error:** "Route /new used 'cookies' inside a function cached with 'unstable_cache(...)'. Accessing Dynamic data sources inside a cache scope is not supported."
|
|
||||||
|
|
||||||
**Root Cause:**
|
|
||||||
- Data fetching functions in `app/lib/data.ts` use `unstable_cache` wrapper
|
|
||||||
- Inside cached functions, `fetchFromApi()` calls `getAuthToken()` which accesses `cookies()`
|
|
||||||
- Next.js doesn't allow dynamic data sources (cookies, headers) inside cached scopes
|
|
||||||
|
|
||||||
**Impact:** Pages fail to load, API calls fail, poor user experience
|
|
||||||
|
|
||||||
## Files to be Modified
|
|
||||||
|
|
||||||
### Core Files Requiring Changes
|
|
||||||
|
|
||||||
#### 1. `/app/layout.tsx`
|
|
||||||
**Changes needed:**
|
|
||||||
- Remove `viewport` property from `metadata` export (line 16)
|
|
||||||
- Add new `viewport` export with proper format
|
|
||||||
- Fix import conflict with Radix UI's Viewport component
|
|
||||||
|
|
||||||
#### 2. `/app/lib/data.ts`
|
|
||||||
**Changes needed:**
|
|
||||||
- Remove `unstable_cache` wrapper from all 10 functions:
|
|
||||||
- `getTeams()` (lines 30-52)
|
|
||||||
- `getTeam()` (lines 58-69)
|
|
||||||
- `getUserInfo()` (lines 73-84)
|
|
||||||
- `getRaidGroups()` (lines 88-99)
|
|
||||||
- `getVersion()` (lines 103-114)
|
|
||||||
- `getFavorites()` (lines 118-129)
|
|
||||||
- `getJobs()` (lines 133-151)
|
|
||||||
- `getJob()` (lines 155-166)
|
|
||||||
- `getJobSkills()` (lines 170-182)
|
|
||||||
- `getJobAccessories()` (lines 186-197)
|
|
||||||
- Remove `unstable_cache` import from line 1
|
|
||||||
|
|
||||||
### Pages That Use These Functions (Testing Required)
|
|
||||||
|
|
||||||
#### 3. `/app/new/page.tsx`
|
|
||||||
- Uses: `getRaidGroups()` (line 14)
|
|
||||||
- Test: Page loads, raid groups populate
|
|
||||||
|
|
||||||
#### 4. `/app/teams/page.tsx`
|
|
||||||
- Uses: `getTeams()`
|
|
||||||
- Test: Teams list loads with filters
|
|
||||||
|
|
||||||
#### 5. `/app/saved/page.tsx`
|
|
||||||
- Uses: `getFavorites()`
|
|
||||||
- Test: Saved teams display correctly
|
|
||||||
|
|
||||||
#### 6. `/app/p/[party]/page.tsx`
|
|
||||||
- Uses: `getTeam()`
|
|
||||||
- Test: Individual party pages load
|
|
||||||
|
|
||||||
#### 7. `/app/[username]/page.tsx`
|
|
||||||
- Uses: `getUserInfo()`, `getTeams()`
|
|
||||||
- Test: User profiles load with their teams
|
|
||||||
|
|
||||||
### Supporting Files (No changes, but related)
|
|
||||||
|
|
||||||
#### 8. `/app/lib/api-utils.ts`
|
|
||||||
- Contains `getAuthToken()` that causes the issue
|
|
||||||
- No changes needed, just awareness
|
|
||||||
|
|
||||||
## Next.js 15 Migration Context
|
|
||||||
|
|
||||||
### Key Next.js 15 Changes
|
|
||||||
1. **Caching Behavior**
|
|
||||||
- Fetch requests no longer cached by default
|
|
||||||
- `unstable_cache` being deprecated in favor of `use cache` directive
|
|
||||||
- More explicit cache control
|
|
||||||
|
|
||||||
2. **Async Request APIs**
|
|
||||||
- `cookies()`, `headers()`, and `params` become async
|
|
||||||
- Better separation between static and dynamic data
|
|
||||||
|
|
||||||
3. **Request Deduplication**
|
|
||||||
- Automatic deduplication of identical requests
|
|
||||||
- No need for manual caching in many cases
|
|
||||||
|
|
||||||
### Migration Strategy
|
|
||||||
Our fixes should align with Next.js 15's philosophy:
|
|
||||||
- Remove implicit caching
|
|
||||||
- Rely on framework's built-in optimizations
|
|
||||||
- Prepare for async request APIs
|
|
||||||
|
|
||||||
## Proposed Solution
|
|
||||||
|
|
||||||
### Solution for Viewport Issue
|
|
||||||
Move viewport configuration to a separate export in `app/layout.tsx`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Remove from metadata
|
|
||||||
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', // REMOVE
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add as separate export
|
|
||||||
export const viewport = {
|
|
||||||
width: 'device-width',
|
|
||||||
initialScale: 1,
|
|
||||||
viewportFit: 'cover',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Solution for Caching Issue
|
|
||||||
Remove `unstable_cache` wrapper from all data fetching functions. This aligns with Next.js 15's default behavior and eliminates the dynamic data issue.
|
|
||||||
|
|
||||||
**Before:**
|
|
||||||
```typescript
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After:**
|
|
||||||
```typescript
|
|
||||||
export async function getRaidGroups() {
|
|
||||||
try {
|
|
||||||
const data = await fetchFromApi('/raids/groups');
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch raid groups', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rationale
|
|
||||||
1. **Simplicity** - Removes complexity of cache management
|
|
||||||
2. **Next.js 15 Ready** - Aligns with new caching defaults
|
|
||||||
3. **No Dynamic Data Issues** - Cookies can be accessed freely
|
|
||||||
4. **Performance** - Next.js automatically deduplicates requests within same render
|
|
||||||
5. **Maintainability** - Less code to maintain and debug
|
|
||||||
|
|
||||||
## Implementation Tasks
|
|
||||||
|
|
||||||
### Phase 1: Immediate Fixes (Next.js 14)
|
|
||||||
|
|
||||||
#### File: `/app/layout.tsx`
|
|
||||||
- [ ] Remove viewport from metadata export (line 16)
|
|
||||||
- [ ] Add viewport as separate export
|
|
||||||
- [ ] Rename Radix Toast Viewport import to avoid naming conflict
|
|
||||||
|
|
||||||
#### File: `/app/lib/data.ts`
|
|
||||||
- [ ] Remove `import { unstable_cache } from 'next/cache'` (line 1)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getTeams()` (lines 30-52)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getTeam()` (lines 58-69)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getUserInfo()` (lines 73-84)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getRaidGroups()` (lines 88-99)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getVersion()` (lines 103-114)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getFavorites()` (lines 118-129)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getJobs()` (lines 133-151)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getJob()` (lines 155-166)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getJobSkills()` (lines 170-182)
|
|
||||||
- [ ] Remove `unstable_cache` wrapper from `getJobAccessories()` (lines 186-197)
|
|
||||||
|
|
||||||
#### Testing
|
|
||||||
- [ ] Test `/new` page loads without errors (uses getRaidGroups)
|
|
||||||
- [ ] Test `/teams` page functionality (uses getTeams)
|
|
||||||
- [ ] Test `/saved` page functionality (uses getFavorites)
|
|
||||||
- [ ] Test `/p/[party]` pages load (uses getTeam)
|
|
||||||
- [ ] Test `/[username]` profile pages (uses getUserInfo, getTeams)
|
|
||||||
- [ ] Verify API calls work with authentication
|
|
||||||
- [ ] Check console for any remaining warnings
|
|
||||||
|
|
||||||
### Phase 2: Next.js 15 Migration Prep
|
|
||||||
- [ ] Document any performance impacts from removing caching
|
|
||||||
- [ ] Identify truly static data that might benefit from explicit caching
|
|
||||||
- [ ] Plan implementation of `use cache` directive for Next.js 15
|
|
||||||
- [ ] Prepare for async `cookies()` and `headers()` migration
|
|
||||||
|
|
||||||
### Phase 3: Next.js 15 Migration
|
|
||||||
- [ ] Update to Next.js 15
|
|
||||||
- [ ] Convert `cookies()` calls to async where needed
|
|
||||||
- [ ] Implement `use cache` directive for static data if needed
|
|
||||||
- [ ] Remove any remaining `unstable_cache` imports
|
|
||||||
- [ ] Test full application functionality
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
### Immediate (Phase 1)
|
|
||||||
- [ ] No viewport metadata warnings in console
|
|
||||||
- [ ] No "cookies inside cached function" errors
|
|
||||||
- [ ] All pages load successfully
|
|
||||||
- [ ] API calls work with and without authentication
|
|
||||||
- [ ] No regression in functionality
|
|
||||||
|
|
||||||
### Long-term (Phase 3)
|
|
||||||
- [ ] Successfully running on Next.js 15
|
|
||||||
- [ ] Optimal caching strategy implemented
|
|
||||||
- [ ] Clean codebase without deprecated APIs
|
|
||||||
- [ ] Performance metrics maintained or improved
|
|
||||||
|
|
||||||
## Risks and Mitigations
|
|
||||||
|
|
||||||
### Risk: Performance Impact
|
|
||||||
**Mitigation:**
|
|
||||||
- Next.js automatically deduplicates requests in same render cycle
|
|
||||||
- Monitor performance metrics before/after change
|
|
||||||
- Can implement selective caching if needed
|
|
||||||
|
|
||||||
### Risk: Increased API Load
|
|
||||||
**Mitigation:**
|
|
||||||
- Most data fetching happens server-side with deduplication
|
|
||||||
- API should handle load or implement server-side caching
|
|
||||||
- Can add Redis/memory cache if needed
|
|
||||||
|
|
||||||
## Timeline
|
|
||||||
- Phase 1: Immediate (1-2 hours)
|
|
||||||
- Phase 2: Before Next.js 15 migration (documentation only)
|
|
||||||
- Phase 3: During Next.js 15 migration sprint
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- This approach prioritizes simplicity and forward compatibility
|
|
||||||
- Aligns with Next.js team's recommended patterns
|
|
||||||
- Reduces technical debt before major version upgrade
|
|
||||||
- Makes codebase more maintainable and predictable
|
|
||||||
- Total of 2 core files need modification, 5 pages need testing
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
# Product Requirements Document: Fix "Element type is invalid" Error
|
|
||||||
|
|
||||||
## Problem Statement
|
|
||||||
After implementing the Party component interface fixes, pages still fail to load with the error:
|
|
||||||
```
|
|
||||||
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined
|
|
||||||
```
|
|
||||||
|
|
||||||
This error occurs when React tries to render a component that is `undefined`, typically due to:
|
|
||||||
- Incorrect imports
|
|
||||||
- Missing exports
|
|
||||||
- Incompatible APIs between Pages Router and App Router
|
|
||||||
- SVG import issues
|
|
||||||
|
|
||||||
## Investigation Findings
|
|
||||||
|
|
||||||
### Components Verified as Working
|
|
||||||
✅ **Party Component** (`/components/party/Party/index.tsx`)
|
|
||||||
- Has proper 'use client' directive
|
|
||||||
- All imports exist and are correct
|
|
||||||
- Proper default export
|
|
||||||
- All child components properly imported
|
|
||||||
|
|
||||||
✅ **Grid Components**
|
|
||||||
- WeaponGrid, SummonGrid, CharacterGrid all have proper exports
|
|
||||||
- All their child components exist and work
|
|
||||||
|
|
||||||
✅ **Common Components**
|
|
||||||
- Button, Overlay, SegmentedControl have proper exports
|
|
||||||
- Alert component exists and works
|
|
||||||
|
|
||||||
✅ **SVG Icons**
|
|
||||||
- All SVG files exist with proper content
|
|
||||||
- Icons: Edit.svg, Remix.svg, Save.svg, Private.svg, Unlisted.svg, Check.svg, Ellipsis.svg
|
|
||||||
|
|
||||||
### Root Cause Analysis
|
|
||||||
|
|
||||||
#### Primary Issue: next/head in Client Components
|
|
||||||
The most critical issue is components using `next/head` which is incompatible with App Router:
|
|
||||||
|
|
||||||
**How this causes the error:**
|
|
||||||
1. `next/head` is a Pages Router API
|
|
||||||
2. In App Router context, `next/head` exports become `undefined`
|
|
||||||
3. When components try to render `<Head>`, they render `undefined`
|
|
||||||
4. React throws "Element type is invalid: got undefined"
|
|
||||||
|
|
||||||
#### Secondary Issues
|
|
||||||
1. **Import path inconsistencies** - Some components use 'types' instead of '~types'
|
|
||||||
2. **Potential SVG import issues** - Some SVG imports might need `.default` accessor
|
|
||||||
3. **Missing error boundaries** - No way to catch and debug component errors
|
|
||||||
|
|
||||||
## Files That Need Changes
|
|
||||||
|
|
||||||
### Critical Files (Using next/head)
|
|
||||||
- [ ] `/components/party/PartyHead/index.tsx` - Uses `import Head from 'next/head'`
|
|
||||||
- [ ] `/components/head/NewHead/index.tsx` - Uses `import Head from 'next/head'`
|
|
||||||
- [ ] `/components/head/ProfileHead/index.tsx` - Uses `import Head from 'next/head'`
|
|
||||||
- [ ] `/components/head/SavedHead/index.tsx` - Uses `import Head from 'next/head'`
|
|
||||||
- [ ] `/components/head/TeamsHead/index.tsx` - Uses `import Head from 'next/head'`
|
|
||||||
- [ ] `/components/about/AboutHead/index.tsx` - Uses `import Head from 'next/head'`
|
|
||||||
|
|
||||||
### Files Already Using next/head (Need Verification)
|
|
||||||
These files in pages directory are okay since they're Pages Router:
|
|
||||||
- `/pages/_app.tsx` - Pages Router, can use next/head
|
|
||||||
- `/pages/*.tsx` - All pages files can use next/head
|
|
||||||
|
|
||||||
### Components That Import Head Components
|
|
||||||
- [ ] Check if any App Router pages import the head components above
|
|
||||||
- [ ] Verify PartyHead is not being used anywhere
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase 1: Remove/Replace next/head Usage
|
|
||||||
|
|
||||||
#### Option A: Remove Head Components (Recommended for unused components)
|
|
||||||
For components that are not actively used in App Router:
|
|
||||||
1. Delete the component file entirely
|
|
||||||
2. Remove any imports of these components
|
|
||||||
3. Metadata should be handled in page.tsx files instead
|
|
||||||
|
|
||||||
#### Option B: Convert to Client-Safe Components
|
|
||||||
For components that need to be preserved:
|
|
||||||
1. Remove `import Head from 'next/head'`
|
|
||||||
2. Remove `<Head>` wrapper
|
|
||||||
3. Return only the content that should be rendered in the component
|
|
||||||
4. Move metadata to the parent page.tsx file
|
|
||||||
|
|
||||||
### Phase 2: Fix Import Issues
|
|
||||||
1. Ensure all imports use correct paths ('~types' not 'types')
|
|
||||||
2. Verify SVG imports work correctly
|
|
||||||
3. Check for circular dependencies
|
|
||||||
|
|
||||||
### Phase 3: Add Error Boundaries
|
|
||||||
1. Create an ErrorBoundary component
|
|
||||||
2. Wrap Party component to catch errors
|
|
||||||
3. Add logging for better debugging
|
|
||||||
|
|
||||||
## Task List
|
|
||||||
|
|
||||||
### Immediate Actions
|
|
||||||
1. **Investigate PartyHead usage**
|
|
||||||
- Check if PartyHead is imported anywhere
|
|
||||||
- If not used, delete it
|
|
||||||
- If used, convert to App Router compatible component
|
|
||||||
|
|
||||||
2. **Remove unused head components**
|
|
||||||
- Delete NewHead (not used, metadata in page.tsx)
|
|
||||||
- Delete ProfileHead (not used, metadata in page.tsx)
|
|
||||||
- Delete SavedHead (not used, metadata in page.tsx)
|
|
||||||
- Delete TeamsHead (not used, metadata in page.tsx)
|
|
||||||
- Delete AboutHead (check usage first)
|
|
||||||
|
|
||||||
3. **Fix PartyHead if needed**
|
|
||||||
- Remove next/head import
|
|
||||||
- Convert to regular component
|
|
||||||
- Move metadata logic to parent
|
|
||||||
|
|
||||||
4. **Verify all imports**
|
|
||||||
- Check all components for correct import paths
|
|
||||||
- Fix any SVG import issues
|
|
||||||
- Resolve any circular dependencies
|
|
||||||
|
|
||||||
5. **Test thoroughly**
|
|
||||||
- Test /new page
|
|
||||||
- Test /p/[party] pages
|
|
||||||
- Test all other App Router pages
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
- No "Element type is invalid" errors
|
|
||||||
- All pages load successfully
|
|
||||||
- Party component renders correctly
|
|
||||||
- Tab navigation works
|
|
||||||
- All interactive features function
|
|
||||||
|
|
||||||
## Risk Mitigation
|
|
||||||
1. **Backup current state** - Commit current changes before modifications
|
|
||||||
2. **Test incrementally** - Fix and test one component at a time
|
|
||||||
3. **Use error boundaries** - Add error handling to isolate issues
|
|
||||||
4. **Maintain rollback plan** - Keep track of changes for easy reversion
|
|
||||||
|
|
||||||
## Expected Outcome
|
|
||||||
After implementing these fixes:
|
|
||||||
1. The "Element type is invalid" error will be resolved
|
|
||||||
2. All App Router pages will load correctly
|
|
||||||
3. The Party component will function as expected
|
|
||||||
4. Better error handling will be in place for future debugging
|
|
||||||
|
|
||||||
## Timeline
|
|
||||||
- Phase 1: 30 minutes (remove/fix head components)
|
|
||||||
- Phase 2: 15 minutes (fix imports)
|
|
||||||
- Phase 3: 15 minutes (add error boundaries)
|
|
||||||
- Testing: 15 minutes
|
|
||||||
- Total: ~1.25 hours
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- The head components were likely created during initial development for Pages Router
|
|
||||||
- In App Router, metadata should be handled via the metadata export in page.tsx files
|
|
||||||
- Many of these head components appear to be unused since metadata is already defined in the App Router pages
|
|
||||||
- Removing unused components will simplify the codebase and prevent future issues
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
# PRD: Fix App Version Fetching with Server Prefetch + Hydrator
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
- The app shows a missing translation key for `common.toasts.update.description.` because `appState.version.update_type` is empty.
|
|
||||||
- Root cause: The app never fetches or populates `appState.version` with backend data.
|
|
||||||
|
|
||||||
## Current State (validated)
|
|
||||||
- Backend API and internal route exist and work: `/app/api/version/route.ts` calls `fetchFromApi('/version')`.
|
|
||||||
- Client util exists but is unused: `utils/fetchLatestVersion.tsx`.
|
|
||||||
- Global state shape: `utils/appState.tsx` initializes `version` as `{ version: '0.0', update_type: '', updated_at: '' }`.
|
|
||||||
- `UpdateToastClient` reads `appState.version` but never triggers a fetch and only checks on mount.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
- Populate `appState.version` reliably on app load without extra client latency.
|
|
||||||
- Show the update toast exactly when appropriate (within time window and not already seen).
|
|
||||||
- Avoid redundant network requests and race conditions.
|
|
||||||
|
|
||||||
## Solution Overview
|
|
||||||
- Server-prefetch the version in the localized layout (`app/[locale]/layout.tsx`).
|
|
||||||
- Hydrate global state on the client via a tiny `VersionHydrator` client component.
|
|
||||||
- Make `UpdateToastClient` reactive to version changes using Valtio snapshots.
|
|
||||||
- Ensure `update_type` maps to valid i18n keys; add a small fallback mapping.
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
1) Server Prefetch in `app/[locale]/layout.tsx`
|
|
||||||
- Fetch version data on the server and pass to the client for hydration.
|
|
||||||
- Use either of:
|
|
||||||
- Direct helper: `const version = await fetchFromApi('/version')` (from `app/lib/api-utils.ts`), or
|
|
||||||
- Next fetch: `const res = await fetch('/api/version', { cache: 'no-store' }); const version = await res.json();`
|
|
||||||
- Do this inside the default exported async layout function.
|
|
||||||
- Handle errors gracefully (wrap in try/catch and set `version = null`).
|
|
||||||
|
|
||||||
2) Add `VersionHydrator` (client)
|
|
||||||
- File: `app/components/VersionHydrator.tsx`
|
|
||||||
- Behavior: On mount and when `version` prop changes, set `appState.version`.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
- 'use client'
|
|
||||||
- import { useEffect } from 'react'
|
|
||||||
- import { appState } from '~/utils/appState'
|
|
||||||
- export default function VersionHydrator({ version }: { version: AppUpdate | null }) {
|
|
||||||
useEffect(() => {
|
|
||||||
if (version && version.updated_at) {
|
|
||||||
appState.version = version
|
|
||||||
}
|
|
||||||
}, [version])
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
3) Wire Hydrator in Layout
|
|
||||||
- In `app/[locale]/layout.tsx`, render `<VersionHydrator version={version} />` alongside `<Header />` and `<UpdateToastClient />`.
|
|
||||||
- Order suggestion (not strict): Hydrator before UpdateToastClient.
|
|
||||||
|
|
||||||
4) Make `UpdateToastClient` reactive
|
|
||||||
- Import `useSnapshot` from `valtio` and observe `appState.version`.
|
|
||||||
- Change the `useEffect` dependency to run when `version?.updated_at` becomes available.
|
|
||||||
- This ensures the toast can open even if hydration happens after mount.
|
|
||||||
|
|
||||||
Sketch:
|
|
||||||
- import { useSnapshot } from 'valtio'
|
|
||||||
- const { version } = useSnapshot(appState)
|
|
||||||
- useEffect(() => {
|
|
||||||
if (version && version.updated_at) {
|
|
||||||
const cookie = getToastCookie(version.updated_at)
|
|
||||||
const now = new Date()
|
|
||||||
const updatedAt = new Date(version.updated_at)
|
|
||||||
const validUntil = add(updatedAt, { days: 7 })
|
|
||||||
if (now < validUntil && !cookie.seen) setUpdateToastOpen(true)
|
|
||||||
}
|
|
||||||
}, [version?.updated_at])
|
|
||||||
|
|
||||||
5) Validate i18n key mapping for `update_type`
|
|
||||||
- Current keys: `common.toasts.update.description.content` and `...feature`.
|
|
||||||
- Ensure API returns only these (or map unknown types to a default):
|
|
||||||
- const typeKey = ['content', 'feature'].includes(version.update_type) ? version.update_type : 'content'
|
|
||||||
- appState.version.update_type = typeKey
|
|
||||||
|
|
||||||
6) Optional: Pages Router compatibility (temporary)
|
|
||||||
- If the Pages Router `pages/_app.tsx` still needs server-unavailable behavior, add a small client hydrator there too or remove coupling to `appState.version`.
|
|
||||||
|
|
||||||
## Files to Update
|
|
||||||
- `app/[locale]/layout.tsx` (server prefetch + render Hydrator)
|
|
||||||
- `app/components/VersionHydrator.tsx` (new)
|
|
||||||
- `app/components/UpdateToastClient.tsx` (valtio snapshot + effect dependency)
|
|
||||||
- Optional: Add a small mapping for `update_type` before setting state
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
- With API up: `appState.version` is populated on first render; toast opens if within 7 days and unseen.
|
|
||||||
- With API down: No crash; state remains initial; toast doesn’t open.
|
|
||||||
- Verify both locales; i18n keys resolve correctly for `update_type`.
|
|
||||||
- Confirm no duplicate requests across navigations.
|
|
||||||
|
|
||||||
## Risks & Mitigations
|
|
||||||
- Race between hydrator and toast effect: mitigated by making toast reactive to `version?.updated_at`.
|
|
||||||
- Caching behavior: using `no-store` avoids stale data; can switch to `revalidate` if desired.
|
|
||||||
- API type mismatch: handle unknown `update_type` with a default mapping.
|
|
||||||
|
|
||||||
## Rollout Steps
|
|
||||||
- Implement server prefetch and hydrator.
|
|
||||||
- Update `UpdateToastClient` reactivity.
|
|
||||||
- Ship behind no feature flags (safe, read-only state).
|
|
||||||
- Observe logs and confirm behavior in both locales.
|
|
||||||
|
|
||||||
|
|
@ -1,454 +0,0 @@
|
||||||
# PRD: Migrate i18n from next-i18next to next-intl for App Router
|
|
||||||
|
|
||||||
## Current Status (Sep 2, 2025)
|
|
||||||
**Migration is ~90% complete. App is now running successfully!**
|
|
||||||
|
|
||||||
### What's Working:
|
|
||||||
- ✅ Core configuration complete (next.config.js wrapped with plugin, middleware composed)
|
|
||||||
- ✅ App Router structure migrated to [locale] segments
|
|
||||||
- ✅ ALL 76 component files now have correct `next-intl` imports
|
|
||||||
- ✅ Navigation helper created at `/i18n/navigation.ts`
|
|
||||||
- ✅ All `react-i18next` and `next-i18next` imports have been replaced
|
|
||||||
- ✅ **App compiles and runs without import errors**
|
|
||||||
- ✅ English locale loads successfully
|
|
||||||
|
|
||||||
### What's Pending:
|
|
||||||
- ⚠️ 18 components have `<Trans>` components that need refactoring to `t.rich()`
|
|
||||||
- ⚠️ Most components still use standard Next.js navigation instead of locale-aware navigation
|
|
||||||
- ⚠️ Minor issue: Missing translation key `common.toasts.update.description.`
|
|
||||||
- ⚠️ Japanese locale testing needed
|
|
||||||
|
|
||||||
### Next Steps:
|
|
||||||
1. Test the app to verify it runs without import errors
|
|
||||||
2. Update navigation imports to use locale-aware routing
|
|
||||||
3. Refactor Trans components to use `t.rich()`
|
|
||||||
4. Test locale switching
|
|
||||||
5. Clean up old config files
|
|
||||||
|
|
||||||
## Problem Statement
|
|
||||||
The application is displaying raw translation keys (e.g., `party.segmented_control.characters`) instead of localized strings. This is because:
|
|
||||||
- The codebase uses `next-i18next` which is designed for Pages Router
|
|
||||||
- App Router pages don't have proper i18n provider setup
|
|
||||||
- Client components are trying to use `useTranslation` from `next-i18next` without a provider
|
|
||||||
|
|
||||||
## Solution: Migrate to next-intl
|
|
||||||
We will migrate from `next-i18next` to `next-intl`, adopting idiomatic App Router patterns (Next 13–15): localized route segment, composed middleware (i18n + auth), and locale-aware navigation. We will reuse our existing JSON translation files and remove legacy Next.js i18n config to avoid conflicts.
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### 1. Install Dependencies
|
|
||||||
```bash
|
|
||||||
npm install next-intl
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Create i18n Configuration
|
|
||||||
Create `/i18n.config.ts` (single source of truth for locales):
|
|
||||||
```typescript
|
|
||||||
export const locales = ['en', 'ja'] as const
|
|
||||||
export type Locale = (typeof locales)[number]
|
|
||||||
export const defaultLocale: Locale = 'en'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Compose Middleware (i18n + auth)
|
|
||||||
Compose next-intl middleware with our existing auth checks. Keep i18n first, then apply auth to the path without the locale prefix.
|
|
||||||
```typescript
|
|
||||||
// middleware.ts
|
|
||||||
import createMiddleware from 'next-intl/middleware'
|
|
||||||
import {locales, defaultLocale, type Locale} from './i18n.config'
|
|
||||||
import {NextResponse} from 'next/server'
|
|
||||||
import type {NextRequest} from 'next/server'
|
|
||||||
|
|
||||||
const intl = createMiddleware({
|
|
||||||
locales,
|
|
||||||
defaultLocale,
|
|
||||||
localePrefix: 'as-needed' // Show locale in URL when not default
|
|
||||||
})
|
|
||||||
|
|
||||||
const PROTECTED_PATHS = ['/saved', '/profile'] as const
|
|
||||||
const MIXED_AUTH_PATHS = ['/api/parties', '/p/'] as const
|
|
||||||
|
|
||||||
export default function middleware(request: NextRequest) {
|
|
||||||
// Run next-intl first (handles locale detection, redirects, etc.)
|
|
||||||
const intlResponse = intl(request)
|
|
||||||
if (intlResponse) return intlResponse
|
|
||||||
|
|
||||||
const {pathname} = request.nextUrl
|
|
||||||
const seg = pathname.split('/')[1]
|
|
||||||
const pathWithoutLocale = locales.includes(seg as Locale)
|
|
||||||
? pathname.slice(seg.length + 1) || '/'
|
|
||||||
: pathname
|
|
||||||
|
|
||||||
const isProtectedPath = PROTECTED_PATHS.some(
|
|
||||||
(p) => pathWithoutLocale === p || pathWithoutLocale.startsWith(p + '/')
|
|
||||||
)
|
|
||||||
const isMixedAuthPath = MIXED_AUTH_PATHS.some(
|
|
||||||
(p) => pathWithoutLocale === p || pathWithoutLocale.startsWith(p)
|
|
||||||
)
|
|
||||||
|
|
||||||
const needsAuth =
|
|
||||||
isProtectedPath || (isMixedAuthPath && ['POST', 'PUT', 'DELETE'].includes(request.method))
|
|
||||||
|
|
||||||
if (!needsAuth) return NextResponse.next()
|
|
||||||
|
|
||||||
const accountCookie = request.cookies.get('account')
|
|
||||||
if (!accountCookie?.value) {
|
|
||||||
if (pathWithoutLocale.startsWith('/api/')) {
|
|
||||||
return NextResponse.json({error: 'Authentication required'}, {status: 401})
|
|
||||||
}
|
|
||||||
// Preserve locale in redirect
|
|
||||||
const url = request.nextUrl.clone()
|
|
||||||
url.pathname = '/teams'
|
|
||||||
return NextResponse.redirect(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const account = JSON.parse(accountCookie.value)
|
|
||||||
if (!account.token) {
|
|
||||||
if (pathWithoutLocale.startsWith('/api/')) {
|
|
||||||
return NextResponse.json({error: 'Authentication required'}, {status: 401})
|
|
||||||
}
|
|
||||||
const url = request.nextUrl.clone()
|
|
||||||
url.pathname = '/teams'
|
|
||||||
return NextResponse.redirect(url)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (pathWithoutLocale.startsWith('/api/')) {
|
|
||||||
return NextResponse.json({error: 'Authentication required'}, {status: 401})
|
|
||||||
}
|
|
||||||
const url = request.nextUrl.clone()
|
|
||||||
url.pathname = '/teams'
|
|
||||||
return NextResponse.redirect(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: ['/((?!_next|_vercel|.*\\..*).*)']
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Localized Layout (App Router)
|
|
||||||
Create `/app/[locale]/layout.tsx` and place `NextIntlClientProvider` here. Use `unstable_setRequestLocale` to tell Next the active locale and pre-generate locale params if statically building.
|
|
||||||
```typescript
|
|
||||||
// app/[locale]/layout.tsx
|
|
||||||
import {NextIntlClientProvider} from 'next-intl'
|
|
||||||
import {getMessages, unstable_setRequestLocale} from 'next-intl/server'
|
|
||||||
import {locales} from '../../i18n.config'
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
|
||||||
return locales.map((locale) => ({locale}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function LocaleLayout({
|
|
||||||
children,
|
|
||||||
params: {locale}
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
params: {locale: string}
|
|
||||||
}) {
|
|
||||||
unstable_setRequestLocale(locale)
|
|
||||||
const messages = await getMessages()
|
|
||||||
return (
|
|
||||||
<html lang={locale}>
|
|
||||||
<body>
|
|
||||||
<NextIntlClientProvider messages={messages}>{children}</NextIntlClientProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Notes:
|
|
||||||
- If `/app/layout.tsx` exists, keep it minimal (global styles/fonts only) and avoid setting the `lang` attribute or an i18n provider there. The locale-specific layout above should own `<html>`/`<body>`.
|
|
||||||
|
|
||||||
### 5. Locale-Aware Navigation
|
|
||||||
Replace `next/link` and `next/navigation` with `next-intl/navigation` to preserve locale on navigation.
|
|
||||||
```typescript
|
|
||||||
// Before
|
|
||||||
import Link from 'next/link'
|
|
||||||
import {useRouter, usePathname, useSearchParams} from 'next/navigation'
|
|
||||||
|
|
||||||
// After
|
|
||||||
import {Link, useRouter, usePathname, useSearchParams} from 'next-intl/navigation'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Update Component Imports
|
|
||||||
Change all translation imports from:
|
|
||||||
```typescript
|
|
||||||
import { useTranslation } from 'next-i18next'
|
|
||||||
import { Trans } from 'next-i18next'
|
|
||||||
```
|
|
||||||
|
|
||||||
To:
|
|
||||||
```typescript
|
|
||||||
import { useTranslations } from 'next-intl'
|
|
||||||
// Note: Trans component usage will need to be refactored
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Update Translation Hook Usage
|
|
||||||
Change from:
|
|
||||||
```typescript
|
|
||||||
const { t } = useTranslation('common')
|
|
||||||
// Usage: t('party.segmented_control.characters')
|
|
||||||
```
|
|
||||||
|
|
||||||
To:
|
|
||||||
```typescript
|
|
||||||
const t = useTranslations('common')
|
|
||||||
// Usage: t('party.segmented_control.characters')
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. Update Trans Component Usage
|
|
||||||
The `Trans` component works differently in next-intl. Change from:
|
|
||||||
```typescript
|
|
||||||
<Trans i18nKey="modals.summons.messages.remove">
|
|
||||||
Are you sure you want to remove{' '}
|
|
||||||
<strong>{{ weapon: gridSummon?.object.name[locale] }}</strong> from
|
|
||||||
your team?
|
|
||||||
</Trans>
|
|
||||||
```
|
|
||||||
|
|
||||||
To using rich text formatting:
|
|
||||||
```typescript
|
|
||||||
t.rich('modals.summons.messages.remove', {
|
|
||||||
weapon: gridSummon?.object.name[locale],
|
|
||||||
strong: (chunks) => <strong>{chunks}</strong>
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. Language Switch
|
|
||||||
Prefer path-based locale switching over cookie+refresh for clarity and correct URL behavior.
|
|
||||||
```typescript
|
|
||||||
// Using next-intl/navigation
|
|
||||||
const router = useRouter()
|
|
||||||
const pathname = usePathname()
|
|
||||||
|
|
||||||
function changeLanguage(to: 'en'|'ja') {
|
|
||||||
router.replace(pathname, {locale: to})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
You may still set `NEXT_LOCALE` for persistence, but the router option should be the source of truth.
|
|
||||||
|
|
||||||
### 10. Server Helpers and Metadata
|
|
||||||
Use server utilities for server-only files and localized metadata.
|
|
||||||
```typescript
|
|
||||||
// In server components/routes (e.g., not-found.tsx)
|
|
||||||
import {getTranslations} from 'next-intl/server'
|
|
||||||
const t = await getTranslations('common')
|
|
||||||
|
|
||||||
// Localized metadata
|
|
||||||
export async function generateMetadata({params: {locale}}) {
|
|
||||||
const t = await getTranslations({locale, namespace: 'common'})
|
|
||||||
return {title: t('title')}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files to Update
|
|
||||||
|
|
||||||
### Core Configuration Files
|
|
||||||
1. `/package.json` - Add `next-intl` dependency
|
|
||||||
2. `/i18n.config.ts` - Create new configuration file
|
|
||||||
3. `/middleware.ts` - Compose next-intl + auth middleware
|
|
||||||
4. `/i18n/request.ts` - Ensure imports from `../i18n.config` are correct
|
|
||||||
5. `/next.config.js` - Remove `i18n` property (avoid conflict with middleware)
|
|
||||||
6. `/next-i18next.config.js` - Remove after migration
|
|
||||||
|
|
||||||
### App Router Files (7 files)
|
|
||||||
1. `/app/[locale]/layout.tsx` - New; add NextIntlClientProvider
|
|
||||||
2. `/app/new/NewPartyClient.tsx` - Update imports and usage
|
|
||||||
3. `/app/[username]/ProfilePageClient.tsx` - Update imports and usage
|
|
||||||
4. `/app/saved/SavedPageClient.tsx` - Update imports and usage
|
|
||||||
5. `/app/p/[party]/PartyPageClient.tsx` - Update imports and usage
|
|
||||||
6. `/app/teams/TeamsPageClient.tsx` - Update imports and usage
|
|
||||||
7. `/app/not-found.tsx` - Update imports and usage
|
|
||||||
8. `/app/layout.tsx` - If present, keep minimal; remove i18n/lang handling
|
|
||||||
|
|
||||||
### Component Files (76 files)
|
|
||||||
All files in `/components` directory that use translations and/or navigation:
|
|
||||||
1. `/components/Header/index.tsx`
|
|
||||||
2. `/components/ErrorSection/index.tsx`
|
|
||||||
3. `/components/party/Party/index.tsx`
|
|
||||||
4. `/components/party/PartyHeader/index.tsx`
|
|
||||||
5. `/components/party/PartyFooter/index.tsx`
|
|
||||||
6. `/components/party/PartySegmentedControl/index.tsx`
|
|
||||||
7. `/components/party/PartyDropdown/index.tsx`
|
|
||||||
8. `/components/party/EditPartyModal/index.tsx`
|
|
||||||
9. `/components/party/PartyVisibilityDialog/index.tsx`
|
|
||||||
10. `/components/character/CharacterUnit/index.tsx`
|
|
||||||
11. `/components/character/CharacterGrid/index.tsx`
|
|
||||||
12. `/components/character/CharacterModal/index.tsx`
|
|
||||||
13. `/components/character/CharacterHovercard/index.tsx`
|
|
||||||
14. `/components/character/CharacterConflictModal/index.tsx`
|
|
||||||
15. `/components/character/CharacterSearchFilterBar/index.tsx`
|
|
||||||
16. `/components/weapon/WeaponUnit/index.tsx`
|
|
||||||
17. `/components/weapon/WeaponGrid/index.tsx`
|
|
||||||
18. `/components/weapon/WeaponModal/index.tsx`
|
|
||||||
19. `/components/weapon/WeaponHovercard/index.tsx`
|
|
||||||
20. `/components/weapon/WeaponConflictModal/index.tsx`
|
|
||||||
21. `/components/weapon/WeaponKeySelect/index.tsx`
|
|
||||||
22. `/components/weapon/WeaponSearchFilterBar/index.tsx`
|
|
||||||
23. `/components/summon/SummonUnit/index.tsx`
|
|
||||||
24. `/components/summon/SummonGrid/index.tsx`
|
|
||||||
25. `/components/summon/SummonHovercard/index.tsx`
|
|
||||||
26. `/components/summon/SummonSearchFilterBar/index.tsx`
|
|
||||||
27. `/components/job/JobSection/index.tsx`
|
|
||||||
28. `/components/job/JobDropdown/index.tsx`
|
|
||||||
29. `/components/job/JobSkillItem/index.tsx`
|
|
||||||
30. `/components/job/JobAccessoryPopover/index.tsx`
|
|
||||||
31. `/components/job/JobSkillSearchFilterBar/index.tsx`
|
|
||||||
32. `/components/auth/LoginModal/index.tsx`
|
|
||||||
33. `/components/auth/SignupModal/index.tsx`
|
|
||||||
34. `/components/auth/AccountModal/index.tsx`
|
|
||||||
35. `/components/raids/RaidCombobox/index.tsx`
|
|
||||||
36. `/components/raids/RaidItem/index.tsx`
|
|
||||||
37. `/components/search/SearchModal/index.tsx`
|
|
||||||
38. `/components/filters/FilterBar/index.tsx`
|
|
||||||
39. `/components/filters/FilterModal/index.tsx`
|
|
||||||
40. `/components/mastery/AwakeningSelectWithInput/index.tsx`
|
|
||||||
41. `/components/mastery/AxSelect/index.tsx`
|
|
||||||
42. `/components/mastery/ExtendedMasterySelect/index.tsx`
|
|
||||||
43. `/components/uncap/UncapIndicator/index.tsx`
|
|
||||||
44. `/components/uncap/TranscendencePopover/index.tsx`
|
|
||||||
45. `/components/uncap/TranscendenceStar/index.tsx`
|
|
||||||
46. `/components/uncap/TranscendenceFragment/index.tsx`
|
|
||||||
47. `/components/extra/GuidebookUnit/index.tsx`
|
|
||||||
48. `/components/extra/GuidebooksGrid/index.tsx`
|
|
||||||
49. `/components/extra/ExtraWeaponsGrid/index.tsx`
|
|
||||||
50. `/components/extra/ExtraSummonsGrid/index.tsx`
|
|
||||||
51. `/components/reps/CharacterRep/index.tsx`
|
|
||||||
52. `/components/reps/GridRep/index.tsx`
|
|
||||||
53. `/components/toasts/UpdateToast/index.tsx`
|
|
||||||
54. `/components/toasts/UrlCopiedToast/index.tsx`
|
|
||||||
55. `/components/toasts/RemixedToast/index.tsx`
|
|
||||||
56. `/components/dialogs/RemixTeamAlert/index.tsx`
|
|
||||||
57. `/components/dialogs/DeleteTeamAlert/index.tsx`
|
|
||||||
58. `/components/common/Editor/index.tsx`
|
|
||||||
59. `/components/common/SelectWithInput/index.tsx`
|
|
||||||
60. `/components/common/ToolbarButton/index.tsx`
|
|
||||||
61. `/components/common/MentionTypeahead/index.tsx`
|
|
||||||
62. `/components/ElementToggle/index.tsx`
|
|
||||||
63. `/components/MentionList/index.tsx`
|
|
||||||
64. `/components/about/AboutPage/index.tsx`
|
|
||||||
65. `/components/about/AboutHead/index.tsx`
|
|
||||||
66. `/components/about/RoadmapPage/index.tsx`
|
|
||||||
67. `/components/about/UpdatesPage/index.tsx`
|
|
||||||
68. `/components/about/ContentUpdate/index.tsx`
|
|
||||||
69. `/components/about/updates/ContentUpdate2022/index.tsx`
|
|
||||||
70. `/components/about/updates/ContentUpdate2023/index.tsx`
|
|
||||||
71. `/components/about/updates/ContentUpdate2024/index.tsx`
|
|
||||||
72. `/components/head/NewHead/index.tsx`
|
|
||||||
73. `/components/head/ProfileHead/index.tsx`
|
|
||||||
74. `/components/head/SavedHead/index.tsx`
|
|
||||||
75. `/components/head/TeamsHead/index.tsx`
|
|
||||||
76. `/components/party/PartyHead/index.tsx`
|
|
||||||
|
|
||||||
### Pages Router Files (temporary)
|
|
||||||
Plan to remove Pages Router usage. Temporarily keep while migrating:
|
|
||||||
1. `/pages/_app.tsx` - Keep until all pages are on App Router
|
|
||||||
2. `/pages/about.tsx` - Keep until migrated
|
|
||||||
|
|
||||||
## Task List
|
|
||||||
|
|
||||||
- [x] **Setup and Configuration**
|
|
||||||
- [x] Install next-intl package (v4.3.5)
|
|
||||||
- [x] Create `/i18n.config.ts` with locale configuration
|
|
||||||
- [x] Create/update `/middleware.ts` for locale routing (composed with auth)
|
|
||||||
- [x] Update `/next.config.js` with `createNextIntlPlugin` wrapper
|
|
||||||
- [x] Create `/i18n/navigation.ts` with `createNavigation` for locale-aware routing
|
|
||||||
|
|
||||||
- [x] **Localized Layout**
|
|
||||||
- [x] Create `/app/[locale]/layout.tsx` with `unstable_setRequestLocale`
|
|
||||||
- [x] Load messages with `getMessages()` and wrap with `NextIntlClientProvider`
|
|
||||||
- [x] Keep `/app/layout.tsx` minimal (no lang/i18n)
|
|
||||||
|
|
||||||
- [x] **Update App Router Pages (7+ files)**
|
|
||||||
- [x] Update `/app/new/NewPartyClient.tsx`
|
|
||||||
- [x] Update `/app/[username]/ProfilePageClient.tsx`
|
|
||||||
- [x] Update `/app/saved/SavedPageClient.tsx`
|
|
||||||
- [x] Update `/app/p/[party]/PartyPageClient.tsx`
|
|
||||||
- [x] Update `/app/teams/TeamsPageClient.tsx`
|
|
||||||
- [x] Update `/app/not-found.tsx`
|
|
||||||
- [x] Update `/app/components/Header.tsx`
|
|
||||||
|
|
||||||
- [ ] **Update Component Imports (76 files)**
|
|
||||||
- [x] Partial: 54/76 files updated to use `next-intl`
|
|
||||||
- [x] Fix remaining 8 files with `react-i18next` imports
|
|
||||||
- [x] Fix remaining 11 files with `next-i18next` imports
|
|
||||||
- [ ] Replace `next/link` and `next/navigation` with locale-aware navigation (only 1 file done)
|
|
||||||
- [ ] Refactor 18 files still using `Trans` component to `t.rich()`
|
|
||||||
|
|
||||||
- [x] **Testing and Verification**
|
|
||||||
- [x] Test English locale - App loads successfully!
|
|
||||||
- [ ] Test Japanese locale
|
|
||||||
- [ ] Verify locale switching works
|
|
||||||
- [ ] Check all translation keys render correctly
|
|
||||||
- [ ] Test dynamic translations with variables
|
|
||||||
|
|
||||||
- [ ] **Cleanup**
|
|
||||||
- [ ] Remove `i18n` from `next.config.js`
|
|
||||||
- [ ] Remove `/next-i18next.config.js`
|
|
||||||
- [ ] Remove `next-i18next` from `package.json`
|
|
||||||
- [ ] Update documentation
|
|
||||||
|
|
||||||
## Files Still Requiring Updates
|
|
||||||
|
|
||||||
### Files with `react-i18next` imports (8 files): ✅ COMPLETED
|
|
||||||
1. ✅ `/components/auth/LoginModal/index.tsx` - Fixed import
|
|
||||||
2. ✅ `/components/filters/FilterModal/index.tsx` - Fixed import, commented Trans
|
|
||||||
3. ✅ `/components/job/JobSkillSearchFilterBar/index.tsx` - Fixed import
|
|
||||||
4. ✅ `/components/party/EditPartyModal/index.tsx` - Fixed import, simplified Trans
|
|
||||||
5. ✅ `/components/party/PartyVisibilityDialog/index.tsx` - Fixed import
|
|
||||||
6. ✅ `/components/raids/RaidCombobox/index.tsx` - Fixed import
|
|
||||||
7. ✅ `/components/search/SearchModal/index.tsx` - Fixed import
|
|
||||||
8. ✅ `/components/weapon/WeaponConflictModal/index.tsx` - Fixed import, simplified Trans
|
|
||||||
|
|
||||||
### Files with `next-i18next` imports (11 files): ✅ COMPLETED
|
|
||||||
1. ✅ `/components/about/AboutPage/index.tsx` - Fixed import, simplified Trans
|
|
||||||
2. ✅ `/components/character/CharacterConflictModal/index.tsx` - Fixed import
|
|
||||||
3. ✅ `/components/character/CharacterModal/index.tsx` - Fixed import
|
|
||||||
4. ✅ `/components/character/CharacterUnit/index.tsx` - Fixed import
|
|
||||||
5. ✅ `/components/dialogs/RemixTeamAlert/index.tsx` - Fixed import
|
|
||||||
6. ✅ `/components/extra/GuidebookUnit/index.tsx` - Fixed import
|
|
||||||
7. ✅ `/components/job/JobSkillItem/index.tsx` - Fixed import
|
|
||||||
8. ✅ `/components/summon/SummonUnit/index.tsx` - Fixed import
|
|
||||||
9. ✅ `/components/toasts/RemixedToast/index.tsx` - Fixed import
|
|
||||||
10. ✅ `/components/weapon/WeaponModal/index.tsx` - Fixed import
|
|
||||||
11. ✅ `/components/weapon/WeaponUnit/index.tsx` - Fixed import
|
|
||||||
|
|
||||||
### Additional files with Trans components but correct imports (3 files):
|
|
||||||
1. `/components/auth/SignupModal/index.tsx` - Imports correct but Trans is commented out
|
|
||||||
2. `/components/uncap/TranscendencePopover/index.tsx`
|
|
||||||
3. `/components/uncap/TranscendenceStar/index.tsx`
|
|
||||||
4. `/components/uncap/UncapIndicator/index.tsx`
|
|
||||||
|
|
||||||
### Navigation Updates Required:
|
|
||||||
- Most components still use `next/navigation` instead of `~/i18n/navigation`
|
|
||||||
- Only 1 component currently uses the locale-aware navigation
|
|
||||||
- The navigation helper exists at `/i18n/navigation.ts` using `createNavigation`
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
1. All translation keys render as localized strings (not raw keys)
|
|
||||||
2. Locale switching between English and Japanese works and preserves locale in URLs
|
|
||||||
3. Dynamic translations with variables work correctly
|
|
||||||
4. No console errors related to i18n
|
|
||||||
5. App Router pages work correctly; Pages Router pages are removed or temporarily functional during migration
|
|
||||||
|
|
||||||
## Risks and Mitigation
|
|
||||||
- **Risk**: Middleware composition can cause routing conflicts
|
|
||||||
- **Mitigation**: Run next-intl middleware first; strip locale before auth checks; unify matcher
|
|
||||||
|
|
||||||
- **Risk**: Breaking Pages Router pages during transition
|
|
||||||
- **Mitigation**: Keep Pages Router temporarily; schedule removal after App Router parity
|
|
||||||
|
|
||||||
- **Risk**: Trans component refactoring may be complex
|
|
||||||
- **Mitigation**: Start with simple replacements; handle rich content with `t.rich()` gradually
|
|
||||||
|
|
||||||
- **Risk**: Large number of files to update
|
|
||||||
- **Mitigation**: Batch replace imports; test incrementally; prioritize high-traffic routes
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- The existing translation files in `/public/locales/` can be reused without changes
|
|
||||||
- The `i18n/request.ts` file already uses next-intl; ensure it imports from the root `i18n.config.ts`
|
|
||||||
- Use `next-intl/navigation` everywhere for links and routers to preserve locale
|
|
||||||
- Remove `i18n` from `next.config.js` to avoid conflicts with next-intl middleware routing
|
|
||||||
- Consider using a codemod/script to automate import updates across 76+ files
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
# PRD: Migrate Web App from Next.js to SvelteKit (Svelte 5 + Runes)
|
|
||||||
|
|
||||||
## Context
|
|
||||||
The app is a small but interactive site for building, saving, and sharing Granblue Fantasy team compositions. It currently runs on Next.js 14 using a hybrid of App Router and legacy Pages, with `next-intl` for i18n, Valtio for state, Radix-based UI wrappers, and Tiptap for rich text. The backend is a Rails API and will not change.
|
|
||||||
|
|
||||||
## Problem / Opportunity
|
|
||||||
- The UI layer carries Next-specific complexity (hybrid routing, middleware composition, Valtio, React wrappers around primitives) for a scope that aligns well with SvelteKit’s simpler mental model.
|
|
||||||
- Svelte 5 (Runes) and SvelteKit provide small, fast bundles and ergonomic state management for highly interactive UIs like the party editor.
|
|
||||||
- A rewrite would impose upfront cost but could yield better performance, maintainability, and developer experience.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
- Achieve feature and URL parity (including localized routing and auth/method guards) with the current Next.js app.
|
|
||||||
- Reuse domain types, data contracts, and i18n content; minimize churn to the Rails API layer.
|
|
||||||
- Maintain or improve perceived performance, accessibility, and UX of primitives (menus, dialogs, tooltips, toasts).
|
|
||||||
|
|
||||||
## Non‑Goals
|
|
||||||
- No changes to the Rails API endpoints, auth semantics, or data shape.
|
|
||||||
- No new product features beyond parity (performance and a11y improvements are in scope).
|
|
||||||
|
|
||||||
## Current State (Summary)
|
|
||||||
- Framework: Next.js 14 with `app/` and `pages/` side by side.
|
|
||||||
- Routing/i18n: `app/[locale]/…` with `next-intl` and “as-needed” default-locale prefixing; middleware composes i18n with auth/method guards.
|
|
||||||
- State: Valtio global stores for `accountState` and `appState` (party/grid/editor flows).
|
|
||||||
- Data: axios-based `utils/api.tsx`, app route handlers under `app/api/**` proxy to Rails; server helpers in `app/lib/*` with zod validation.
|
|
||||||
- UI: Components under `components/**` using SCSS modules, Radix-based wrappers, custom editor based on Tiptap React + custom extensions.
|
|
||||||
- URL State: `nuqs` binds filters to query params.
|
|
||||||
- Storybook: React/Next preset.
|
|
||||||
|
|
||||||
## Feature/Parity Requirements
|
|
||||||
- Public routes: `/`, `/new`, `/teams`, `/p/[party]` (+ tab segments), `/[username]`, `/about`, `/updates`, `/roadmap` with locale variants under `/ja` and default-locale “as-needed” behavior.
|
|
||||||
- Auth/method gates: protect `/saved` and `/profile`; 401 JSON for unauthorized API mutations; preserve method-based rules under `/api/**`.
|
|
||||||
- Party editor: weapons/summons/characters, job/skills/accessory, guidebooks, toggles, remix/delete, editability rules by cookie/user.
|
|
||||||
- Search & filters: team discovery with element/raid/recency, advanced filters from cookie, pagination (append/replace).
|
|
||||||
- i18n: reuse JSON namespaces; preserve current translation keys and locale cookie behavior.
|
|
||||||
- Primitives: dropdown menu, dialog, tooltip, toast, popover, select, switch, slider, command, segmented control with keyboard a11y.
|
|
||||||
- Editor: Tiptap with mentions, link, lists, headings, youtube, highlight, placeholder.
|
|
||||||
|
|
||||||
## Proposed Direction
|
|
||||||
Rebuild the web app on SvelteKit 2 and Svelte 5 Runes. Keep the Rails API contract intact. Adopt Svelte stores for global state and Runes for local component state. Replace React-specific libraries with Svelte-native equivalents while reusing JSON messages, type declarations, API contracts, and Tiptap core extensions.
|
|
||||||
|
|
||||||
## Target Architecture (SvelteKit)
|
|
||||||
- Routing: `src/routes/[lang=locale]/…` to mirror localized segments; top-level `/` redirects to `/new`.
|
|
||||||
- Hooks: `hooks.server.ts` handles locale detection/redirects and reproduces auth/method gates; `handleFetch` attaches bearer tokens from cookies.
|
|
||||||
- Data loading: `+layout.server.ts`/`+page.server.ts` for SSR (e.g., version, lists); `+page.ts` for client/SSR-unified fetches.
|
|
||||||
- Endpoints: `src/routes/api/**/+server.ts` proxy to Rails with zod validation and consistent status codes.
|
|
||||||
- State: Svelte `writable` stores for `account` and `app` (party, grid, search, jobs, skills, version), with typed helper actions; local UI state via runes.
|
|
||||||
- Theming: small store plus SSR-safe initial theme; apply class/data-theme on `<html>`.
|
|
||||||
- i18n: `svelte-i18n` (recommended) loading existing JSON bundles; locale cookie + accept-language fallback; “as-needed” default-locale paths.
|
|
||||||
|
|
||||||
## Dependency Mapping (Equivalents)
|
|
||||||
- Framework/Core:
|
|
||||||
- `next` → `@sveltejs/kit`
|
|
||||||
- `react`, `react-dom` → `svelte@5`
|
|
||||||
- `@svgr/webpack` → `vite-svg-loader` (or inline SVG)
|
|
||||||
- Routing/URL State:
|
|
||||||
- `next/navigation` → SvelteKit `goto`, `$page`, `afterNavigate`
|
|
||||||
- `nuqs` → `$page.url.searchParams` + small helper or `sveltekit-search-params`
|
|
||||||
- Data/Query:
|
|
||||||
- `@tanstack/react-query` → `@tanstack/svelte-query@5`
|
|
||||||
- `axios` → keep or migrate to `fetch`; keep zod
|
|
||||||
- i18n:
|
|
||||||
- `next-intl` → `svelte-i18n` (reuse JSON); remove `next-i18next`
|
|
||||||
- `i18next*` libs → only if choosing an i18next-based Svelte binding (otherwise remove)
|
|
||||||
- UI/A11y:
|
|
||||||
- `@radix-ui/*` → `bits-ui` primitives and/or `shadcn-svelte`
|
|
||||||
- `tippy.js` → `svelte-tippy` (or small custom)
|
|
||||||
- `cmdk` → `cmdk-svelte`/`cmdk-sv`
|
|
||||||
- Theming:
|
|
||||||
- `next-themes` → small Svelte store + cookie bootstrap
|
|
||||||
- Editor:
|
|
||||||
- `@tiptap/react` → Tiptap core with Svelte integration (e.g., `svelte-tiptap`); reuse custom extensions
|
|
||||||
- Typeahead/Select/Infinite:
|
|
||||||
- `react-bootstrap-typeahead` → `svelte-select`
|
|
||||||
- `react-infinite-scroll-component` → `svelte-infinite-loading` or IO-based action
|
|
||||||
- Misc:
|
|
||||||
- `cookies-next` → `event.cookies` server-side; `js-cookie` client
|
|
||||||
- `react-use`/`usehooks-ts` → `svelte-use`
|
|
||||||
- `remixicon-react` → `svelte-remixicon` or `unplugin-icons` (`ri:*`)
|
|
||||||
- `react-linkify` → `linkifyjs`/`linkify-html`
|
|
||||||
- SCSS → `svelte-preprocess` with component-scoped styles and global imports
|
|
||||||
- Storybook: `@storybook/nextjs` → `@storybook/sveltekit`
|
|
||||||
|
|
||||||
## Reuse vs. Rewrite
|
|
||||||
- Reuse
|
|
||||||
- i18n bundles under `public/locales/{en,ja}`
|
|
||||||
- Domain types `types/*.d.ts` (move to `src/lib/types`)
|
|
||||||
- Pure utilities in `utils/*` (parsers, enums, formatters)
|
|
||||||
- API contracts and zod schemas (`app/lib/api-utils.ts`)
|
|
||||||
- Tiptap custom extensions (`extensions/CustomMention`, `CustomSuggestion`)
|
|
||||||
- Public assets and SVGs
|
|
||||||
- Rewrite
|
|
||||||
- All React components and Valtio stores as Svelte components/stores
|
|
||||||
- Next middleware as `hooks.server.ts` (auth + locale); API handlers as `+server.ts`
|
|
||||||
- URL/query state helpers (replace `nuqs`)
|
|
||||||
- Theming (replace `next-themes`)
|
|
||||||
- Storybook stories
|
|
||||||
- Font handling (move `pages/fonts/gk-variable.woff2` to `static/` and add `@font-face`)
|
|
||||||
|
|
||||||
## Routing & i18n Details
|
|
||||||
- Localized route group `[lang=locale]` constrained to `en|ja` (from existing `i18n.config.ts`).
|
|
||||||
- “As-needed” default-locale behavior via redirects in `handle` and locale-aware links.
|
|
||||||
- Preserve existing route structure and query semantics (e.g., `/teams?element=…&raid=…`).
|
|
||||||
|
|
||||||
## Auth & Cookies
|
|
||||||
- Mirror `middleware.ts` logic: protect `/saved` and `/profile`; method-guard mixed routes; return 401 JSON for API.
|
|
||||||
- Server: use `event.cookies` and set `locals.user`; client: `js-cookie` where necessary.
|
|
||||||
- Use `handleFetch` to inject `Authorization` header from `account` cookie token.
|
|
||||||
|
|
||||||
## State & Data
|
|
||||||
- Global state: writable stores mirroring `initialAccountState` and `initialAppState` with typed actions; local UI state via runes.
|
|
||||||
- Queries: `@tanstack/svelte-query` for client/SSR-safe caching; SSR data via `+page.server.ts` where beneficial.
|
|
||||||
- Invalidation: use SvelteKit `invalidate`/`invalidateAll` in place of Next `revalidatePath`.
|
|
||||||
|
|
||||||
## UI & Editor
|
|
||||||
- Primitives: build on Bits-UI/shadcn-svelte; verify keyboard navigation and focus management; carry over SCSS look-and-feel.
|
|
||||||
- Editor: use Tiptap core with Svelte wrapper; reuse mention/suggestion logic and toolbar feature set (bold/italic/strike/heading/list/link/youtube/highlight/placeholder).
|
|
||||||
|
|
||||||
## Performance & SEO
|
|
||||||
- Expect smaller bundles and faster hydration from Svelte runtime.
|
|
||||||
- Maintain SSR for primary pages and content; keep linkable, crawlable party pages and listings.
|
|
||||||
- Optimize images/SVGs via Vite pipeline; keep existing public assets.
|
|
||||||
|
|
||||||
## Tradeoffs (Pros / Cons)
|
|
||||||
- Pros: performance, simpler state and data flows, clearer SSR/CSR boundaries, component‑scoped styling, fast DX.
|
|
||||||
- Cons: full component rewrite, parity tuning for UI primitives, community-maintained editor bindings, team ramp‑up, Storybook conversion.
|
|
||||||
|
|
||||||
## Alternatives Considered
|
|
||||||
- Stay on Next.js and complete the ongoing App Router + next‑intl consolidation (lowest risk and cost).
|
|
||||||
- Prototype SvelteKit for the party editor only (dual‑stack) and evaluate before committing.
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
- Keep proxy endpoints under `/api/**` or talk to Rails directly with CORS? (Proxy simplifies auth and error shaping.)
|
|
||||||
- UI library choice finalization: Bits‑UI vs shadcn‑svelte (or a minimal custom layer where needed).
|
|
||||||
- Icon strategy: `unplugin-icons` vs direct SVG imports.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
- Route and locale parity with auth/method guards and “as‑needed” default‑locale behavior.
|
|
||||||
- Party editor parity (create/update/remix/delete; grids; jobs/skills/accessories; guidebooks; toggles).
|
|
||||||
- Query‑linked filters and pagination on `/teams`, `/saved`, `/[username]`.
|
|
||||||
- Editor parity (mentions, link, lists, headings, youtube, highlight, placeholder) and equivalent styling.
|
|
||||||
- Primitives parity with keyboard a11y and visual consistency.
|
|
||||||
- i18n keys resolve from existing JSON bundles; theme persists SSR + client.
|
|
||||||
|
|
||||||
## Success Measures
|
|
||||||
- Time‑to‑interactive and bundle size improved vs. Next build.
|
|
||||||
- No regression in error rates for API interactions (remix/delete/save).
|
|
||||||
- Positive qualitative feedback on responsiveness in editor/grid interactions.
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
# Migration Advice: Next App Router + next-intl vs. SvelteKit
|
|
||||||
|
|
||||||
## Context
|
|
||||||
Small hobby app for a niche game community: create/share team compositions, with aspirations for collection tracking. Current stack is Next.js with a hybrid of Pages + App Router and `next-i18next`. The path forward considers migrating to Next 15 with `next-intl` or rewriting in SvelteKit.
|
|
||||||
|
|
||||||
## Opinionated Summary
|
|
||||||
- If you stay on Next: unify on App Router and `next-intl`. It’s the idiomatic, low-risk path on Next 15 and removes hybrid friction.
|
|
||||||
- If you prefer SvelteKit and want a better DX long term: a focused rewrite is reasonable for this scope. Expect higher upfront cost, but likely faster iteration afterwards.
|
|
||||||
|
|
||||||
## When Staying on Next
|
|
||||||
- App Router: move fully off Pages Router to reduce complexity.
|
|
||||||
- i18n: adopt `next-intl` idioms.
|
|
||||||
- Localized segment `app/[locale]/layout.tsx` with `unstable_setRequestLocale` and `getMessages()`.
|
|
||||||
- Compose middleware: run `next-intl` first, then auth; strip locale before auth checks.
|
|
||||||
- Replace `next/link` and `next/navigation` with `next-intl/navigation` to preserve locale.
|
|
||||||
- Replace `useTranslation` with `useTranslations` and `<Trans>` with `t.rich()`.
|
|
||||||
- Localize metadata via `getTranslations` in `generateMetadata`.
|
|
||||||
- Remove `i18n` from `next.config.js`; delete `next-i18next.config.js` when done.
|
|
||||||
- Effort: medium. Many small file touches, predictable changes, minimal risk.
|
|
||||||
|
|
||||||
## When Switching to SvelteKit
|
|
||||||
- Fit: Excellent for small, interactive apps; simpler routing and data loading; stores are ergonomic; great DX.
|
|
||||||
- i18n: Use `svelte-i18n` or `typesafe-i18n`; reuse existing JSON namespaces.
|
|
||||||
- Migration outline:
|
|
||||||
- Rebuild routes and core UI (builder, share pages) first.
|
|
||||||
- Port styles and assets; map API endpoints or use SvelteKit endpoints.
|
|
||||||
- Re-implement auth/cookies and URL structure for shareability.
|
|
||||||
- Add collection tracking after the core flow is stable.
|
|
||||||
- Effort: medium–high. A rewrite is real work, but constrained domain makes it feasible.
|
|
||||||
|
|
||||||
## Decision Guidance
|
|
||||||
- Choose Next if you want quickest path to stability with current code.
|
|
||||||
- Choose SvelteKit if you value developer experience and faster iteration beyond the migration.
|
|
||||||
|
|
||||||
## Practical Next Steps (Next.js Path)
|
|
||||||
1. Create `i18n.config.ts` with `locales` and `defaultLocale`.
|
|
||||||
2. Compose `middleware.ts` with `next-intl/middleware` first, then auth (strip locale).
|
|
||||||
3. Add `app/[locale]/layout.tsx` with `unstable_setRequestLocale` + `getMessages()` and `NextIntlClientProvider`.
|
|
||||||
4. Replace `next/link` and `next/navigation` with `next-intl/navigation`.
|
|
||||||
5. Swap `next-i18next` usages for `next-intl` (`useTranslations`, `t.rich`).
|
|
||||||
6. Localize `generateMetadata`; update server-only paths to use `getTranslations`.
|
|
||||||
7. Remove `i18n` from `next.config.js`; delete `next-i18next.config.js` and dependency.
|
|
||||||
8. Remove Pages Router pages once App Router feature parity is confirmed.
|
|
||||||
|
|
||||||
## Practical Next Steps (SvelteKit Path)
|
|
||||||
1. Prototype the builder and a share page in SvelteKit.
|
|
||||||
2. Port translations; wire `svelte-i18n` or `typesafe-i18n`.
|
|
||||||
3. Recreate auth/session and URL structures.
|
|
||||||
4. Decide incremental migration (subdomain) vs. cutover.
|
|
||||||
5. Migrate remaining features; add collection tracking last.
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
Stabilize quickly by finishing the Next App Router + `next-intl` migration you’ve already started. If Next still feels clunky after that, plan a small SvelteKit prototype; if the DX clicks, proceed with a focused rewrite.
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue