Compare commits

...

12 Commits

Author SHA1 Message Date
d2e4345d20 Enhance SheetHeader component: add total attribute calculation methods (LP, AP, KP, SP), update progress bar logic, and streamline data handling. 2025-07-14 00:07:09 +02:00
ca72b7119f Add Shaddar est Mudor.json to public/data: introduce new JSON file for data management. 2025-07-13 23:48:23 +02:00
b5acb2b76d Add CharacterSheet type definition: introduce foundational TypeScript interfaces and types for character management. 2025-07-13 02:31:29 +02:00
81b365d8a2 Refactor Skills component: improve data loading logic with useCallback, add error handling, update UI styles, and enhance state management. 2025-07-13 02:28:15 +02:00
fbd5b2cfb6 Refactor SheetHeader styles and layout: adjust attribute card design, improve responsiveness, and streamline conditional rendering logic. 2025-07-13 02:28:04 +02:00
2a1d0a470f Enhance SheetHeader component: add error handling, implement loading states, support conditional rendering for attributes, and improve state management with useCallback. 2025-07-12 20:13:48 +02:00
44f60270eb Refactor imports and paths: implement base URL aliasing with @, adjust TypeScript configuration, and update all relevant imports. 2025-07-06 23:21:27 +02:00
87a533858a Enhance SheetHeader tooltips: add hoverable tooltips for attribute names. 2025-07-06 22:36:18 +02:00
fd11a2efe0 Refactor SheetHeader component: improve formatting, adjust styles, and update data handling logic. 2025-07-06 22:33:41 +02:00
b5d85bc0fc Refactor SheetHeader component: improve formatting, adjust styles, and update data handling logic. 2025-07-06 22:32:52 +02:00
44d74396e5 added racevariant parsing 2025-07-06 22:19:41 +02:00
1a4c5d96e7 Refactor data loading logic: replace dynamic imports with fetch, update skills type, and integrate vite-plugin-static-copy for JSON assets. 2025-07-06 21:37:07 +02:00
19 changed files with 1085 additions and 320 deletions

191
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@types/node": "^24.0.10",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@@ -26,7 +27,8 @@
"globals": "^16.2.0", "globals": "^16.2.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.34.1", "typescript-eslint": "^8.34.1",
"vite": "^7.0.0" "vite": "^7.0.0",
"vite-plugin-static-copy": "^3.1.0"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -1636,6 +1638,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "24.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.1.8", "version": "19.1.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
@@ -2003,6 +2014,20 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2016,6 +2041,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -2121,6 +2159,44 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chokidar/node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/chownr": { "node_modules/chownr": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -2606,6 +2682,21 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/fs-extra": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2716,6 +2807,19 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2831,6 +2935,19 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3237,6 +3354,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3287,6 +3414,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-map": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz",
"integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3439,6 +3579,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3756,6 +3909,22 @@
"typescript": ">=4.8.4 <5.9.0" "typescript": ">=4.8.4 <5.9.0"
} }
}, },
"node_modules/undici-types": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -3871,6 +4040,26 @@
} }
} }
}, },
"node_modules/vite-plugin-static-copy": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.0.tgz",
"integrity": "sha512-ONFBaYoN1qIiCxMCfeHI96lqLza7ujx/QClIXp4kEULUbyH2qLgYoaL8JHhk3FWjSB4TpzoaN3iMCyCFldyXzw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.3",
"fs-extra": "^11.3.0",
"p-map": "^7.0.3",
"picocolors": "^1.1.1",
"tinyglobby": "^0.2.14"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
"version": "6.4.6", "version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",

View File

@@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@types/node": "^24.0.10",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@@ -28,6 +29,7 @@
"globals": "^16.2.0", "globals": "^16.2.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.34.1", "typescript-eslint": "^8.34.1",
"vite": "^7.0.0" "vite": "^7.0.0",
"vite-plugin-static-copy": "^3.1.0"
} }
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,46 +1,46 @@
import './App.css' import '@/App.css'
import Main from "./modules/main/components/Main.tsx"; import {useState} from "react";
import Footer from "./modules/footer/components/Footer.tsx"; import type {CharacterData} from "@/types/CharacterJson.ts";
import {useEffect, useState} from "react"; import Header from "@/modules/header/components/Header.tsx";
import type {CharacterData} from "./types/CharacterJson.ts"; import Footer from "@/modules/footer/components/Footer.tsx";
import Header from "./modules/header/components/Header.tsx"; import Main from "@/modules/main/components/Main.tsx";
function App() { function App() {
const [jsonData, setJsonData] = useState<CharacterData | null>(null); const [jsonData, setJsonData] = useState<CharacterData | null>(null);
const [loading, setLoading] = useState(true); // const [loading, setLoading] = useState(true);
useEffect(() => { // useEffect(() => {
const loadData = async () => { // const loadData = async () => {
try { // try {
const response = await fetch('/data/Faelyn Eichenahauch.json'); // const response = await fetch('/data/Faelyn Eichenahauch.json');
if (!response.ok) { // if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); // throw new Error(`HTTP error! status: ${response.status}`);
} // }
const data = await response.json(); // const data = await response.json();
setJsonData(data); // setJsonData(data);
} catch (error) { // } catch (error) {
console.error('Error loading JSON file:', error); // console.error('Error loading JSON file:', error);
} finally { // } finally {
setLoading(false); // setLoading(false);
} // }
}; // };
//
loadData(); // loadData();
}, []); // }, []);
const handleFileLoad = (data: CharacterData) => { const handleFileLoad = (data: CharacterData) => {
setJsonData(data); setJsonData(data);
setLoading(false); // setLoading(false);
}; };
if (loading) return <div>Loading...</div>; // if (loading) return <div>Loading...</div>;
if (!jsonData) return ( if (!jsonData) return (
<> <>
<Header onFileLoad={handleFileLoad}/> <Header onFileLoad={handleFileLoad}/>
<Footer/> <Footer/>
</> </>
); );

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App' import App from '@/App'
import './index.css' import '@/index.css'
// Ensure root element exists // Ensure root element exists
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
@@ -11,7 +11,7 @@ if (!rootElement) {
// Create root and render app // Create root and render app
ReactDOM.createRoot(rootElement).render( ReactDOM.createRoot(rootElement).render(
<React.StrictMode> <React.StrictMode>
<App /> <App/>
</React.StrictMode>, </React.StrictMode>,
); );

View File

@@ -1,11 +1,11 @@
import ImportButton from "./ImportButton"; import type {CharacterData} from '@/types/CharacterJson.ts';
import type { CharacterData } from '../../../types/CharacterJson.ts'; import ImportButton from "@/modules/header/components/ImportButton";
interface HeaderProps { interface HeaderProps {
onFileLoad: (data: CharacterData) => void; onFileLoad: (data: CharacterData) => void;
} }
export default function Header({ onFileLoad }: HeaderProps) { export default function Header({onFileLoad}: HeaderProps) {
return ( return (
<header className="bg-gray-800"> <header className="bg-gray-800">
<nav className="container mx-auto flex justify-between items-center py-6 px-6"> <nav className="container mx-auto flex justify-between items-center py-6 px-6">

View File

@@ -1,13 +1,13 @@
import { useRef } from 'react'; import {useRef} from 'react';
import type { CharacterData } from '../../../../types/CharacterJson.ts'; import type {CharacterData} from '@/types/CharacterJson.ts';
interface ImportButtonProps { interface ImportButtonProps {
onFileLoad: (data: CharacterData) => void; onFileLoad: (data: CharacterData) => void;
} }
export default function ImportButton({ onFileLoad }: ImportButtonProps) { export default function ImportButton({onFileLoad}: ImportButtonProps) {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
@@ -38,23 +38,23 @@ export default function ImportButton({ onFileLoad }: ImportButtonProps) {
fileInputRef.current.value = ''; fileInputRef.current.value = '';
} }
}; };
return ( return (
<div className="relative"> <div className="relative">
<input <input
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
accept=".json" accept=".json"
onChange={handleFileChange} onChange={handleFileChange}
className="hidden" className="hidden"
id="character-import" id="character-import"
/> />
<label <label
htmlFor="character-import" htmlFor="character-import"
className="cursor-pointer bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors duration-200" className="cursor-pointer bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors duration-200"
> >
Charakter laden Charakter laden
</label> </label>
</div> </div>
); );
} }

View File

@@ -1,6 +1,6 @@
import Tabs from "./Tabs"; import type {CharacterData} from "@/types/CharacterJson.ts";
import SheetHeader from "../modules/sheetHeader/components/SheetHeader.tsx"; import SheetHeader from "@/modules/main/modules/sheetHeader/components/SheetHeader.tsx";
import type {CharacterData} from "../../../types/CharacterJson.ts"; import Tabs from "@/modules/main/components/Tabs.tsx";
export default function Main({jsonData}: { jsonData: CharacterData }) { export default function Main({jsonData}: { jsonData: CharacterData }) {
return ( return (

View File

@@ -1,13 +1,13 @@
import {useState} from "react"; import {useState} from "react";
import Skills from "../modules/skills/components/Skills.tsx"; import Skills from "@/modules/main/modules/skills/components/Skills.tsx";
import Combat from "../modules/combat/components/Combat.tsx"; import Combat from "@/modules/main/modules/combat/components/Combat.tsx";
import Equipment from "../modules/equipment/components/Equipment.tsx"; import Attributes from "@/modules/main/modules/attributes/components/Attributes.tsx";
import Attributes from "../modules/attributes/components/Attributes.tsx"; import Equipment from "@/modules/main/modules/equipment/components/Equipment.tsx";
import State from "../modules/state/components/State.tsx"; import State from "@/modules/main/modules/state/components/State.tsx";
import Magic from "../modules/magic/components/Magic.tsx"; import Magic from "@/modules/main/modules/magic/components/Magic.tsx";
import Religion from "../modules/religion/components/Religion.tsx"; import Religion from "@/modules/main/modules/religion/components/Religion.tsx";
import Notes from "../modules/notes/components/Notes.tsx"; import Notes from "@/modules/main/modules/notes/components/Notes.tsx";
import type {CharacterData} from "../../../types/CharacterJson.ts"; import type {CharacterData} from "@/types/CharacterJson.ts";
export default function Tabs({jsonData}: { jsonData: CharacterData }) { export default function Tabs({jsonData}: { jsonData: CharacterData }) {
const [activeTab, setActiveTab] = useState("skills"); const [activeTab, setActiveTab] = useState("skills");

View File

@@ -1,131 +1,253 @@
import type {CharacterData} from "../../../../../types/CharacterJson.ts"; import type {CharacterData} from "@/types/CharacterJson.ts";
import type {AttributeWithValue} from "../../../../../utils/loaders"; import {
import {useState, useEffect} from 'react'; type AttributeWithValue,
import {loadRace, loadProfession, loadAttributesWithValues} from "../../../../../utils/loaders"; loadAttributesWithValues,
loadProfession,
loadRace,
loadRaceVariant
} from "@/utils/loaders";
import {useCallback, useEffect, useState} from 'react';
export default function SheetHeader({jsonData}: { jsonData: CharacterData }) { export default function SheetHeader({jsonData}: { jsonData: CharacterData }) {
const [raceName, setRaceName] = useState<string>(jsonData.r); const [raceName, setRaceName] = useState<string>(jsonData.r);
const [raceVariantName, setRaceVariantName] = useState<string>(jsonData.rv || '');
const [professionName, setProfessionName] = useState<string>(jsonData.p); const [professionName, setProfessionName] = useState<string>(jsonData.p);
const [attributes, setAttributes] = useState<AttributeWithValue[]>([]); const [attributes, setAttributes] = useState<AttributeWithValue[]>([]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => { const [error, setError] = useState<string | null>(null);
let isMounted = true;
const resetToDefaults = useCallback(() => {
const loadData = async () => { setRaceName(jsonData.r);
try { setRaceVariantName(jsonData.rv || '');
// Load race and profession using the new loader functions setProfessionName(jsonData.p);
const [race, profession] = await Promise.all([ setAttributes([]);
loadRace(jsonData.r), }, [jsonData.r, jsonData.rv, jsonData.p]);
loadProfession(jsonData.p)
]); const loadData = useCallback(async (signal: AbortSignal) => {
setLoading(true);
// Process attributes using the new loadAttributesWithValues function setError(null);
const loadedAttributes = await loadAttributesWithValues(jsonData.attr.values);
try {
if (isMounted) { // Load race and profession using the new loader functions
setRaceName(race?.name || jsonData.r); const [race, raceVariant, profession, loadedAttributes] = await Promise.all([
// For profession, handle gendered name loadRace(jsonData.r),
if (profession?.name) { loadRaceVariant(jsonData.rv || ''),
setProfessionName(profession.name.m || profession.name.f || jsonData.p); loadProfession(jsonData.p),
} else { loadAttributesWithValues(jsonData.attr.values),
setProfessionName(jsonData.p); ]);
}
// Set the attributes with their values, names, and short forms // Check if component is still mounted and request wasn't cancelled
setAttributes(loadedAttributes); if (signal.aborted) return;
}
} catch (error) { // Update state with loaded data
console.error('Error loading data:', error); setRaceName(race?.name || jsonData.r);
if (isMounted) { setRaceVariantName(raceVariant?.name || '');
setRaceName(jsonData.r);
setProfessionName(jsonData.p); // Handle profession name with gender preference
} if (profession?.name) {
setProfessionName(profession.name.m || profession.name.f || jsonData.p);
} else {
setProfessionName(jsonData.p);
} }
};
setAttributes(loadedAttributes);
loadData(); } catch (err) {
if (signal.aborted) return;
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
console.error('Error loading character data:', errorMessage);
setError(errorMessage);
// Reset to default values on error
resetToDefaults();
} finally {
if (!signal.aborted) {
setLoading(false);
}
}
}, [jsonData.r, jsonData.rv, jsonData.p, jsonData.attr, resetToDefaults]);
useEffect(() => {
const abortController = new AbortController();
loadData(abortController.signal);
return () => { return () => {
isMounted = false; abortController.abort();
}; };
}, [jsonData.r, jsonData.p, jsonData.attr]); }, [loadData]);
console.log(jsonData) // Optional: Add loading state handling
if (loading) {
return <div className="animate-pulse">Loading character data...</div>;
}
// Optional: Add error state handling
if (error) {
return (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
Error loading character data: {error}
</div>
);
}
const calculateTotalLp = () => {
// GW der Spezies + KO + KO
const tempRaceLp = 5 // TODO: Needs to be pulled from universal stats
const constVal = attributes.find(attr => attr.id === 'ATTR_7')?.value || 0; // ATTR_7 = Constitution
const bonusMalus = 0 // Negative when malus
const zukauf = 0
return (jsonData.attr.lp + constVal * 2 + tempRaceLp + zukauf + bonusMalus);
}
const calculateTotalAP = () => {
// TODO: Only show if Zauberer
// 20 durch Zauberer + Leiteigenschaft
const initial = 20
const leiteigenschaft = 0 // TODO: needs to be pulled
const bonusMalus = 0 // Negative when malus
const zukauf = 0
return (jsonData.attr.ae + initial + leiteigenschaft + bonusMalus + zukauf);
}
const calculateTotalKP = () => {
// TODO: Only show if Geweihter
// 20 durch Geweiht + Leiteigenschaft
const initial = 20
const leiteigenschaft = 0 // TODO: needs to be pulled
const bonusMalus = 0 // Negative when malus
const zukauf = 0
return (jsonData.attr.kp + initial + leiteigenschaft + bonusMalus + zukauf);
}
const calculateTotalSp = () => {
// TODO: needs to be pulled
const initial = 3
const bonus = 0
return (jsonData.attr.sp || 0 + initial + bonus);
}
return ( return (
<> <>
<div className="flex justify-between items-center py-5 px-5"> <div className="flex justify-between items-end py-5 px-5">
<div className="w-[30%]"> <div className="w-[30%]">
<p className="border-b border-gray-500 font-semibold"> <p className="border-b border-gray-500 font-semibold">
{jsonData.name} {jsonData.name}
</p> </p>
<small>Name</small> <small className="text-gray-400">Name</small>
</div> </div>
<div className="w-[30%]"> <div className="w-[30%]">
<p className="border-b border-gray-500 font-semibold"> <p className="border-b border-gray-500 font-semibold">
{raceName} {raceName} {raceVariantName ? <>({raceVariantName})</> : ''}
</p> </p>
<small>Spezies</small> <small className="text-gray-400">Spezies</small>
</div> </div>
<div className="w-[30%]"> <div className="w-[30%]">
<p className="border-b border-gray-500 font-semibold"> <p className="border-b border-gray-500 font-semibold">
{professionName} {professionName}
</p> </p>
<small>Profession</small> <small className="text-gray-400">Profession</small>
</div> </div>
</div> </div>
<div className="flex justify-between items-center py-3 px-10"> <div className="flex justify-between items-center py-5 px-5 gap-6">
<div className="w-[20%] h-[20%] rounded-full overflow-hidden"> <div className="w-[30%] h-full rounded-md bg-gray-600 overflow-hidden"
<img src="#" alt="char bild"/> style={{height: '12.5em', width: '9.375em'}}>
<img src="#" alt="char bild" className="object-cover w-full h-full"/>
</div> </div>
<div className="flex flex-col gap-2 w-[80%]"> <div className="w-[70%] flex flex-col gap-2 w-[80%]">
<div className="flex justify-between"> <div className="w-[100%] flex justify-between">
{attributes.map(attr => ( {attributes.map(attr => (
<div key={attr.id} className="w-[10%] text-center"> <div key={attr.id} className="">
<div className="font-bold">{attr.short}</div> <div className="relative group cursor-help flex items-center justify-center">
<div>{attr.value}</div> <div>{attr.short}</div>
<span
className="absolute left-1/2 transform -translate-x-1/2 bottom-full bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
{attr.name}
</span>
</div>
<span className="font-bold" style={{fontSize: '1.5em'}}>{attr.value}</span>
</div> </div>
))} ))}
</div> </div>
<div className="w-[100%] flex pt-5"> <div className="w-[100%] flex pt-2">
<span className="relative group cursor-help"> <span className="relative group cursor-help">
LP LP
<span <span
className="absolute left-0 bottom-full mb-1 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap"> className="absolute left-0 bottom-full mb-1 px-2 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
Lebenspunkte Lebenspunkte
</span> </span>
</span> </span>
<div <div
className="ms-5 w-[50%] bg-red-800 rounded-s-full h-6 flex items-center justify-center text-white text-sm font-medium"> className="ms-5 w-[100%] bg-gray-600 rounded-full h-6 flex items-center overflow-hidden relative">
15 / 15 <div
className="bg-gradient-to-r from-red-900 to-red-600 h-full transition-all duration-300"
style={{width: `${Math.min(((calculateTotalLp() - jsonData.attr.permanentLP.lost) / calculateTotalLp()) * 100, 100)}%`}}
/>
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
{calculateTotalLp()} / {calculateTotalLp() - jsonData.attr.permanentLP.lost}
</div>
</div> </div>
<div
className="me-5 w-[50%] bg-green-800 rounded-e-full h-6 flex items-center justify-center text-white text-sm font-medium">
16 / 16
</div>
<span className="relative group cursor-help whitespace-nowrap">
AP/KP
<span
className="absolute right-0 bottom-full mb-1 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
Astralpunkte / Karmapunkte
</span>
</span>
</div> </div>
<div className="w-[100%] flex pt-5"> <div className="w-[100%] flex pt-2">
<span className="w-[10%] relative group cursor-help"> <span className="relative group cursor-help">
SP AP
<span <span
className="absolute left-0 bottom-full mb-1 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap"> className="absolute left-0 bottom-full mb-1 px-2 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
Schicksalspunkte Astralpunkte
</span> </span>
</span> </span>
<div className="w-[90%] flex items-center gap-2"> <div
<div className="rounded-full w-5 h-5 bg-violet-400"></div> className="ms-5 w-[100%] bg-gray-600 rounded-full h-6 flex items-center overflow-hidden relative">
<div className="rounded-full w-5 h-5 bg-violet-400"></div> <div
<div className="rounded-full w-5 h-5 bg-violet-400"></div> className="bg-gradient-to-r from-blue-900 to-blue-500 h-full transition-all duration-300"
style={{width: `${Math.min(((calculateTotalAP() - jsonData.attr.permanentAE.lost) / calculateTotalAP()) * 100, 100)}%`}}
/>
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
{calculateTotalAP()} / {calculateTotalAP() - jsonData.attr.permanentAE.lost}
</div>
</div>
</div>
<div className="w-[100%] flex pt-2">
<span className="relative group cursor-help">
KP
<span
className="absolute left-0 bottom-full mb-1 px-2 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
Karmapunkte
</span>
</span>
<div
className="ms-5 w-[100%] bg-gray-600 rounded-full h-6 flex items-center overflow-hidden relative">
<div
className="bg-gradient-to-r from-emerald-900 to-emerald-500 h-full transition-all duration-300"
style={{width: `${Math.min(((calculateTotalKP() - jsonData.attr.permanentKP.lost) / calculateTotalKP()) * 100, 100)}%`}}
/>
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
{calculateTotalKP()} / {calculateTotalKP() - jsonData.attr.permanentKP.lost}
</div>
</div>
</div>
<div className="w-[100%] flex pt-2">
<span className="relative group cursor-help">
SP
<span
className="absolute left-0 bottom-full mb-1 px-2 py-1 bg-gray-800 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
Schicksalspunkte
</span>
</span>
<div className="ms-5 w-[100%] flex items-center gap-2">
{Array.from({length: calculateTotalSp()}, (_, index) => (
<div
key={`sp-available-${index}`}
className="rounded-full w-5 h-5 bg-gradient-to-br from-violet-700 to-violet-500"
/>
))}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,84 +1,105 @@
import { useState, useEffect } from 'react'; import {useCallback, useEffect, useState} from 'react';
import type { CharacterData } from '../../../../../types/CharacterJson'; import type {CharacterData} from '@/types/CharacterJson.ts';
import { loadSkillsWithValues, type SkillWithValue } from '../../../../../utils/loaders'; import {loadSkillsWithValues, type SkillWithValue} from '@/utils/loaders.ts';
export default function Skills({ jsonData }: { jsonData: CharacterData }) { export default function Skills({jsonData}: { jsonData: CharacterData }) {
const [skills, setSkills] = useState<SkillWithValue[]>([]); const [skills, setSkills] = useState<SkillWithValue[]>([]);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { const resetToDefaults = useCallback(() => {
let isMounted = true; setSkills([]);
}, []);
const loadData = async () => {
try { const loadData = useCallback(async (signal: AbortSignal) => {
setLoading(true); setLoading(true);
// Load skills with their values using the new loader function setError(null);
const loadedSkills = await loadSkillsWithValues(jsonData.talents);
try {
if (isMounted) { // Load skills with their values using the new loader function
setSkills(loadedSkills); const loadedSkills = await loadSkillsWithValues(jsonData.talents);
setLoading(false);
} // Check if component is still mounted and request wasn't cancelled
} catch (error) { if (signal.aborted) return;
console.error('Error loading skills:', error);
if (isMounted) { setSkills(loadedSkills);
setError('Failed to load skills. Please try again.'); } catch (err) {
setLoading(false); if (signal.aborted) return;
}
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
console.error('Error loading skills:', errorMessage);
setError('Failed to load skills. Please try again.');
// Reset to default values on error
resetToDefaults();
} finally {
if (!signal.aborted) {
setLoading(false);
} }
}; }
}, [jsonData.talents, resetToDefaults]);
loadData();
useEffect(() => {
const abortController = new AbortController();
loadData(abortController.signal);
return () => { return () => {
isMounted = false; abortController.abort();
}; };
}, [jsonData.talents]); }, [loadData]);
if (loading) { if (loading) {
return <div>Loading skills...</div>; return <div className="animate-pulse">Loading skills...</div>;
} }
if (error) { if (error) {
return <div className="text-red-500">{error}</div>; return (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
Error loading skills: {error}
</div>
);
} }
if (skills.length === 0) { if (skills.length === 0) {
return <div>No skills found.</div>; return <div>No skills found.</div>;
} }
console.log(jsonData);
return ( return (
<div className="p-4"> <div className="p-4">
<h1 className="text-2xl font-bold mb-4">Skills</h1> <h1 className="text-2xl font-bold mb-4">Skills</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{skills.map(skill => ( {skills.map(skill => (
<div key={skill.id} className="border p-3 rounded shadow"> <div key={skill.id} className="border p-3 rounded shadow">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">{skill.name}</h2> <h2 className="text-lg font-semibold">{skill.name}</h2>
<span className="bg-blue-100 text-blue-800 font-bold px-2 py-1 rounded"> <span
className="bg-blue-100 text-blue-800 font-bold px-2 py-1 rounded flex items-center justify-center"
style={{width: '2.5em'}}>
{skill.value} {skill.value}
</span> </span>
</div> </div>
{/*{skill.applications && skill.applications.length > 0 && (*/} {/*{skill.applications && skill.applications.length > 0 && (*/}
{/* <div className="mt-2">*/} {/* <div className="mt-2">*/}
{/* <h3 className="font-medium">Applications:</h3>*/} {/* <h3 className="font-medium">Applications:</h3>*/}
{/* <ul className="list-disc list-inside">*/} {/* <ul className="list-disc list-inside">*/}
{/* {skill.applications.map((app: any, index: number) => (*/} {/* {skill.applications.map((app: any, index: number) => (*/}
{/* <li key={index}>{app.name}</li>*/} {/* <li key={index}>{app.name}</li>*/}
{/* ))}*/} {/* ))}*/}
{/* </ul>*/} {/* </ul>*/}
{/* </div>*/} {/* </div>*/}
{/*)}*/} {/*)}*/}
{/*{skill.tools && (*/} {skill.tools && (
{/* <div className="mt-2">*/} <div className="mt-2">
{/* <h3 className="font-medium">Tools:</h3>*/} <h3 className="font-medium">Tools:</h3>
{/* <p className="text-sm">{skill.tools}</p>*/} <p className="text-sm">{skill.tools}</p>
{/* </div>*/} </div>
{/*)}*/} )}
</div> </div>
))} ))}
</div>
</div> </div>
</div>
); );
} }

View File

@@ -1,5 +1,3 @@
import type {Skills} from "./Skill.ts";
export interface AttributeValue { export interface AttributeValue {
id: string; id: string;
value: number; value: number;
@@ -16,11 +14,10 @@ export interface Attributes {
ae: number; ae: number;
kp: number; kp: number;
lp: number; lp: number;
sp: number;
permanentAE: PermanentAttribute; permanentAE: PermanentAttribute;
permanentKP: PermanentAttribute; permanentKP: PermanentAttribute;
permanentLP: { permanentLP: PermanentAttribute;
lost: number;
};
} }
export interface PersonalDetails { export interface PersonalDetails {
@@ -89,13 +86,14 @@ export interface CharacterData {
ap: AdventurePoints; ap: AdventurePoints;
el: string; // Experience Level el: string; // Experience Level
r: string; // Race r: string; // Race
rv?: string;// Race_Variant
c: string; // Culture c: string; // Culture
p: string; // Profession p: string; // Profession
sex: string; sex: string;
pers: PersonalDetails; pers: PersonalDetails;
attr: Attributes; attr: Attributes;
activatable: Activatables; activatable: Activatables;
talents: Skills; talents: Record<string, number>;
ct: CombatTechniques; ct: CombatTechniques;
spells: Spells; spells: Spells;
cantrips: string[]; cantrips: string[];

142
src/types/CharacterSheet.ts Normal file
View File

@@ -0,0 +1,142 @@
// Basic ID pattern types
type HeroId = string; // Pattern: "^H_[1-9]\\d*$"
type RaceId = string; // Pattern: "^R_[1-9]\\d*$"
type RaceVariantId = string; // Pattern: "^RV_[1-9]\\d*$"
type CultureId = string; // Pattern: "^C_[1-9]\\d*$"
type ProfessionId = string; // Pattern: "^P_[1-9]\\d*$"
type ProfessionVariantId = string; // Pattern: "^PV_[1-9]\\d*$"
type ExperienceLevelId = string; // Pattern: "^EL_[1-9]\\d*$"
type AttributeId = string; // Pattern: "^ATTR_[1-9]\\d*$"
type TalentId = string; // Pattern: "^TAL_[1-9]\\d*$"
type CombatTechniqueId = string; // Pattern: "^CT_[1-9]\\d*$"
type SpellId = string; // Pattern: "^SPELL_[1-9]\\d*$"
type CantripId = string; // Pattern: "^CANTRIP_[1-9]\\d*$"
type LiturgyId = string; // Pattern: "^LITURGY_[1-9]\\d*$"
type BlessingId = string; // Pattern: "^BLESSING_[1-9]\\d*$"
type ItemId = string; // Pattern: "^ITEM_[1-9]\\d*$"
type ActivatableId = string; // Pattern: "^(ADV|DISADV|SA)_[1-9]\\d*$"
// Adventure Points
interface AdventurePoints {
total: number; // minimum: 1
}
// Rules configuration
interface Rules {
higherParadeValues: 0 | 2 | 4;
attributeValueLimit: boolean;
enableAllRuleBooks: boolean;
enabledRuleBooks: string[];
enableLanguageSpecializations: boolean;
}
// Personal data
interface PersonalData {
family?: string;
placeofbirth?: string;
dateofbirth?: string;
age?: string;
haircolor?: string;
eyecolor?: string;
size?: string;
weight?: string;
title?: string;
socialstatus?: number; // 1-5
characteristics?: string;
otherinfo?: string;
cultureAreaKnowledge?: string;
}
// Activatable items (advantages, disadvantages, special abilities)
interface ActivatableOption {
sid?: string | number;
sid2?: string | number;
sid3?: string | number;
tier?: number; // minimum: 1
cost?: number; // minimum: 0
}
// Permanent energy attributes
interface PermanentEnergy {
lost: number; // minimum: 0
redeemed?: number; // minimum: 0
}
interface PermanentLP {
lost: number; // minimum: 0
}
// Attribute value (older version - tuple format)
type AttributeTuple = [AttributeId, number, number]; // [id, value, adjustment]
// Attribute value (newer version - object format)
interface AttributeValue {
id: AttributeId;
value: number; // minimum: 8
}
// Attributes - supports both old and new formats
interface AttributesOld {
values: AttributeTuple[];
lp: number; // minimum: 0
ae: number; // minimum: 0
kp: number; // minimum: 0
permanentLP?: PermanentLP;
permanentAE: PermanentEnergy;
permanentKP: PermanentEnergy;
}
interface AttributesNew {
values: AttributeValue[];
attributeAdjustmentSelected: AttributeId;
lp: number; // minimum: 0
ae: number; // minimum: 0
kp: number; // minimum: 0
permanentLP?: PermanentLP;
permanentAE: PermanentEnergy;
permanentKP: PermanentEnergy;
}
// Belongings
interface Item {
id: ItemId;
price?: number; // minimum: 0
weight?: number; // minimum: 0
}
interface Belongings {
items: Record<ItemId, Item>;
}
// Main CharacterSheet type
export interface CharacterSheet {
id: HeroId;
name: string;
clientVersion: string; // Version pattern: (0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-(\\w+)\\.(0|[1-9]\\d*))?
dateCreated: string; // ISO date-time format
dateModified: string; // ISO date-time format
locale?: string; // Pattern: "^[a-z]\\-[A-Z]$"
avatar?: string;
ap: AdventurePoints;
r?: RaceId;
rv?: RaceVariantId;
c?: CultureId;
isCulturalPackageActive?: boolean;
p?: ProfessionId;
professionName?: string;
pv?: ProfessionVariantId;
sex: "m" | "f";
rules: Rules;
phase: 1 | 2 | 3; // 1: RCP selection, 2: standard creation, 3: after creation
el: ExperienceLevelId;
pers: PersonalData;
activatable: Record<ActivatableId, ActivatableOption[]>;
attr: AttributesOld | AttributesNew;
talents: Record<TalentId, number>; // minimum: 0
ct: Record<CombatTechniqueId, number>; // minimum: 0
spells: Record<SpellId, number>; // minimum: 0
cantrips: CantripId[];
liturgies: Record<LiturgyId, number>; // minimum: 0
blessings: BlessingId[];
belongings?: Belongings;
}

View File

@@ -1,64 +1,65 @@
import type {Race} from '../types/Race'; import type {Race} from '@/types/Race';
import type {Profession} from '../types/Profession'; import type {Profession} from '@/types/Profession';
import type {Attribute} from '../types/Attribute'; import type {Attribute} from '@/types/Attribute';
import type {AttributeValue} from '../types/CharacterJson'; import type {AttributeValue} from '@/types/CharacterJson';
import type {Advantage} from '../types/Advantage'; import type {Advantage} from '@/types/Advantage';
import type {AnimistForce} from '../types/AnimistForce'; import type {AnimistForce} from '@/types/AnimistForce';
// import type { ArcaneBardTradition } from '../types/ArcaneBardTradition'; // import type { ArcaneBardTradition } from '@/types/ArcaneBardTradition';
// import type { ArcaneDancerTradition } from '../types/ArcaneDancerTradition'; // import type { ArcaneDancerTradition } from '@/types/ArcaneDancerTradition';
// import type { ArmorType } from '../types/ArmorType'; // import type { ArmorType } from '@/types/ArmorType';
// import type { Aspect } from '../types/Aspect'; // import type { Aspect } from '@/types/Aspect';
// import type { BlessedTradition } from '../types/BlessedTradition'; // import type { BlessedTradition } from '@/types/BlessedTradition';
// import type { Blessing } from '../types/Blessing'; // import type { Blessing } from '@/types/Blessing';
// import type { Book } from '../types/Book'; // import type { Book } from '@/types/Book';
// import type { Brew } from '../types/Brew'; // import type { Brew } from '@/types/Brew';
// import type { Cantrip } from '../types/Cantrip'; // import type { Cantrip } from '@/types/Cantrip';
// import type { CombatSpecialAbilityGroup } from '../types/CombatSpecialAbilityGroup'; // import type { CombatSpecialAbilityGroup } from '@/types/CombatSpecialAbilityGroup';
// import type { CombatTechniqueGroup } from '../types/CombatTechniqueGroup'; // import type { CombatTechniqueGroup } from '@/types/CombatTechniqueGroup';
// import type { CombatTechnique } from '../types/CombatTechnique'; // import type { CombatTechnique } from '@/types/CombatTechnique';
// import type { Condition } from '../types/Condition'; // import type { Condition } from '@/types/Condition';
// import type { Culture } from '../types/Culture'; // import type { Culture } from '@/types/Culture';
// import type { Curse } from '../types/Curse'; // import type { Curse } from '@/types/Curse';
// import type { DerivedCharacteristic } from '../types/DerivedCharacteristic'; // import type { DerivedCharacteristic } from '@/types/DerivedCharacteristic';
// import type { Disadvantage } from '../types/Disadvantage'; // import type { Disadvantage } from '@/types/Disadvantage';
// import type { DominationRitual } from '../types/DominationRitual'; // import type { DominationRitual } from '@/types/DominationRitual';
// import type { Element } from '../types/Element'; // import type { Element } from '@/types/Element';
// import type { ElvenMagicalSong } from '../types/ElvenMagicalSong'; // import type { ElvenMagicalSong } from '@/types/ElvenMagicalSong';
// import type { EquipmentGroup } from '../types/EquipmentGroup'; // import type { EquipmentGroup } from '@/types/EquipmentGroup';
// import type { EquipmentPackage } from '../types/EquipmentPackage'; // import type { EquipmentPackage } from '@/types/EquipmentPackage';
// import type { Equipment } from '../types/Equipment'; // import type { Equipment } from '@/types/Equipment';
// import type { ExperienceLevel } from '../types/ExperienceLevel'; // import type { ExperienceLevel } from '@/types/ExperienceLevel';
// import type { EyeColor } from '../types/EyeColor'; // import type { EyeColor } from '@/types/EyeColor';
// import type { FocusRule } from '../types/FocusRule'; // import type { FocusRule } from '@/types/FocusRule';
// import type { GeodeRitual } from '../types/GeodeRitual'; // import type { GeodeRitual } from '@/types/GeodeRitual';
// import type { HairColor } from '../types/HairColor'; // import type { HairColor } from '@/types/HairColor';
// import type { LiturgicalChantEnhancement } from '../types/LiturgicalChantEnhancement'; // import type { LiturgicalChantEnhancement } from '@/types/LiturgicalChantEnhancement';
// import type { LiturgicalChantGroup } from '../types/LiturgicalChantGroup'; // import type { LiturgicalChantGroup } from '@/types/LiturgicalChantGroup';
// import type { LiturgicalChant } from '../types/LiturgicalChant'; // import type { LiturgicalChant } from '@/types/LiturgicalChant';
// import type { MagicalDance } from '../types/MagicalDance'; // import type { MagicalDance } from '@/types/MagicalDance';
// import type { MagicalMelodie } from '../types/MagicalMelodie'; // import type { MagicalMelodie } from '@/types/MagicalMelodie';
// import type { MagicalTradition } from '../types/MagicalTradition'; // import type { MagicalTradition } from '@/types/MagicalTradition';
// import type { OptionalRule } from '../types/OptionalRule'; // import type { OptionalRule } from '@/types/OptionalRule';
// import type { Pact } from '../types/Pact'; // import type { Pact } from '@/types/Pact';
// import type { PatronCategorie } from '../types/PatronCategorie'; // import type { PatronCategorie } from '@/types/PatronCategorie';
// import type { Patron } from '../types/Patron'; // import type { Patron } from '@/types/Patron';
// import type { ProfessionVariant } from '../types/ProfessionVariant'; // import type { ProfessionVariant } from '@/types/ProfessionVariant';
// import type { Propertie } from '../types/Propertie'; // import type { Propertie } from '@/types/Propertie';
// import type { RaceVariant } from '../types/RaceVariant'; // import type { RaceVariant } from '@/types/RaceVariant';
// import type { Reache } from '../types/Reache'; // import type { Reache } from '@/types/Reache';
// import type { RogueSpell } from '../types/RogueSpell'; // import type { RogueSpell } from '@/types/RogueSpell';
// import type { SkillGroup } from '../types/SkillGroup'; // import type { SkillGroup } from '@/types/SkillGroup';
import type {Skill, Skills} from '../types/Skill'; import type {Skill} from '@/types/Skill';
// import type { SocialStatuse } from '../types/SocialStatuse'; import type {RaceVariant} from "@/types/RaceVariant.ts";
// import type { SpecialAbilitie } from '../types/SpecialAbilitie'; // import type { SocialStatuse } from '@/types/SocialStatuse';
// import type { SpecialAbilityGroup } from '../types/SpecialAbilityGroup'; // import type { SpecialAbilitie } from '@/types/SpecialAbilitie';
// import type { SpellEnhancement } from '../types/SpellEnhancement'; // import type { SpecialAbilityGroup } from '@/types/SpecialAbilityGroup';
// import type { SpellGroup } from '../types/SpellGroup'; // import type { SpellEnhancement } from '@/types/SpellEnhancement';
// import type { Spell } from '../types/Spell'; // import type { SpellGroup } from '@/types/SpellGroup';
// import type { State } from '../types/State'; // import type { Spell } from '@/types/Spell';
// import type { Subject } from '../types/Subject'; // import type { State } from '@/types/State';
// import type { Tribe } from '../types/Tribe'; // import type { Subject } from '@/types/Subject';
// import type { ZibiljaRitual } from '../types/ZibiljaRitual'; // import type { Tribe } from '@/types/Tribe';
// import type { ZibiljaRitual } from '@/types/ZibiljaRitual';
/** /**
* Generic function to load data from a JSON file * Generic function to load data from a JSON file
@@ -67,12 +68,11 @@ import type {Skill, Skills} from '../types/Skill';
*/ */
async function loadData<T>(fileName: string): Promise<T[]> { async function loadData<T>(fileName: string): Promise<T[]> {
try { try {
// Ensure proper file extension const response = await fetch(`./database/${fileName}`);
const fileNameWithExt = fileName.endsWith('.json') ? fileName : `${fileName}.json`; if (!response.ok) {
const module = await import(`../assets/database/${fileNameWithExt}`); throw new Error(`Failed to load ${fileName}: ${response.statusText}`);
}
// Access the default export or named export const data = await response.json();
const data = module.default || module;
// Validate it's an array // Validate it's an array
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
@@ -137,6 +137,11 @@ export async function loadRace(id: string): Promise<Race | undefined> {
return getItemById<Race>('Races.json', id); return getItemById<Race>('Races.json', id);
} }
// Race_Variant loader
export async function loadRaceVariant(id: string): Promise<RaceVariant | undefined> {
return getItemById<RaceVariant>('RaceVariants.json', id);
}
// Profession loader // Profession loader
export async function loadProfession(id: string): Promise<Profession | undefined> { export async function loadProfession(id: string): Promise<Profession | undefined> {
return getItemById<Profession>('Professions.json', id); return getItemById<Profession>('Professions.json', id);
@@ -432,9 +437,8 @@ export async function loadSkillWithValue(id: string, value: number): Promise<Ski
} }
// Load multiple skills with values // Load multiple skills with values
export async function loadSkillsWithValues(talents: Skills): Promise<SkillWithValue[]> { export async function loadSkillsWithValues(talents: Record<string, number>): Promise<SkillWithValue[]> {
const skillIds = Object.keys(talents); const skillIds = Object.keys(talents);
// @ts-ignore
const promises = skillIds.map((id) => loadSkillWithValue(id, talents[id])); const promises = skillIds.map((id) => loadSkillWithValue(id, talents[id]));
const skills = await Promise.all(promises); const skills = await Promise.all(promises);

View File

@@ -3,10 +3,13 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022", "target": "ES2022",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@@ -14,14 +17,21 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}, },
"include": ["src"] "include": [
"src"
]
} }

View File

@@ -1,14 +1,26 @@
import {defineConfig} from 'vite' import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import {viteStaticCopy} from "vite-plugin-static-copy";
import path from 'path'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
react(), react(),
tailwindcss(), tailwindcss(),
viteStaticCopy({
targets: [
{src: 'src/assets/database/*.json', dest: 'database'},
],
}),
], ],
base: './', base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
}
},
build: { build: {
minify: 'esbuild', // ← Statt terser minify: 'esbuild', // ← Statt terser
rollupOptions: { rollupOptions: {