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 <noreply@anthropic.com>
This commit is contained in:
6
.githooks/pre-commit
Executable file
6
.githooks/pre-commit
Executable file
@@ -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
|
||||
7
Justfile
7
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
|
||||
|
||||
|
||||
209
package-lock.json
generated
209
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<HTMLDivElement>(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);
|
||||
@@ -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<string, boolean>;
|
||||
|
||||
// Saved/shared payloads use abbreviated keys (with legacy long-name
|
||||
// fallbacks), so the shape is intentionally loose.
|
||||
type SavedData = Record<string, any>;
|
||||
|
||||
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<Fields>(BLANK_FIELDS);
|
||||
const [bonds, setBonds] = useState<Bond[]>(BLANK_BONDS);
|
||||
const [primaryClasses, setPrimaryClasses] = useState<ClassEntry[]>([]);
|
||||
const [otherClasses, setOtherClasses] = useState<ClassEntry[]>([]);
|
||||
const [spells, setSpells] = useState<Spell[]>([]);
|
||||
const [statuses, setStatuses] = useState<CheckMap>({});
|
||||
const [martial, setMartial] = useState<CheckMap>({});
|
||||
const [disciplines, setDisciplines] = useState<CheckMap>({});
|
||||
const [theme, setTheme] = useState<string>(
|
||||
() =>
|
||||
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<HTMLInputElement>(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 })),
|
||||
<K extends keyof Fields>(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<HTMLInputElement>) => {
|
||||
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() {
|
||||
<span className="icon">◈</span> Attributes
|
||||
</div>
|
||||
<div className="attr-grid">
|
||||
{[
|
||||
{([
|
||||
{ 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 }) => (
|
||||
<div key={label} className="attr-block">
|
||||
<div className="attr-name">{label}</div>
|
||||
<div className="attr-inputs">
|
||||
@@ -917,7 +982,7 @@ export default function CharacterSheet() {
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginTop: 14 }}>
|
||||
{[
|
||||
{([
|
||||
{
|
||||
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 }) => (
|
||||
<div key={slot} className="equip-row">
|
||||
<div className="equip-slot">{slot}</div>
|
||||
<div className="equip-fields">
|
||||
@@ -1346,6 +1411,7 @@ export default function CharacterSheet() {
|
||||
<button
|
||||
className="btn-load btn-import btn-lg"
|
||||
onClick={() => {
|
||||
if (!importFileRef.current) return;
|
||||
importFileRef.current.value = "";
|
||||
importFileRef.current.click();
|
||||
}}
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import BookIndex from './BookIndex.jsx';
|
||||
import BookIndex from './BookIndex';
|
||||
|
||||
const { title, logoText, pages } = window.__BOOK_DATA__;
|
||||
createRoot(document.getElementById('root')).render(
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<BookIndex title={title} logoText={logoText} pages={pages} />
|
||||
);
|
||||
16
src/globals.d.ts
vendored
Normal file
16
src/globals.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
declare module "*.css";
|
||||
|
||||
interface BookPage {
|
||||
n: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface BookData {
|
||||
title: string;
|
||||
logoText: string;
|
||||
pages: BookPage[];
|
||||
}
|
||||
|
||||
interface Window {
|
||||
__BOOK_DATA__: BookData;
|
||||
}
|
||||
@@ -2,4 +2,4 @@ import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import CharacterSheet from './CharacterSheet';
|
||||
|
||||
createRoot(document.getElementById('root')).render(<CharacterSheet />);
|
||||
createRoot(document.getElementById('root')!).render(<CharacterSheet />);
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -44,8 +44,8 @@ module.exports = (env, argv) => {
|
||||
|
||||
return {
|
||||
entry: {
|
||||
sheet: "./src/sheet-main.jsx",
|
||||
book: "./src/book.js",
|
||||
sheet: "./src/sheet-main.tsx",
|
||||
book: "./src/book.tsx",
|
||||
},
|
||||
output: {
|
||||
filename: isProd ? "[name].[contenthash].js" : "[name].js",
|
||||
@@ -56,11 +56,13 @@ module.exports = (env, argv) => {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
test: /\.(js|jsx|ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: { presets: ["@babel/preset-react"] },
|
||||
options: {
|
||||
presets: ["@babel/preset-react", "@babel/preset-typescript"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -73,7 +75,7 @@ module.exports = (env, argv) => {
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".jsx"],
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
||||
},
|
||||
plugins: [
|
||||
...(isProd
|
||||
|
||||
Reference in New Issue
Block a user