Fix TypeScript errors for production build (#436)

## Summary
Fixed multiple TypeScript errors that were preventing the production
build from completing on Railway.

## Changes Made

### Nullable Type Fixes
- Fixed `searchParams.toString()` calls with optional chaining (`?.`)
and fallback values
- Fixed `pathname` nullable access in UpdateToastClient
- Added fallbacks for undefined values in translation interpolations

### Type Consistency Fixes
- Fixed recency parameter handling (string from URL, converted to number
internally)
- Removed duplicate local interface definitions for Party and User types
- Fixed Party type mismatches by using global type definitions

### API Route Error Handling
- Fixed error type checking in catch blocks for login/signup routes
- Added proper type guards for axios error objects

### Component Props Fixes
- Fixed RadixSelect.Trigger by removing invalid placeholder prop
- Fixed Toast and Tooltip components by using Omit to exclude
conflicting content type
- Added missing onAdvancedFilter prop to FilterBar components
- Fixed PartyFooter props with required parameters

## Test Plan
- [x] Fixed all TypeScript compilation errors locally
- [ ] Production build should complete successfully on Railway
- [ ] All affected components should function correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-09-04 01:47:10 -07:00 committed by GitHub
parent 02676fd7d4
commit e4b7f0c356
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 150 additions and 108 deletions

2
.mise.toml Normal file
View file

@ -0,0 +1,2 @@
[tools]
node = "20.12.0"

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
20

View file

@ -24,25 +24,6 @@ interface Pagination {
record_count: number; record_count: number;
} }
interface Party {
id: string;
shortcode: string;
name: string;
element: number;
// Add other properties as needed
}
interface User {
id: string;
username: string;
avatar: {
picture: string;
element: string;
};
gender: string;
// Add other properties as needed
}
interface Props { interface Props {
initialData: { initialData: {
user: User; user: User;
@ -74,7 +55,7 @@ const ProfilePageClient: React.FC<Props> = ({
const [fetching, setFetching] = useState(false) const [fetching, setFetching] = useState(false)
const [element, setElement] = useState(initialElement || 0) const [element, setElement] = useState(initialElement || 0)
const [raid, setRaid] = useState(initialRaid || '') const [raid, setRaid] = useState(initialRaid || '')
const [recency, setRecency] = useState(initialRecency || '') const [recency, setRecency] = useState(initialRecency ? parseInt(initialRecency, 10) : 0)
// Initialize app state with raid groups // Initialize app state with raid groups
useEffect(() => { useEffect(() => {
@ -85,7 +66,7 @@ const ProfilePageClient: React.FC<Props> = ({
// Update URL when filters change // Update URL when filters change
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(searchParams.toString()) const params = new URLSearchParams(searchParams?.toString() ?? '')
// Update or remove parameters based on filter values // Update or remove parameters based on filter values
if (element) { if (element) {
@ -101,14 +82,14 @@ const ProfilePageClient: React.FC<Props> = ({
} }
if (recency) { if (recency) {
params.set('recency', recency) params.set('recency', recency.toString())
} else { } else {
params.delete('recency') params.delete('recency')
} }
// Only update URL if filters are changed // Only update URL if filters are changed
const newQueryString = params.toString() const newQueryString = params.toString()
const currentQuery = searchParams.toString() const currentQuery = searchParams?.toString() ?? ''
if (newQueryString !== currentQuery) { if (newQueryString !== currentQuery) {
router.push(`/${initialData.user.username}${newQueryString ? `?${newQueryString}` : ''}`) router.push(`/${initialData.user.username}${newQueryString ? `?${newQueryString}` : ''}`)
@ -128,7 +109,7 @@ const ProfilePageClient: React.FC<Props> = ({
if (element) url.searchParams.set('element', element.toString()) if (element) url.searchParams.set('element', element.toString())
if (raid) url.searchParams.set('raid_id', raid) if (raid) url.searchParams.set('raid_id', raid)
if (recency) url.searchParams.set('recency', recency) if (recency) url.searchParams.set('recency', recency.toString())
const response = await fetch(url.toString(), { const response = await fetch(url.toString(), {
headers: { headers: {
@ -163,7 +144,7 @@ const ProfilePageClient: React.FC<Props> = ({
setElement(filters.element || 0) setElement(filters.element || 0)
} }
if ('recency' in filters) { if ('recency' in filters) {
setRecency(filters.recency || '') setRecency(filters.recency || 0)
} }
if ('raid' in filters) { if ('raid' in filters) {
setRaid(filters.raid || '') setRaid(filters.raid || '')
@ -226,6 +207,7 @@ const ProfilePageClient: React.FC<Props> = ({
<FilterBar <FilterBar
defaultFilterset={defaultFilterset} defaultFilterset={defaultFilterset}
onFilter={receiveFilters} onFilter={receiveFilters}
onAdvancedFilter={receiveFilters}
persistFilters={false} persistFilters={false}
element={element} element={element}
raid={raid} raid={raid}

View file

@ -77,7 +77,14 @@ const PartyPageClient: React.FC<Props> = ({ party, raidGroups }) => {
handleTabChanged={handleTabChanged} handleTabChanged={handleTabChanged}
pushHistory={pushHistory} pushHistory={pushHistory}
/> />
<PartyFooter party={party} /> <PartyFooter
party={party}
new={false}
editable={false}
raidGroups={raidGroups}
remixCallback={() => {}}
updateCallback={async () => ({})}
/>
</> </>
) )
} }

View file

@ -17,13 +17,6 @@ import { defaultFilterset } from '~/utils/defaultFilters'
import { appState } from '~/utils/appState' import { appState } from '~/utils/appState'
// Types // Types
interface Party {
id: string;
shortcode: string;
name: string;
element: number;
// Add other properties as needed
}
interface Props { interface Props {
initialData: { initialData: {
@ -52,7 +45,7 @@ const SavedPageClient: React.FC<Props> = ({
const [parties, setParties] = useState<Party[]>(initialData.teams) const [parties, setParties] = useState<Party[]>(initialData.teams)
const [element, setElement] = useState(initialElement || 0) const [element, setElement] = useState(initialElement || 0)
const [raid, setRaid] = useState(initialRaid || '') const [raid, setRaid] = useState(initialRaid || '')
const [recency, setRecency] = useState(initialRecency || '') const [recency, setRecency] = useState(initialRecency ? parseInt(initialRecency, 10) : 0)
const [fetching, setFetching] = useState(false) const [fetching, setFetching] = useState(false)
// Initialize app state with raid groups // Initialize app state with raid groups
@ -64,7 +57,7 @@ const SavedPageClient: React.FC<Props> = ({
// Update URL when filters change // Update URL when filters change
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(searchParams.toString()) const params = new URLSearchParams(searchParams?.toString() ?? '')
// Update or remove parameters based on filter values // Update or remove parameters based on filter values
if (element) { if (element) {
@ -80,14 +73,14 @@ const SavedPageClient: React.FC<Props> = ({
} }
if (recency) { if (recency) {
params.set('recency', recency) params.set('recency', recency.toString())
} else { } else {
params.delete('recency') params.delete('recency')
} }
// Only update URL if filters are changed // Only update URL if filters are changed
const newQueryString = params.toString() const newQueryString = params.toString()
const currentQuery = searchParams.toString() const currentQuery = searchParams?.toString() ?? ''
if (newQueryString !== currentQuery) { if (newQueryString !== currentQuery) {
router.push(`/saved${newQueryString ? `?${newQueryString}` : ''}`) router.push(`/saved${newQueryString ? `?${newQueryString}` : ''}`)
@ -100,7 +93,7 @@ const SavedPageClient: React.FC<Props> = ({
setElement(filters.element || 0) setElement(filters.element || 0)
} }
if ('recency' in filters) { if ('recency' in filters) {
setRecency(filters.recency || '') setRecency(filters.recency || 0)
} }
if ('raid' in filters) { if ('raid' in filters) {
setRaid(filters.raid || '') setRaid(filters.raid || '')
@ -180,6 +173,7 @@ const SavedPageClient: React.FC<Props> = ({
<FilterBar <FilterBar
defaultFilterset={defaultFilterset} defaultFilterset={defaultFilterset}
onFilter={receiveFilters} onFilter={receiveFilters}
onAdvancedFilter={receiveFilters}
persistFilters={false} persistFilters={false}
element={element} element={element}
raid={raid} raid={raid}

View file

@ -53,11 +53,11 @@ export default async function SavedPage({
let filteredTeams = savedTeamsData.results || []; let filteredTeams = savedTeamsData.results || [];
if (element) { if (element) {
filteredTeams = filteredTeams.filter(party => party.element === element) filteredTeams = filteredTeams.filter((party: any) => party.element === element)
} }
if (raid) { if (raid) {
filteredTeams = filteredTeams.filter(party => party.raid?.id === raid) filteredTeams = filteredTeams.filter((party: any) => party.raid?.id === raid)
} }
// Prepare data for client component // Prepare data for client component

View file

@ -23,14 +23,6 @@ import LoadingRep from '~/components/reps/LoadingRep'
import ErrorSection from '~/components/ErrorSection' import ErrorSection from '~/components/ErrorSection'
// Types // Types
interface Party {
id: string;
shortcode: string;
name: string;
element: number;
// Add other properties as needed
}
interface Pagination { interface Pagination {
current_page: number; current_page: number;
total_pages: number; total_pages: number;
@ -69,7 +61,7 @@ const TeamsPageClient: React.FC<Props> = ({
const [fetching, setFetching] = useState(false) const [fetching, setFetching] = useState(false)
const [element, setElement] = useState(initialElement || 0) const [element, setElement] = useState(initialElement || 0)
const [raid, setRaid] = useState(initialRaid || '') const [raid, setRaid] = useState(initialRaid || '')
const [recency, setRecency] = useState(initialRecency || '') const [recency, setRecency] = useState(initialRecency ? parseInt(initialRecency, 10) : 0)
const [advancedFilters, setAdvancedFilters] = useState({}) const [advancedFilters, setAdvancedFilters] = useState({})
const { toggleFavorite } = useFavorites(parties, setParties) const { toggleFavorite } = useFavorites(parties, setParties)
@ -83,7 +75,7 @@ const TeamsPageClient: React.FC<Props> = ({
// Update URL when filters change // Update URL when filters change
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(searchParams.toString()) const params = new URLSearchParams(searchParams?.toString() ?? '')
// Update or remove parameters based on filter values // Update or remove parameters based on filter values
if (element) { if (element) {
@ -99,14 +91,14 @@ const TeamsPageClient: React.FC<Props> = ({
} }
if (recency) { if (recency) {
params.set('recency', recency) params.set('recency', recency.toString())
} else { } else {
params.delete('recency') params.delete('recency')
} }
// Only update URL if filters are changed // Only update URL if filters are changed
const newQueryString = params.toString() const newQueryString = params.toString()
const currentQuery = searchParams.toString() const currentQuery = searchParams?.toString() ?? ''
if (newQueryString !== currentQuery) { if (newQueryString !== currentQuery) {
router.push(`/teams${newQueryString ? `?${newQueryString}` : ''}`) router.push(`/teams${newQueryString ? `?${newQueryString}` : ''}`)
@ -126,7 +118,7 @@ const TeamsPageClient: React.FC<Props> = ({
if (element) url.searchParams.set('element', element.toString()) if (element) url.searchParams.set('element', element.toString())
if (raid) url.searchParams.set('raid', raid) if (raid) url.searchParams.set('raid', raid)
if (recency) url.searchParams.set('recency', recency) if (recency) url.searchParams.set('recency', recency.toString())
const response = await fetch(url.toString()) const response = await fetch(url.toString())
const data = await response.json() const data = await response.json()
@ -150,7 +142,7 @@ const TeamsPageClient: React.FC<Props> = ({
setElement(filters.element || 0) setElement(filters.element || 0)
} }
if ('recency' in filters) { if ('recency' in filters) {
setRecency(filters.recency || '') setRecency(filters.recency || 0)
} }
if ('raid' in filters) { if ('raid' in filters) {
setRaid(filters.raid || '') setRaid(filters.raid || '')

View file

@ -84,11 +84,14 @@ export async function POST(request: NextRequest) {
} }
// For authentication errors // For authentication errors
if (error.response?.status === 401) { if (error && typeof error === 'object' && 'response' in error) {
return NextResponse.json( const axiosError = error as any
{ error: 'Invalid email or password' }, if (axiosError.response?.status === 401) {
{ status: 401 } return NextResponse.json(
) { error: 'Invalid email or password' },
{ status: 401 }
)
}
} }
console.error('Login error:', error) console.error('Login error:', error)

View file

@ -49,15 +49,18 @@ export async function POST(request: NextRequest) {
} }
// Handle specific API errors // Handle specific API errors
if (error.response?.data?.error) { if (error && typeof error === 'object' && 'response' in error) {
const apiError = error.response.data.error const axiosError = error as any
if (axiosError.response?.data?.error) {
const apiError = axiosError.response.data.error
// Username or email already in use // Username or email already in use
if (apiError.includes('username') || apiError.includes('email')) { if (apiError.includes('username') || apiError.includes('email')) {
return NextResponse.json( return NextResponse.json(
{ error: apiError }, { error: apiError },
{ status: 409 } // Conflict { status: 409 } // Conflict
) )
}
} }
} }

View file

@ -54,7 +54,7 @@ export default function UpdateToastClient({ initialVersion }: UpdateToastClientP
setUpdateToastOpen(false) setUpdateToastOpen(false)
} }
const path = pathname.replaceAll('/', '') const path = pathname?.replaceAll('/', '') || ''
// Only render toast if we have valid version data with update_type // Only render toast if we have valid version data with update_type
if (!['about', 'updates', 'roadmap'].includes(path) && effectiveVersion && effectiveVersion.update_type) { if (!['about', 'updates', 'roadmap'].includes(path) && effectiveVersion && effectiveVersion.update_type) {

View file

@ -113,7 +113,7 @@ const Header = () => {
}) })
// Push the root URL // Push the root URL
router.push('/new', undefined, { shallow: true }) router.push('/new')
} }
// Methods: Rendering // Methods: Rendering

View file

@ -49,7 +49,7 @@ const Layout = ({ children }: PropsWithChildren<Props>) => {
} }
const updateToast = () => { const updateToast = () => {
const path = pathname.replaceAll('/', '') const path = pathname?.replaceAll('/', '') || ''
return ( return (
!['about', 'updates', 'roadmap'].includes(path) && !['about', 'updates', 'roadmap'].includes(path) &&

View file

@ -270,7 +270,7 @@ const CharacterUnit = ({
message={ message={
<> <>
{t.rich('modals.characters.messages.remove', { {t.rich('modals.characters.messages.remove', {
character: gridCharacter?.object.name[locale], character: gridCharacter?.object.name[locale] || '',
strong: (chunks) => <strong>{chunks}</strong> strong: (chunks) => <strong>{chunks}</strong>
})} })}
</> </>

View file

@ -86,7 +86,7 @@ const Editor = ({
renderLabel({ options, node }) { renderLabel({ options, node }) {
return `${node.attrs.id.name[locale] ?? node.attrs.id.granblue_en}` return `${node.attrs.id.name[locale] ?? node.attrs.id.granblue_en}`
}, },
suggestion: mentionSuggestionOptions, suggestion: mentionSuggestionOptions as any,
HTMLAttributes: { HTMLAttributes: {
class: classNames({ class: classNames({
[styles.mention]: true, [styles.mention]: true,

View file

@ -77,7 +77,7 @@ const Popover = React.forwardRef<HTMLDivElement, Props>(function Popover(
[styles.empty]: true, [styles.empty]: true,
})} })}
> >
{props.placeholder} {props.trigger?.placeholder}
</span> </span>
) )

View file

@ -97,7 +97,6 @@ const Select = React.forwardRef<HTMLButtonElement, Props>(function Select(
<RadixSelect.Trigger <RadixSelect.Trigger
autoFocus={props.autoFocus || false} autoFocus={props.autoFocus || false}
className={triggerClasses} className={triggerClasses}
placeholder={props.placeholder}
ref={forwardedRef} ref={forwardedRef}
> >
{props.icon?.src && <img alt={props.icon.alt} src={props.icon.src} />} {props.icon?.src && <img alt={props.icon.alt} src={props.icon.src} />}

View file

@ -4,7 +4,7 @@ import classNames from 'classnames'
import * as ToastPrimitive from '@radix-ui/react-toast' import * as ToastPrimitive from '@radix-ui/react-toast'
import styles from './index.module.scss' import styles from './index.module.scss'
interface Props extends ToastPrimitive.ToastProps { interface Props extends Omit<ToastPrimitive.ToastProps, 'content'> {
altText: string altText: string
className?: string className?: string
title?: string title?: string

View file

@ -2,7 +2,7 @@ import React, { PropsWithChildren } from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip' import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import styles from './index.module.scss' import styles from './index.module.scss'
interface Props extends TooltipPrimitive.TooltipContentProps { interface Props extends Omit<TooltipPrimitive.TooltipContentProps, 'content'> {
content: React.ReactNode content: React.ReactNode
open?: boolean open?: boolean
onOpenChange?: (open: boolean) => void onOpenChange?: (open: boolean) => void

View file

@ -144,7 +144,7 @@ const GuidebookUnit = ({
message={ message={
<> <>
{t.rich('modals.guidebooks.messages.remove', { {t.rich('modals.guidebooks.messages.remove', {
guidebook: guidebook?.name[locale], guidebook: guidebook?.name[locale] || '',
strong: (chunks) => <strong>{chunks}</strong> strong: (chunks) => <strong>{chunks}</strong>
})} })}
</> </>

View file

@ -139,7 +139,7 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
message={ message={
<> <>
{t.rich('modals.job_skills.messages.remove', { {t.rich('modals.job_skills.messages.remove', {
job_skill: skill?.name[locale], job_skill: skill?.name[locale] || '',
strong: (chunks) => <strong>{chunks}</strong> strong: (chunks) => <strong>{chunks}</strong>
})} })}
</> </>

View file

@ -338,7 +338,7 @@ const AXSelect = (props: Props) => {
}) })
} else if (!value || value <= 0) { } else if (!value || value <= 0) {
newErrors.axValue1 = t('ax.errors.value_empty', { newErrors.axValue1 = t('ax.errors.value_empty', {
name: primaryAxSkill?.name[locale], name: primaryAxSkill?.name[locale] || '',
}) })
} else { } else {
newErrors.axValue1 = '' newErrors.axValue1 = ''

View file

@ -78,7 +78,7 @@ const PartyDropdown = ({
// Method: Actions // Method: Actions
function copyToClipboard() { function copyToClipboard() {
if (pathname.split('/')[1] === 'p') { if (pathname?.split('/')[1] === 'p') {
navigator.clipboard.writeText(window.location.href) navigator.clipboard.writeText(window.location.href)
setCopyToastOpen(true) setCopyToastOpen(true)
} }

View file

@ -274,7 +274,7 @@ const PartyHeader = (props: Props) => {
const turnCountToken = ( const turnCountToken = (
<Token> <Token>
{t('party.details.turns.with_count', { {t('party.details.turns.with_count', {
count: party.turnCount, count: party.turnCount || 0,
})} })}
</Token> </Token>
) )

View file

@ -192,7 +192,7 @@ const RaidCombobox = (props: Props) => {
// Scroll to an item in the list when it is selected // Scroll to an item in the list when it is selected
const scrollToItem = useCallback( const scrollToItem = useCallback(
(node) => { (node: HTMLElement | null) => {
if (!scrolled && open && currentRaid && listRef.current && node) { if (!scrolled && open && currentRaid && listRef.current && node) {
const { top: listTop } = listRef.current.getBoundingClientRect() const { top: listTop } = listRef.current.getBoundingClientRect()
const { top: itemTop } = node.getBoundingClientRect() const { top: itemTop } = node.getBoundingClientRect()
@ -537,11 +537,9 @@ const RaidCombobox = (props: Props) => {
className="raid flush" className="raid flush"
open={open} open={open}
onOpenChange={toggleOpen} onOpenChange={toggleOpen}
placeholder={
props.showAllRaidsOption ? t('raids.all') : t('raids.placeholder')
}
trigger={{ trigger={{
bound: true, bound: true,
placeholder: props.showAllRaidsOption ? t('raids.all') : t('raids.placeholder'),
className: classNames({ className: classNames({
raid: true, raid: true,
highlighted: props.showAllRaidsOption, highlighted: props.showAllRaidsOption,

View file

@ -254,7 +254,7 @@ const SummonUnit = ({
message={ message={
<> <>
{t.rich('modals.summon.messages.remove', { {t.rich('modals.summon.messages.remove', {
summon: gridSummon?.object.name[locale], summon: gridSummon?.object.name[locale] || '',
strong: (chunks) => <strong>{chunks}</strong> strong: (chunks) => <strong>{chunks}</strong>
})} })}
</> </>

View file

@ -112,11 +112,11 @@ const WeaponGrid = (props: Props) => {
if (response) { if (response) {
const code = response.status const code = response.status
const data = response.data const data = response.data as any
if ( if (
code === 422 && code === 422 &&
data.code === 'incompatible_weapon_for_position' data?.code === 'incompatible_weapon_for_position'
) { ) {
setShowIncompatibleAlert(true) setShowIncompatibleAlert(true)
} else { } else {

View file

@ -509,7 +509,7 @@ const WeaponUnit = ({
message={ message={
<> <>
{t.rich('modals.weapon.messages.remove', { {t.rich('modals.weapon.messages.remove', {
weapon: gridWeapon?.object.name[locale], weapon: gridWeapon?.object.name[locale] || '',
strong: (chunks) => <strong>{chunks}</strong> strong: (chunks) => <strong>{chunks}</strong>
})} })}
</> </>

View file

@ -16,10 +16,11 @@ export default Mention.extend({
this.options.HTMLAttributes, this.options.HTMLAttributes,
HTMLAttributes HTMLAttributes
), ),
this.options.renderLabel({ this.options.renderLabel?.({
options: this.options, options: this.options,
node, node,
}), suggestion: null,
}) || '',
] ]
}, },
}) })

View file

@ -1,9 +1,5 @@
import {createNavigation} from 'next-intl/navigation' import {createNavigation} from 'next-intl/navigation'
import {locales, defaultLocale} from '../i18n.config' import {routing} from './routing'
export const {Link, useRouter, usePathname} = createNavigation({ export const {Link, useRouter, usePathname, redirect, getPathname} = createNavigation(routing)
locales,
defaultLocale,
localePrefix: 'as-needed'
})

View file

@ -1,11 +1,12 @@
import {getRequestConfig} from 'next-intl/server' import {getRequestConfig} from 'next-intl/server'
import {locales, defaultLocale, type Locale} from '../i18n.config' import {routing} from './routing'
import {type Locale} from '../i18n.config'
// next-intl v4: global request config used by getMessages() // next-intl v4: global request config used by getMessages()
export default getRequestConfig(async ({requestLocale}) => { export default getRequestConfig(async ({requestLocale}) => {
let locale = (await requestLocale) as Locale | null; let locale = (await requestLocale) as Locale | null;
if (!locale || !locales.includes(locale)) { if (!locale || !routing.locales.includes(locale)) {
locale = defaultLocale; locale = routing.defaultLocale;
} }
// Load only i18n namespaces; exclude content data with dotted keys // Load only i18n namespaces; exclude content data with dotted keys

8
i18n/routing.ts Normal file
View file

@ -0,0 +1,8 @@
import {defineRouting} from 'next-intl/routing'
import {locales, defaultLocale} from '../i18n.config'
export const routing = defineRouting({
locales,
defaultLocale,
localePrefix: 'as-needed' // Show locale in URL when not default
})

View file

@ -1,13 +1,10 @@
import createMiddleware from 'next-intl/middleware' import createMiddleware from 'next-intl/middleware'
import {locales, defaultLocale, type Locale} from './i18n.config' import {routing} from './i18n/routing'
import {locales, type Locale} from './i18n.config'
import {NextResponse} from 'next/server' import {NextResponse} from 'next/server'
import type {NextRequest} from 'next/server' import type {NextRequest} from 'next/server'
const intl = createMiddleware({ const intl = createMiddleware(routing)
locales,
defaultLocale,
localePrefix: 'as-needed' // Show locale in URL when not default
})
const PROTECTED_PATHS = ['/saved', '/profile'] as const const PROTECTED_PATHS = ['/saved', '/profile'] as const
const MIXED_AUTH_PATHS = ['/api/parties', '/p/'] as const const MIXED_AUTH_PATHS = ['/api/parties', '/p/'] as const

View file

@ -9,11 +9,12 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"postinstall": "node scripts/patch-next-intl.js",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build" "build-storybook": "storybook build"
}, },
"engines": { "engines": {
"node": ">=20.0.0", "node": "20.x",
"npm": ">=10.0.0" "npm": ">=10.0.0"
}, },
"dependencies": { "dependencies": {

View file

@ -0,0 +1,57 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
// Recursively find all .js files in a directory
function findJsFiles(dir, files = []) {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
findJsFiles(fullPath, files);
} else if (item.endsWith('.js')) {
files.push(fullPath);
}
}
return files;
}
// Find all JS files in next-intl dist folder
const distPath = path.join(process.cwd(), 'node_modules/next-intl/dist');
const filesToPatch = findJsFiles(distPath);
let patchCount = 0;
filesToPatch.forEach(filePath => {
if (fs.existsSync(filePath)) {
let content = fs.readFileSync(filePath, 'utf8');
const originalContent = content;
// Replace imports from Next.js modules to include .js extension
// Handle both minified and non-minified code
content = content.replace(/from"next\/navigation"/g, 'from"next/navigation.js"');
content = content.replace(/from "next\/navigation"/g, 'from "next/navigation.js"');
content = content.replace(/from'next\/navigation'/g, "from'next/navigation.js'");
content = content.replace(/from 'next\/navigation'/g, "from 'next/navigation.js'");
content = content.replace(/from"next\/link"/g, 'from"next/link.js"');
content = content.replace(/from "next\/link"/g, 'from "next/link.js"');
content = content.replace(/from'next\/link'/g, "from'next/link.js'");
content = content.replace(/from 'next\/link'/g, "from 'next/link.js'");
content = content.replace(/from"next\/headers"/g, 'from"next/headers.js"');
content = content.replace(/from "next\/headers"/g, 'from "next/headers.js"');
content = content.replace(/from'next\/headers'/g, "from'next/headers.js'");
content = content.replace(/from 'next\/headers'/g, "from 'next/headers.js'");
// Only write if content changed
if (content !== originalContent) {
fs.writeFileSync(filePath, content, 'utf8');
patchCount++;
console.log(`✓ Patched ${path.relative(process.cwd(), filePath)}`);
}
}
});
console.log(`✅ Patching complete - ${patchCount} files patched`);