Fix Railway deployment and TypeScript errors
- Add next-intl routing configuration using defineRouting
- Update navigation and middleware to use new routing config
- Fix all TypeScript errors in components
- Add Node.js 20 configuration for Railway (.nvmrc and .mise.toml)
- Add patch script for next-intl ESM compatibility
- Fix nullable types and missing props across components
- Update package.json engines to specify Node.js 20.x
This fixes the deployment failure on Railway by:
1. Resolving all TypeScript compilation errors
2. Working around Node.js ESM module resolution issues with next-intl
3. Specifying Node.js 20 for consistent builds
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d880643fca
commit
e132d31b57
19 changed files with 94 additions and 32 deletions
2
.mise.toml
Normal file
2
.mise.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[tools]
|
||||
node = "20.12.0"
|
||||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
20
|
||||
|
|
@ -113,7 +113,7 @@ const Header = () => {
|
|||
})
|
||||
|
||||
// Push the root URL
|
||||
router.push('/new', undefined, { shallow: true })
|
||||
router.push('/new')
|
||||
}
|
||||
|
||||
// Methods: Rendering
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const Layout = ({ children }: PropsWithChildren<Props>) => {
|
|||
}
|
||||
|
||||
const updateToast = () => {
|
||||
const path = pathname.replaceAll('/', '')
|
||||
const path = pathname?.replaceAll('/', '') || ''
|
||||
|
||||
return (
|
||||
!['about', 'updates', 'roadmap'].includes(path) &&
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ const JobSkillItem = React.forwardRef<HTMLDivElement, Props>(
|
|||
message={
|
||||
<>
|
||||
{t.rich('modals.job_skills.messages.remove', {
|
||||
job_skill: skill?.name[locale],
|
||||
job_skill: skill?.name[locale] || '',
|
||||
strong: (chunks) => <strong>{chunks}</strong>
|
||||
})}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ const AXSelect = (props: Props) => {
|
|||
})
|
||||
} else if (!value || value <= 0) {
|
||||
newErrors.axValue1 = t('ax.errors.value_empty', {
|
||||
name: primaryAxSkill?.name[locale],
|
||||
name: primaryAxSkill?.name[locale] || '',
|
||||
})
|
||||
} else {
|
||||
newErrors.axValue1 = ''
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ const PartyDropdown = ({
|
|||
|
||||
// Method: Actions
|
||||
function copyToClipboard() {
|
||||
if (pathname.split('/')[1] === 'p') {
|
||||
if (pathname?.split('/')[1] === 'p') {
|
||||
navigator.clipboard.writeText(window.location.href)
|
||||
setCopyToastOpen(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ const PartyHeader = (props: Props) => {
|
|||
const turnCountToken = (
|
||||
<Token>
|
||||
{t('party.details.turns.with_count', {
|
||||
count: party.turnCount,
|
||||
count: party.turnCount || 0,
|
||||
})}
|
||||
</Token>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ const RaidCombobox = (props: Props) => {
|
|||
|
||||
// Scroll to an item in the list when it is selected
|
||||
const scrollToItem = useCallback(
|
||||
(node) => {
|
||||
(node: HTMLElement | null) => {
|
||||
if (!scrolled && open && currentRaid && listRef.current && node) {
|
||||
const { top: listTop } = listRef.current.getBoundingClientRect()
|
||||
const { top: itemTop } = node.getBoundingClientRect()
|
||||
|
|
@ -537,11 +537,9 @@ const RaidCombobox = (props: Props) => {
|
|||
className="raid flush"
|
||||
open={open}
|
||||
onOpenChange={toggleOpen}
|
||||
placeholder={
|
||||
props.showAllRaidsOption ? t('raids.all') : t('raids.placeholder')
|
||||
}
|
||||
trigger={{
|
||||
bound: true,
|
||||
placeholder: props.showAllRaidsOption ? t('raids.all') : t('raids.placeholder'),
|
||||
className: classNames({
|
||||
raid: true,
|
||||
highlighted: props.showAllRaidsOption,
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ const SummonUnit = ({
|
|||
message={
|
||||
<>
|
||||
{t.rich('modals.summon.messages.remove', {
|
||||
summon: gridSummon?.object.name[locale],
|
||||
summon: gridSummon?.object.name[locale] || '',
|
||||
strong: (chunks) => <strong>{chunks}</strong>
|
||||
})}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -112,11 +112,11 @@ const WeaponGrid = (props: Props) => {
|
|||
|
||||
if (response) {
|
||||
const code = response.status
|
||||
const data = response.data
|
||||
const data = response.data as any
|
||||
|
||||
if (
|
||||
code === 422 &&
|
||||
data.code === 'incompatible_weapon_for_position'
|
||||
data?.code === 'incompatible_weapon_for_position'
|
||||
) {
|
||||
setShowIncompatibleAlert(true)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ const WeaponUnit = ({
|
|||
message={
|
||||
<>
|
||||
{t.rich('modals.weapon.messages.remove', {
|
||||
weapon: gridWeapon?.object.name[locale],
|
||||
weapon: gridWeapon?.object.name[locale] || '',
|
||||
strong: (chunks) => <strong>{chunks}</strong>
|
||||
})}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ export default Mention.extend({
|
|||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
),
|
||||
this.options.renderLabel({
|
||||
this.options.renderLabel?.({
|
||||
options: this.options,
|
||||
node,
|
||||
}),
|
||||
suggestion: null,
|
||||
}) || '',
|
||||
]
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import {createNavigation} from 'next-intl/navigation'
|
||||
import {locales, defaultLocale} from '../i18n.config'
|
||||
import {routing} from './routing'
|
||||
|
||||
export const {Link, useRouter, usePathname} = createNavigation({
|
||||
locales,
|
||||
defaultLocale,
|
||||
localePrefix: 'as-needed'
|
||||
})
|
||||
export const {Link, useRouter, usePathname, redirect, getPathname} = createNavigation(routing)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
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()
|
||||
export default getRequestConfig(async ({requestLocale}) => {
|
||||
let locale = (await requestLocale) as Locale | null;
|
||||
if (!locale || !locales.includes(locale)) {
|
||||
locale = defaultLocale;
|
||||
if (!locale || !routing.locales.includes(locale)) {
|
||||
locale = routing.defaultLocale;
|
||||
}
|
||||
|
||||
// Load only i18n namespaces; exclude content data with dotted keys
|
||||
|
|
|
|||
8
i18n/routing.ts
Normal file
8
i18n/routing.ts
Normal 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
|
||||
})
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
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 type {NextRequest} from 'next/server'
|
||||
|
||||
const intl = createMiddleware({
|
||||
locales,
|
||||
defaultLocale,
|
||||
localePrefix: 'as-needed' // Show locale in URL when not default
|
||||
})
|
||||
const intl = createMiddleware(routing)
|
||||
|
||||
const PROTECTED_PATHS = ['/saved', '/profile'] as const
|
||||
const MIXED_AUTH_PATHS = ['/api/parties', '/p/'] as const
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"postinstall": "node scripts/patch-next-intl.js",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"node": "20.x",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
57
scripts/patch-next-intl.js
Normal file
57
scripts/patch-next-intl.js
Normal 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`);
|
||||
Loading…
Reference in a new issue