From 6461039bd757b0cdfb1501c98f776ad9de999762 Mon Sep 17 00:00:00 2001 From: Drew Malzahn Date: Wed, 17 Jun 2026 00:51:53 +0000 Subject: [PATCH] refactor: Migrate the app to TypeScript Convert the React app from JS/JSX to TS/TSX and add type-checking: - Rename sheet-main, book, BookIndex, CharacterSheet to .ts(x) and add types (Fields, Bond, ClassEntry, Spell, CheckMap; loose SavedData for abbreviated save/share payloads) - Add globals.d.ts for CSS imports and the __BOOK_DATA__ global - tsconfig.json (strict, noEmit) and a 'typecheck' npm script - webpack: handle ts/tsx via @babel/preset-typescript - Enforce types with a tracked pre-commit hook (core.hooksPath), wired up automatically via the 'prepare' script - Update stale Justfile format target for the src/ layout Co-Authored-By: Claude Opus 4.8 --- .githooks/pre-commit | 6 + Justfile | 7 +- package-lock.json | 209 ++++++++++++++++++ package.json | 8 +- src/{BookIndex.jsx => BookIndex.tsx} | 17 +- ...{CharacterSheet.jsx => CharacterSheet.tsx} | 138 +++++++++--- src/{book.js => book.tsx} | 4 +- src/globals.d.ts | 16 ++ src/{sheet-main.jsx => sheet-main.tsx} | 2 +- tsconfig.json | 18 ++ webpack.config.js | 12 +- 11 files changed, 384 insertions(+), 53 deletions(-) create mode 100755 .githooks/pre-commit rename src/{BookIndex.jsx => BookIndex.tsx} (91%) rename src/{CharacterSheet.jsx => CharacterSheet.tsx} (93%) rename src/{book.js => book.tsx} (68%) create mode 100644 src/globals.d.ts rename src/{sheet-main.jsx => sheet-main.tsx} (62%) create mode 100644 tsconfig.json diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..ad15716 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,6 @@ +#!/bin/sh +# Run TypeScript type-checking before each commit. +# Babel strips types without checking them, so this is what actually +# enforces type correctness. Bypass with `git commit --no-verify`. + +npm run typecheck diff --git a/Justfile b/Justfile index 1b1e139..130f416 100644 --- a/Justfile +++ b/Justfile @@ -20,8 +20,9 @@ deploy: build format: npx prettier --write books/ - npx prettier --write fabula-ultima-sheet.js + npx prettier --write src/ npx prettier --write webpack.config.js - npx prettier --write fabula-ultima-sheet.css - npx prettier --write fabula-ultima-sheet.html + +typecheck: + npm run typecheck diff --git a/package-lock.json b/package-lock.json index 92620ed..27269cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,10 @@ "devDependencies": { "@babel/core": "^7.29.7", "@babel/preset-react": "^7.29.7", + "@babel/preset-typescript": "^7.29.7", "@babel/register": "^7.29.7", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "babel-loader": "^10.1.1", "copy-webpack-plugin": "^14.0.0", "css-loader": "^7.1.4", @@ -18,6 +21,7 @@ "react": "^19.2.7", "react-dom": "^19.2.7", "style-loader": "^4.0.0", + "typescript": "^6.0.3", "webpack": "^5.107.2", "webpack-cli": "^7.0.3", "webpack-dev-server": "^5.2.4" @@ -171,6 +175,38 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -181,6 +217,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -213,6 +263,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", @@ -223,6 +286,38 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -299,6 +394,39 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-display-name": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", @@ -368,6 +496,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-react": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", @@ -389,6 +537,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/register": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", @@ -1396,6 +1564,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", @@ -2751,6 +2939,13 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6587,6 +6782,20 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", diff --git a/package.json b/package.json index c02897e..8afc52d 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,17 @@ { "scripts": { "build": "webpack --mode=production", - "dev": "webpack serve --mode=development" + "dev": "webpack serve --mode=development", + "typecheck": "tsc --noEmit", + "prepare": "git config core.hooksPath .githooks || true" }, "devDependencies": { "@babel/core": "^7.29.7", "@babel/preset-react": "^7.29.7", + "@babel/preset-typescript": "^7.29.7", "@babel/register": "^7.29.7", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "babel-loader": "^10.1.1", "copy-webpack-plugin": "^14.0.0", "css-loader": "^7.1.4", @@ -17,6 +22,7 @@ "react": "^19.2.7", "react-dom": "^19.2.7", "style-loader": "^4.0.0", + "typescript": "^6.0.3", "webpack": "^5.107.2", "webpack-cli": "^7.0.3", "webpack-dev-server": "^5.2.4" diff --git a/src/BookIndex.jsx b/src/BookIndex.tsx similarity index 91% rename from src/BookIndex.jsx rename to src/BookIndex.tsx index 02204f8..348ea34 100644 --- a/src/BookIndex.jsx +++ b/src/BookIndex.tsx @@ -1,6 +1,12 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; -export default function BookIndex({ title, logoText, pages }) { +interface BookIndexProps { + title: string; + logoText: string; + pages: BookPage[]; +} + +export default function BookIndex({ title, logoText, pages }: BookIndexProps) { const pageNums = useMemo(() => pages.map(p => p.n), [pages]); const total = pageNums.length; @@ -13,9 +19,9 @@ export default function BookIndex({ title, logoText, pages }) { return 0; }); - const contentRef = useRef(null); + const contentRef = useRef(null); - const goTo = useCallback((n, smooth, push) => { + const goTo = useCallback((n: number, smooth: boolean, push: boolean) => { const idx = pageNums.indexOf(n); if (idx === -1) return; const sec = document.getElementById(`page-${n}`); @@ -62,8 +68,9 @@ export default function BookIndex({ title, logoText, pages }) { // Keyboard navigation useEffect(() => { - const handleKeyDown = e => { - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + const handleKeyDown = (e: KeyboardEvent) => { + const tag = (e.target as HTMLElement).tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA') return; setCurrentIdx(prev => { if ((e.key === 'ArrowLeft' || e.key === 'ArrowUp') && prev > 0) { goTo(pageNums[prev - 1], true, true); diff --git a/src/CharacterSheet.jsx b/src/CharacterSheet.tsx similarity index 93% rename from src/CharacterSheet.jsx rename to src/CharacterSheet.tsx index 0a525ea..ff305c2 100644 --- a/src/CharacterSheet.jsx +++ b/src/CharacterSheet.tsx @@ -25,7 +25,71 @@ const DISCIPLINES = [ "Spiritism", ]; -const BLANK_FIELDS = { +interface Fields { + charName: string; + charPronouns: string; + charIdentity: string; + charTheme: string; + charOrigin: string; + charTraits: string; + xpCurrent: string | number; + zenit: string | number; + initMod: string | number; + defense: string | number; + magDef: string | number; + dexBase: string | number; + dexCur: string | number; + insBase: string | number; + insCur: string | number; + migBase: string | number; + migCur: string | number; + wlpBase: string | number; + wlpCur: string | number; + hpMax: string | number; + hpCur: string | number; + mpMax: string | number; + mpCur: string | number; + ipMax: string | number; + ipCur: string | number; + backpack: string; + heroicSkills: string; + ritualsNotes: string; + accName: string; + accDesc: string; + armName: string; + armDesc: string; + mhName: string; + mhDesc: string; + ohName: string; + ohDesc: string; +} + +interface Bond { + name: string; + feelings: string[]; +} + +interface ClassEntry { + name: string; + benefits: string; + skills: string; +} + +interface Spell { + name: string; + notes: string; + mp: string | number; + targets: string; + duration: string; +} + +type CheckMap = Record; + +// Saved/shared payloads use abbreviated keys (with legacy long-name +// fallbacks), so the shape is intentionally loose. +type SavedData = Record; + +const BLANK_FIELDS: Fields = { charName: "", charPronouns: "", charIdentity: "", @@ -64,12 +128,12 @@ const BLANK_FIELDS = { ohDesc: "", }; -const BLANK_BONDS = Array.from({ length: 6 }, () => ({ +const BLANK_BONDS: Bond[] = Array.from({ length: 6 }, () => ({ name: "", feelings: [], })); -async function compressToBase64(str) { +async function compressToBase64(str: string) { const stream = new CompressionStream("deflate-raw"); const writer = stream.writable.getWriter(); writer.write(new TextEncoder().encode(str)); @@ -82,7 +146,7 @@ async function compressToBase64(str) { return btoa(binary); } -async function decompressFromBase64(b64) { +async function decompressFromBase64(b64: string) { try { const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); const stream = new DecompressionStream("deflate-raw"); @@ -103,15 +167,15 @@ export default function CharacterSheet() { const [copyStatus, setCopyStatus] = useState(false); const [level, setLevel] = useState(1); const [fp, setFp] = useState(0); - const [fields, setFields] = useState(BLANK_FIELDS); - const [bonds, setBonds] = useState(BLANK_BONDS); - const [primaryClasses, setPrimaryClasses] = useState([]); - const [otherClasses, setOtherClasses] = useState([]); - const [spells, setSpells] = useState([]); - const [statuses, setStatuses] = useState({}); - const [martial, setMartial] = useState({}); - const [disciplines, setDisciplines] = useState({}); - const [theme, setTheme] = useState( + const [fields, setFields] = useState(BLANK_FIELDS); + const [bonds, setBonds] = useState(BLANK_BONDS); + const [primaryClasses, setPrimaryClasses] = useState([]); + const [otherClasses, setOtherClasses] = useState([]); + const [spells, setSpells] = useState([]); + const [statuses, setStatuses] = useState({}); + const [martial, setMartial] = useState({}); + const [disciplines, setDisciplines] = useState({}); + const [theme, setTheme] = useState( () => localStorage.getItem("fabulaUltimaTheme") || (window.matchMedia("(prefers-color-scheme: light)").matches @@ -119,7 +183,7 @@ export default function CharacterSheet() { : "dark"), ); - const importFileRef = useRef(null); + const importFileRef = useRef(null); useEffect(() => { document.documentElement.dataset.theme = theme; @@ -127,14 +191,15 @@ export default function CharacterSheet() { }, [theme]); const f = useCallback( - (key, val) => setFields((prev) => ({ ...prev, [key]: val })), + (key: K, val: Fields[K]) => + setFields((prev) => ({ ...prev, [key]: val })), [], ); - const xp = parseInt(fields.xpCurrent) || 0; + const xp = parseInt(String(fields.xpCurrent)) || 0; const xpPct = Math.min((xp % 10) * 10, 100); - const hpMax = parseInt(fields.hpMax) || 0; - const hpCur = parseInt(fields.hpCur) || 0; + const hpMax = parseInt(String(fields.hpMax)) || 0; + const hpCur = parseInt(String(fields.hpCur)) || 0; const inCrisis = hpMax > 0 && hpCur <= Math.floor(hpMax / 2); const fpTotal = Math.max(10, fp); @@ -210,7 +275,7 @@ export default function CharacterSheet() { ], ); - const applyData = useCallback((d) => { + const applyData = useCallback((d: SavedData) => { setFields({ charName: d.n ?? d.name ?? "", charPronouns: d.pn ?? d.pronouns ?? "", @@ -266,7 +331,7 @@ export default function CharacterSheet() { const rawBonds = d.bo ?? d.bonds; if (rawBonds) setBonds( - rawBonds.map((b) => ({ + rawBonds.map((b: SavedData) => ({ name: b.n ?? b.name ?? "", feelings: b.f ?? b.feelings ?? [], })), @@ -275,7 +340,7 @@ export default function CharacterSheet() { const rawPrimary = d.pc ?? d.primaryClasses; if (rawPrimary) setPrimaryClasses( - rawPrimary.map((c) => ({ + rawPrimary.map((c: SavedData) => ({ name: c.n ?? c.name ?? "", benefits: c.b ?? c.benefits ?? "", skills: c.s ?? c.skills ?? "", @@ -285,7 +350,7 @@ export default function CharacterSheet() { const rawOther = d.oc ?? d.otherClasses; if (rawOther) setOtherClasses( - rawOther.map((c) => ({ + rawOther.map((c: SavedData) => ({ name: c.n ?? c.name ?? "", benefits: c.b ?? c.benefits ?? "", skills: c.s ?? c.skills ?? "", @@ -295,7 +360,7 @@ export default function CharacterSheet() { const rawSpells = d.sp ?? d.spells; if (rawSpells) setSpells( - rawSpells.map((s) => ({ + rawSpells.map((s: SavedData) => ({ name: s.n ?? s.name ?? "", notes: s.nt ?? s.notes ?? "", mp: s.mp ?? "", @@ -369,13 +434,13 @@ export default function CharacterSheet() { }, [collectData]); const handleImportFile = useCallback( - (e) => { - const file = e.target.files[0]; + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { - applyData(JSON.parse(ev.target.result)); + applyData(JSON.parse(ev.target?.result as string)); saveSheet(); } catch { alert("Could not import: invalid JSON file."); @@ -399,25 +464,25 @@ export default function CharacterSheet() { }, [collectData]); const calcHP = useCallback(() => { - const mig = parseInt(fields.migBase) || 6; + const mig = parseInt(String(fields.migBase)) || 6; const max = mig * 5 + level; setFields((prev) => ({ ...prev, hpMax: max, hpCur: prev.hpCur || max })); }, [fields.migBase, level]); const calcMP = useCallback(() => { - const wlp = parseInt(fields.wlpBase) || 6; + const wlp = parseInt(String(fields.wlpBase)) || 6; const max = wlp * 5 + level; setFields((prev) => ({ ...prev, mpMax: max, mpCur: prev.mpCur || max })); }, [fields.wlpBase, level]); - const toggleStatus = (s) => + const toggleStatus = (s: string) => setStatuses((prev) => ({ ...prev, [s]: !prev[s] })); - const toggleMartial = (m) => + const toggleMartial = (m: string) => setMartial((prev) => ({ ...prev, [m]: !prev[m] })); - const toggleDisc = (d) => + const toggleDisc = (d: string) => setDisciplines((prev) => ({ ...prev, [d]: !prev[d] })); - const toggleFeeling = (bondIdx, feeling) => { + const toggleFeeling = (bondIdx: number, feeling: string) => { setBonds((prev) => prev.map((b, i) => { if (i !== bondIdx) return b; @@ -647,12 +712,12 @@ export default function CharacterSheet() { Attributes
- {[ + {([ { label: "Dexterity", base: "dexBase", cur: "dexCur" }, { label: "Insight", base: "insBase", cur: "insCur" }, { label: "Might", base: "migBase", cur: "migCur" }, { label: "Willpower", base: "wlpBase", cur: "wlpCur" }, - ].map(({ label, base, cur }) => ( + ] as const).map(({ label, base, cur }) => (
{label}
@@ -917,7 +982,7 @@ export default function CharacterSheet() { ))}
- {[ + {([ { slot: "Accessory", name: "accName", @@ -946,7 +1011,7 @@ export default function CharacterSheet() { namePh: "Weapon / shield", descPh: "Damage / effect", }, - ].map(({ slot, name, desc, namePh, descPh }) => ( + ] as const).map(({ slot, name, desc, namePh, descPh }) => (
{slot}
@@ -1346,6 +1411,7 @@ export default function CharacterSheet() {