diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 00000000..6a50b632 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +node = "20.12.0" diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..2edeafb0 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 \ No newline at end of file diff --git a/components/Header/index.tsx b/components/Header/index.tsx index d462f0c8..11f10250 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -113,7 +113,7 @@ const Header = () => { }) // Push the root URL - router.push('/new', undefined, { shallow: true }) + router.push('/new') } // Methods: Rendering diff --git a/components/Layout/index.tsx b/components/Layout/index.tsx index 4b811dcc..d7a59d46 100644 --- a/components/Layout/index.tsx +++ b/components/Layout/index.tsx @@ -49,7 +49,7 @@ const Layout = ({ children }: PropsWithChildren) => { } const updateToast = () => { - const path = pathname.replaceAll('/', '') + const path = pathname?.replaceAll('/', '') || '' return ( !['about', 'updates', 'roadmap'].includes(path) && diff --git a/components/job/JobSkillItem/index.tsx b/components/job/JobSkillItem/index.tsx index 2da276a8..108ce5e5 100644 --- a/components/job/JobSkillItem/index.tsx +++ b/components/job/JobSkillItem/index.tsx @@ -139,7 +139,7 @@ const JobSkillItem = React.forwardRef( message={ <> {t.rich('modals.job_skills.messages.remove', { - job_skill: skill?.name[locale], + job_skill: skill?.name[locale] || '', strong: (chunks) => {chunks} })} diff --git a/components/mastery/AxSelect/index.tsx b/components/mastery/AxSelect/index.tsx index 703cb153..f1090bf5 100644 --- a/components/mastery/AxSelect/index.tsx +++ b/components/mastery/AxSelect/index.tsx @@ -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 = '' diff --git a/components/party/PartyDropdown/index.tsx b/components/party/PartyDropdown/index.tsx index 6b19479a..5b2bfd04 100644 --- a/components/party/PartyDropdown/index.tsx +++ b/components/party/PartyDropdown/index.tsx @@ -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) } diff --git a/components/party/PartyHeader/index.tsx b/components/party/PartyHeader/index.tsx index 10fe7b66..60332a23 100644 --- a/components/party/PartyHeader/index.tsx +++ b/components/party/PartyHeader/index.tsx @@ -274,7 +274,7 @@ const PartyHeader = (props: Props) => { const turnCountToken = ( {t('party.details.turns.with_count', { - count: party.turnCount, + count: party.turnCount || 0, })} ) diff --git a/components/raids/RaidCombobox/index.tsx b/components/raids/RaidCombobox/index.tsx index 2c0064c1..5c0e4135 100644 --- a/components/raids/RaidCombobox/index.tsx +++ b/components/raids/RaidCombobox/index.tsx @@ -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, diff --git a/components/summon/SummonUnit/index.tsx b/components/summon/SummonUnit/index.tsx index d402d3dd..d50f97f3 100644 --- a/components/summon/SummonUnit/index.tsx +++ b/components/summon/SummonUnit/index.tsx @@ -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) => {chunks} })} diff --git a/components/weapon/WeaponGrid/index.tsx b/components/weapon/WeaponGrid/index.tsx index 77a0ce9d..c4ce790a 100644 --- a/components/weapon/WeaponGrid/index.tsx +++ b/components/weapon/WeaponGrid/index.tsx @@ -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 { diff --git a/components/weapon/WeaponUnit/index.tsx b/components/weapon/WeaponUnit/index.tsx index a916c683..ddd15e18 100644 --- a/components/weapon/WeaponUnit/index.tsx +++ b/components/weapon/WeaponUnit/index.tsx @@ -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) => {chunks} })} diff --git a/extensions/CustomMention/index.tsx b/extensions/CustomMention/index.tsx index a82e4305..d1db2bff 100644 --- a/extensions/CustomMention/index.tsx +++ b/extensions/CustomMention/index.tsx @@ -16,10 +16,11 @@ export default Mention.extend({ this.options.HTMLAttributes, HTMLAttributes ), - this.options.renderLabel({ + this.options.renderLabel?.({ options: this.options, node, - }), + suggestion: null, + }) || '', ] }, }) diff --git a/i18n/navigation.ts b/i18n/navigation.ts index 801d0856..b94fbbd9 100644 --- a/i18n/navigation.ts +++ b/i18n/navigation.ts @@ -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) diff --git a/i18n/request.ts b/i18n/request.ts index 61eaffd9..59f2f9c6 100644 --- a/i18n/request.ts +++ b/i18n/request.ts @@ -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 diff --git a/i18n/routing.ts b/i18n/routing.ts new file mode 100644 index 00000000..9d74ff0a --- /dev/null +++ b/i18n/routing.ts @@ -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 +}) \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index 1cbdf7a6..57ac2912 100644 --- a/middleware.ts +++ b/middleware.ts @@ -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 diff --git a/package.json b/package.json index b9f4c315..50a63770 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/scripts/patch-next-intl.js b/scripts/patch-next-intl.js new file mode 100644 index 00000000..62c278ed --- /dev/null +++ b/scripts/patch-next-intl.js @@ -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`); \ No newline at end of file