Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54ddc11b44 | |||
| 5da5fd2c7e |
@@ -40,3 +40,6 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# ai agent settings
|
||||||
|
.claude
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package-lock.json
|
||||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnPaste": true,
|
"editor.formatOnPaste": true,
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ backend service, polls until parsing finishes, and displays the result in a
|
|||||||
grid.
|
grid.
|
||||||
|
|
||||||
- **Next.js 16** (App Router, Turbopack) with **React 19**
|
- **Next.js 16** (App Router, Turbopack) with **React 19**
|
||||||
- **TypeScript 6**, **ESLint 9** (flat config), **Prettier 3**
|
- **TypeScript 6 and 7** side by side (see "Things that will bite you"),
|
||||||
|
**ESLint 9** (flat config), **Prettier 3**
|
||||||
- **ag-grid** for the data grid, **Bootstrap 5** for styling
|
- **ag-grid** for the data grid, **Bootstrap 5** for styling
|
||||||
|
|
||||||
Source files live in `src/`:
|
Source files live in `src/`:
|
||||||
@@ -21,6 +22,50 @@ Source files live in `src/`:
|
|||||||
- `config/` for app config
|
- `config/` for app config
|
||||||
- `types/` for .d.ts type declaration files
|
- `types/` for .d.ts type declaration files
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The parsing backend is a **separate service in its own repository**, not part
|
||||||
|
of this project. Start it manually before running the frontend; it is expected
|
||||||
|
at `http://127.0.0.1:5226`.
|
||||||
|
|
||||||
|
The browser never calls that backend directly. Requests go through a Next route
|
||||||
|
handler that forwards them:
|
||||||
|
|
||||||
|
```
|
||||||
|
SaveParserBackendService -> /api/proxy/saveParserBackend/* -> backend
|
||||||
|
(client, fetch) (server, route handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [src/services/SaveParserBackendService.ts](src/services/SaveParserBackendService.ts)
|
||||||
|
is the only place that talks to the backend. It exposes `uploadSave`,
|
||||||
|
`getSaveStatus` and `getParsedSave` — upload, poll until parsing finishes,
|
||||||
|
then fetch the result.
|
||||||
|
- [src/app/api/proxy/saveParserBackend/[[...path]]/route.ts](src/app/api/proxy/saveParserBackend/[[...path]]/route.ts)
|
||||||
|
forwards every method to the configured backend URL. It exists only to keep
|
||||||
|
requests same-origin, and is meant to be deleted once the backend handles
|
||||||
|
CORS itself.
|
||||||
|
- Route paths live in [src/app/api/proxy/paths.ts](src/app/api/proxy/paths.ts)
|
||||||
|
rather than as string literals, so both sides of the proxy stay in sync.
|
||||||
|
|
||||||
|
Configuration comes from [src/config/AppConfig.ts](src/config/AppConfig.ts)
|
||||||
|
(defaults) merged with `runtimeConfig.json` at the repo root, which holds the
|
||||||
|
backend URL. That file is gitignored, so a fresh clone starts without one and
|
||||||
|
falls back to the defaults.
|
||||||
|
|
||||||
|
### Current status
|
||||||
|
|
||||||
|
Much of the app is still scaffold. Do not assume a feature works because the UI
|
||||||
|
for it exists:
|
||||||
|
|
||||||
|
- **The grid is not wired up.** [src/app/browse/page.tsx](src/app/browse/page.tsx)
|
||||||
|
renders two hardcoded rows and never calls the backend. Nothing connects the
|
||||||
|
upload flow to it yet.
|
||||||
|
- **Auth does not work.** `AuthService` posts to `/api/auth/login` and
|
||||||
|
`/api/auth/register`, which do not exist as routes. The service is slated for
|
||||||
|
removal in favour of Discord OAuth.
|
||||||
|
- **`hashPassword` is a no-op** that returns the password unchanged. Treat any
|
||||||
|
current auth code as a placeholder, not as something to build on.
|
||||||
|
|
||||||
## Terminal
|
## Terminal
|
||||||
|
|
||||||
**On Windows, prefer `bash.exe`** — but only a real MSYS2, Cygwin, or
|
**On Windows, prefer `bash.exe`** — but only a real MSYS2, Cygwin, or
|
||||||
@@ -43,7 +88,7 @@ problem rather than a missing install: MSYS2 defaults `MSYS2_PATH_TYPE` to
|
|||||||
npm run dev # dev server
|
npm run dev # dev server
|
||||||
npm run build # production build
|
npm run build # production build
|
||||||
npm run lint # eslint
|
npm run lint # eslint
|
||||||
npm run format # prettier, rewrites files in src/
|
npm run format # prettier, rewrites everything except .gitignore/.prettierignore
|
||||||
npx tsc --noEmit # typecheck
|
npx tsc --noEmit # typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -108,9 +153,58 @@ Avoid `any`; `no-explicit-any` is enforced. Prefer `unknown` plus narrowing.
|
|||||||
|
|
||||||
## Things that will bite you
|
## Things that will bite you
|
||||||
|
|
||||||
**TypeScript is pinned to 6 on purpose.** Do not upgrade it to 7. The Next.js
|
**Both TypeScript 6 and 7 are installed on purpose, under swapped names.**
|
||||||
ESLint config depends on typescript-eslint, which refuses to run under
|
TypeScript 7 is the native Go compiler and type-checks much faster, but
|
||||||
TypeScript 7, and that would break `npm run lint` entirely.
|
typescript-eslint refuses to run under it, which would break `npm run lint`
|
||||||
|
entirely. So each tool gets the version it can use:
|
||||||
|
|
||||||
|
| package.json entry | resolves to | used by |
|
||||||
|
| -------------------- | ------------------------------------------------ | ----------------- |
|
||||||
|
| `typescript` | `@typescript/typescript6` (TS 6 API, bin `tsc6`) | typescript-eslint |
|
||||||
|
| `@typescript/native` | `typescript@7` (Go compiler) | `next build` |
|
||||||
|
|
||||||
|
The alias is deliberately reversed: typescript-eslint looks the compiler up by
|
||||||
|
the package _name_ `typescript`, so that name must stay on TS 6. Do not "fix"
|
||||||
|
this by renaming the entries.
|
||||||
|
|
||||||
|
`next.config.ts` sets `experimental.useTypeScriptCli`, which makes Next spawn a
|
||||||
|
`tsc` binary instead of using the compiler API. Next resolves that binary from
|
||||||
|
the package named `typescript` too, so it would also get TS 6. The `prebuild`
|
||||||
|
script [scripts/replace-tsc6-with-tsc7.mjs](scripts/replace-tsc6-with-tsc7.mjs)
|
||||||
|
works around this by rewriting `node_modules/typescript/bin/tsc6` into a
|
||||||
|
one-line stub that imports TS 7's `lib/tsc.js`.
|
||||||
|
|
||||||
|
Consequences to keep in mind:
|
||||||
|
|
||||||
|
- The patch lives in `node_modules`, so `npm install` undoes it. `prebuild`
|
||||||
|
reapplies it, so `npm run build` is always correct; nothing else needs doing.
|
||||||
|
- `npx tsc --noEmit` is unaffected either way. `node_modules/.bin/tsc` links
|
||||||
|
straight to `@typescript/native`, so the standalone typecheck is always TS 7.
|
||||||
|
- Do not drop the `prebuild` script while leaving `useTypeScriptCli` enabled.
|
||||||
|
Nothing breaks visibly — the build just silently goes back to type-checking
|
||||||
|
with TS 6.
|
||||||
|
|
||||||
|
**All of this is temporary.** The only reason TS 6 is still here is that
|
||||||
|
TypeScript 7.0 ships no stable programmatic JS API, so typescript-eslint cannot
|
||||||
|
run on it. That API arrives in **TypeScript 7.1, planned for 2026-11-24**. Once
|
||||||
|
it is out and typescript-eslint supports it, tear the whole arrangement down:
|
||||||
|
drop `@typescript/native` and the `@typescript/typescript6` alias for a plain
|
||||||
|
`"typescript": "^7.1"`, delete `scripts/replace-tsc6-with-tsc7.mjs` and the
|
||||||
|
`prebuild` script, and re-check whether `useTypeScriptCli` is still worth
|
||||||
|
keeping.
|
||||||
|
|
||||||
|
**Env vars in npm scripts must go through `cross-env`.** The `dev` and `build`
|
||||||
|
scripts set `NEXT_TELEMETRY_DISABLED=1`, which stops Next from collecting
|
||||||
|
anonymous usage data and saves the ~0.5s the telemetry flush adds to every
|
||||||
|
build. The `FOO=1 cmd` prefix is POSIX shell syntax, and on Windows npm runs
|
||||||
|
scripts through `cmd`, where it is a syntax error — so the prefix is written as
|
||||||
|
`cross-env NEXT_TELEMETRY_DISABLED=1 next ...` instead. Keep `cross-env` on any
|
||||||
|
new script that needs an env var.
|
||||||
|
|
||||||
|
Setting the variable from `prebuild` would not work: `prebuild` and `build` are
|
||||||
|
sibling processes, so an env var exported by one is not visible to the other.
|
||||||
|
Each script that needs it sets it itself. `prebuild` has no `cross-env` prefix
|
||||||
|
because it only rewrites the tsc bin and never talks to telemetry.
|
||||||
|
|
||||||
**`instrumentation.ts` is compiled for the edge runtime too and it can't be disabled.**
|
**`instrumentation.ts` is compiled for the edge runtime too and it can't be disabled.**
|
||||||
Anything using `fs`, `path`, or other Node APIs must be imported behind the
|
Anything using `fs`, `path`, or other Node APIs must be imported behind the
|
||||||
|
|||||||
+6
-4
@@ -1,7 +1,9 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from 'next'
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
experimental: {
|
||||||
};
|
useTypeScriptCli: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig
|
||||||
|
|||||||
Generated
+873
-469
File diff suppressed because it is too large
Load Diff
+8
-4
@@ -3,13 +3,15 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "cross-env NEXT_TELEMETRY_DISABLED=1 next dev --turbopack",
|
||||||
"build": "next build",
|
"prebuild": "node scripts/replace-tsc6-with-tsc7.mjs",
|
||||||
|
"build": "cross-env NEXT_TELEMETRY_DISABLED=1 next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"format": "npx prettier src --write"
|
"format": "npx prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ag-grid-community": "^36",
|
||||||
"ag-grid-react": "^36",
|
"ag-grid-react": "^36",
|
||||||
"bootstrap": "^5",
|
"bootstrap": "^5",
|
||||||
"next": "^16",
|
"next": "^16",
|
||||||
@@ -20,10 +22,12 @@
|
|||||||
"@types/node": "^26",
|
"@types/node": "^26",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@typescript/native": "npm:typescript@^7.0",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "^16",
|
"eslint-config-next": "^16",
|
||||||
"prettier": "^3",
|
"prettier": "^3",
|
||||||
"typescript": "^6"
|
"typescript": "npm:@typescript/typescript6@^6.0"
|
||||||
},
|
},
|
||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
"unrs-resolver": false
|
"unrs-resolver": false
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
Replaces tsc6 script which is called by next.js to run tsc 7,
|
||||||
|
which is a native executable written in go and works faster.
|
||||||
|
The problem is typescript 7 is supported with nextjs,
|
||||||
|
but is not supported by eslint which we use for static code analysis.
|
||||||
|
This script solves the problem.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import { writeFileSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
|
||||||
|
const tsc6 = path.join(
|
||||||
|
path.dirname(require.resolve('typescript/package.json')),
|
||||||
|
'bin',
|
||||||
|
'tsc6'
|
||||||
|
)
|
||||||
|
const nativeTsc = path.join(
|
||||||
|
path.dirname(require.resolve('@typescript/native/package.json')),
|
||||||
|
'lib',
|
||||||
|
'tsc.js'
|
||||||
|
)
|
||||||
|
const specifier = path
|
||||||
|
.relative(path.dirname(tsc6), nativeTsc)
|
||||||
|
.split(path.sep)
|
||||||
|
.join('/')
|
||||||
|
|
||||||
|
writeFileSync(tsc6, `#!/usr/bin/env node\nimport('${specifier}')\n`)
|
||||||
|
|
||||||
|
console.log(`patched ${tsc6} -> ${nativeTsc}`)
|
||||||
@@ -4,6 +4,10 @@ import { redirectRequest, respondToOPTIONS } from '@/app/api/proxy/functions'
|
|||||||
import { PROXY_PATHS } from '@/app/api/proxy/paths'
|
import { PROXY_PATHS } from '@/app/api/proxy/paths'
|
||||||
import { appConfig } from '@/lib/configLoader'
|
import { appConfig } from '@/lib/configLoader'
|
||||||
|
|
||||||
|
//TODO: remove this proxy once the backend handles CORS itself. It only exists
|
||||||
|
// so the browser can make same-origin requests; with CORS on the backend the
|
||||||
|
// client can call it directly and this extra hop disappears.
|
||||||
|
|
||||||
// Forwards the incoming request to the save parser backend.
|
// Forwards the incoming request to the save parser backend.
|
||||||
async function redirect(req: NextRequest): Promise<NextResponse> {
|
async function redirect(req: NextRequest): Promise<NextResponse> {
|
||||||
return await redirectRequest(
|
return await redirectRequest(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const DARK_SCHEME_QUERY = '(prefers-color-scheme: dark)'
|
|||||||
const BASE_THEME = themeQuartz.withParams({
|
const BASE_THEME = themeQuartz.withParams({
|
||||||
columnBorder: true,
|
columnBorder: true,
|
||||||
headerColumnBorder: true,
|
headerColumnBorder: true,
|
||||||
|
headerCellBackgroundTransitionDuration: 0,
|
||||||
})
|
})
|
||||||
// Built once, so the grid keeps seeing the same theme object between renders.
|
// Built once, so the grid keeps seeing the same theme object between renders.
|
||||||
const LIGHT_THEME = BASE_THEME
|
const LIGHT_THEME = BASE_THEME
|
||||||
@@ -53,6 +54,9 @@ export default function BrowseSaveDataPage() {
|
|||||||
<AgGridReact
|
<AgGridReact
|
||||||
theme={agTheme}
|
theme={agTheme}
|
||||||
modules={[AllCommunityModule]}
|
modules={[AllCommunityModule]}
|
||||||
|
// ddisable all animations
|
||||||
|
animateRows={false}
|
||||||
|
suppressColumnMoveAnimation={true}
|
||||||
rowData={[
|
rowData={[
|
||||||
{ id: 1, col1: 'Hello', col2: 'AG Grid' },
|
{ id: 1, col1: 'Hello', col2: 'AG Grid' },
|
||||||
{ id: 2, col1: 'Dark Mode', col2: 'Support' },
|
{ id: 2, col1: 'Dark Mode', col2: 'Support' },
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
font-family: var(--font-ibm-plex-sans), monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|||||||
+2
-4
@@ -1,8 +1,8 @@
|
|||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
|
|
||||||
import Navbar from '@/components/Navbar'
|
import Navbar from '@/components/Navbar'
|
||||||
|
import { font_IbmPlexSans } from '@/lib/myFonts'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Home',
|
title: 'Home',
|
||||||
@@ -12,14 +12,12 @@ export const metadata: Metadata = {
|
|||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
// Register the ag-grid community features
|
|
||||||
ModuleRegistry.registerModules([AllCommunityModule])
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>paradox-save-parser-frontend</title>
|
<title>paradox-save-parser-frontend</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body className={font_IbmPlexSans.className}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { font_Hack } from '@/lib/myFonts'
|
|
||||||
|
|
||||||
// Top navigation bar with a home link and login/register buttons.
|
// Top navigation bar with a home link and login/register buttons.
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav className={`navbar navbar-expand-lg navbar-dark bg-dark px-3`}>
|
||||||
className={`navbar navbar-expand-lg navbar-dark bg-dark px-3 ${font_Hack.className}`}
|
|
||||||
>
|
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
{/* Left: Home Link */}
|
{/* Left: Home Link */}
|
||||||
<Link href="/" className="navbar-brand">
|
<Link href="/" className="navbar-brand">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import fs from 'fs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { AppConfig, defaultConfig } from '@/config/AppConfig'
|
import { AppConfig, defaultConfig } from '@/config/AppConfig'
|
||||||
|
|
||||||
|
// TODO: get rid of this file and read about configs for next.js. Maybe use .env file where variables NEXT_PUBLIC_* are avaliable from client.
|
||||||
const configFilePath = path.join(process.cwd(), 'runtimeConfig.json')
|
const configFilePath = path.join(process.cwd(), 'runtimeConfig.json')
|
||||||
|
|
||||||
// Reads runtimeConfig.json (merging it over the defaults) and creates new config file if missing
|
// Reads runtimeConfig.json (merging it over the defaults) and creates new config file if missing
|
||||||
@@ -18,7 +19,7 @@ function loadConfig(): AppConfig {
|
|||||||
console.log(`creating default config file '${configFilePath}'`)
|
console.log(`creating default config file '${configFilePath}'`)
|
||||||
fs.mkdirSync(path.dirname(configFilePath), { recursive: true })
|
fs.mkdirSync(path.dirname(configFilePath), { recursive: true })
|
||||||
}
|
}
|
||||||
fs.writeFileSync(configFilePath, JSON.stringify(defaultConfig, null, 4))
|
fs.writeFileSync(configFilePath, JSON.stringify(config, null, 4))
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import localFont from 'next/font/local'
|
|
||||||
import { IBM_Plex_Sans } from 'next/font/google'
|
import { IBM_Plex_Sans } from 'next/font/google'
|
||||||
|
|
||||||
// Loads the Google-hosted IBM Plex Sans font.
|
// Loads the Google-hosted IBM Plex Sans font.
|
||||||
@@ -9,31 +8,3 @@ export const font_IbmPlexSans = IBM_Plex_Sans({
|
|||||||
display: 'swap',
|
display: 'swap',
|
||||||
variable: '--font-ibm-plex-sans',
|
variable: '--font-ibm-plex-sans',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Loads the local Hack font used by the navbar.
|
|
||||||
export const font_Hack = localFont({
|
|
||||||
src: [
|
|
||||||
{
|
|
||||||
path: '../../public/fonts/hack-v3.003/Hack-Regular.ttf',
|
|
||||||
weight: '400',
|
|
||||||
style: 'normal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '../../public/fonts/hack-v3.003/Hack-Bold.ttf',
|
|
||||||
weight: '700',
|
|
||||||
style: 'normal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '../../public/fonts/hack-v3.003/Hack-Italic.ttf',
|
|
||||||
weight: '400',
|
|
||||||
style: 'italic',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '../../public/fonts/hack-v3.003/Hack-BoldItalic.ttf',
|
|
||||||
weight: '700',
|
|
||||||
style: 'italic',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
display: 'swap',
|
|
||||||
variable: '--font-hack',
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
//TODO: replace this whole service with Discord OAuth. The target audience
|
||||||
|
// already uses Discord, so email/password login is not worth building. The
|
||||||
|
// /api/auth/login and /api/auth/register routes below are never implemented —
|
||||||
|
// delete them along with this service.
|
||||||
//TODO: controllable rate limits
|
//TODO: controllable rate limits
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
|
|||||||
Reference in New Issue
Block a user