Add character data structure and Tabs component

This commit is contained in:
2025-07-06 18:09:00 +02:00
parent 0b50aebdd4
commit a910e41588
15 changed files with 728 additions and 10 deletions

View File

@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" />-->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DSA 5e Charakterbogen</title>
</head>

View File

@@ -1,14 +1,38 @@
import './App.css'
import Header from "./modules/header/components/Header.tsx";
import Main from "./modules/main/components/Main.tsx";
import Footer from "./modules/footer/components/Footer.tsx";
import {useEffect, useState} from "react";
import type {CharacterData} from "./types/CharacterJson.ts";
function App() {
const [jsonData, setJsonData] = useState<CharacterData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadData = async () => {
try {
const data = await import('./assets/data/Faelyn Eichenhauch.json');
setJsonData(data.default);
} catch (error) {
console.error('Error loading JSON file:', error);
} finally {
setLoading(false);
}
};
loadData();
}, []);
if (loading) return <div>Loading...</div>;
if (!jsonData) return <div>Error loading JSON file.</div>;
return (
<>
<Header/>
{/*<Header/>*/}
<Main/>
<Main jsonData={jsonData}/>
<Footer/>
</>

View File

@@ -1 +1,284 @@
{"clientVersion":"1.5.2","dateCreated":"2025-06-27T19:06:06.297Z","dateModified":"2025-06-27T21:24:26.013Z","id":"H_1751051166297","phase":3,"locale":"de-DE","name":"Faelyn Eichenhauch","ap":{"total":1400},"el":"EL_5","r":"R_3","c":"C_5","p":"P_24","sex":"m","pers":{"family":"Eichenhauch","placeofbirth":"Fasar","age":"57","haircolor":8,"eyecolor":11,"size":"188","weight":"82","socialstatus":2,"cultureAreaKnowledge":"Fasar / Punin"},"attr":{"values":[{"id":"ATTR_1","value":12},{"id":"ATTR_2","value":14},{"id":"ATTR_3","value":14},{"id":"ATTR_4","value":12},{"id":"ATTR_5","value":10},{"id":"ATTR_6","value":13},{"id":"ATTR_7","value":14},{"id":"ATTR_8","value":11}],"attributeAdjustmentSelected":"ATTR_3","ae":0,"kp":0,"lp":0,"permanentAE":{"lost":0,"redeemed":0},"permanentKP":{"lost":0,"redeemed":0},"permanentLP":{"lost":0}},"activatable":{"ADV_50":[{}],"ADV_48":[],"ADV_11":[],"ADV_70":[{}],"ADV_6":[{"tier":1}],"ADV_13":[{"tier":1}],"ADV_18":[{"sid":4},{"sid":3}],"ADV_37":[{}],"ADV_23":[{"tier":1}],"DISADV_5":[],"DISADV_37":[{"sid":8}],"DISADV_46":[{}],"DISADV_44":[],"DISADV_36":[{"sid":1}],"DISADV_25":[],"DISADV_7":[],"DISADV_48":[],"DISADV_26":[],"SA_27":[{"sid":9},{"sid":14},{"sid":7}],"SA_29":[{"sid":8,"tier":4},{"sid":23,"tier":2},{"sid":1,"tier":2}],"SA_1":[{}],"SA_40":[{}],"SA_9":[{"sid2":2,"sid":"TAL_28"}],"SA_12":[{"sid":9}],"SA_1011":[{}],"SA_22":[{"sid":"Festum"},{"sid":"Waldgebiet westlich von Punin"},{"sid":"Punin"}],"SA_1035":[{}],"SA_70":[{"sid":"SPELL_30"}],"SA_76":[{}],"SA_681":[],"SA_83":[{}]},"talents":{"TAL_4":7,"TAL_8":5,"TAL_10":6,"TAL_20":5,"TAL_23":7,"TAL_27":5,"TAL_34":5,"TAL_36":10,"TAL_40":4,"TAL_41":5,"TAL_42":4,"TAL_47":3,"TAL_54":4,"TAL_16":4,"TAL_44":1,"TAL_18":4,"TAL_19":4,"TAL_32":3,"TAL_33":6,"TAL_46":1,"TAL_37":1,"TAL_38":6,"TAL_39":2,"TAL_11":1,"TAL_28":12,"TAL_3":5,"TAL_24":4,"TAL_30":5,"TAL_29":6,"TAL_50":5,"TAL_51":8,"TAL_48":2},"ct":{"CT_3":11,"CT_13":13},"spells":{"SPELL_6":7,"SPELL_7":5,"SPELL_9":5,"SPELL_20":6,"SPELL_30":5,"SPELL_35":6,"SPELL_40":6,"SPELL_113":6,"SPELL_81":5,"SPELL_43":6},"cantrips":["CANTRIP_1","CANTRIP_46","CANTRIP_4","CANTRIP_17","CANTRIP_34","CANTRIP_35","CANTRIP_38"],"liturgies":{},"blessings":[],"belongings":{"items":{},"armorZones":{},"purse":{"d":"","s":"","h":"","k":""}},"rules":{"higherParadeValues":0,"attributeValueLimit":false,"enableAllRuleBooks":true,"enabledRuleBooks":[],"enableLanguageSpecializations":false},"pets":{}}
{
"clientVersion": "1.5.2",
"dateCreated": "2025-06-27T19:06:06.297Z",
"dateModified": "2025-06-27T21:24:26.013Z",
"id": "H_1751051166297",
"phase": 3,
"locale": "de-DE",
"name": "Faelyn Eichenhauch",
"ap": {
"total": 1400
},
"el": "EL_5",
"r": "R_3",
"c": "C_5",
"p": "P_24",
"sex": "m",
"pers": {
"family": "Eichenhauch",
"placeofbirth": "Fasar",
"age": "57",
"haircolor": 8,
"eyecolor": 11,
"size": "188",
"weight": "82",
"socialstatus": 2,
"cultureAreaKnowledge": "Fasar / Punin"
},
"attr": {
"values": [
{
"id": "ATTR_1",
"value": 12
},
{
"id": "ATTR_2",
"value": 14
},
{
"id": "ATTR_3",
"value": 14
},
{
"id": "ATTR_4",
"value": 12
},
{
"id": "ATTR_5",
"value": 10
},
{
"id": "ATTR_6",
"value": 13
},
{
"id": "ATTR_7",
"value": 14
},
{
"id": "ATTR_8",
"value": 11
}
],
"attributeAdjustmentSelected": "ATTR_3",
"ae": 0,
"kp": 0,
"lp": 0,
"permanentAE": {
"lost": 0,
"redeemed": 0
},
"permanentKP": {
"lost": 0,
"redeemed": 0
},
"permanentLP": {
"lost": 0
}
},
"activatable": {
"ADV_50": [
{}
],
"ADV_48": [],
"ADV_11": [],
"ADV_70": [
{}
],
"ADV_6": [
{
"tier": 1
}
],
"ADV_13": [
{
"tier": 1
}
],
"ADV_18": [
{
"sid": 4
},
{
"sid": 3
}
],
"ADV_37": [
{}
],
"ADV_23": [
{
"tier": 1
}
],
"DISADV_5": [],
"DISADV_37": [
{
"sid": 8
}
],
"DISADV_46": [
{}
],
"DISADV_44": [],
"DISADV_36": [
{
"sid": 1
}
],
"DISADV_25": [],
"DISADV_7": [],
"DISADV_48": [],
"DISADV_26": [],
"SA_27": [
{
"sid": 9
},
{
"sid": 14
},
{
"sid": 7
}
],
"SA_29": [
{
"sid": 8,
"tier": 4
},
{
"sid": 23,
"tier": 2
},
{
"sid": 1,
"tier": 2
}
],
"SA_1": [
{}
],
"SA_40": [
{}
],
"SA_9": [
{
"sid2": 2,
"sid": "TAL_28"
}
],
"SA_12": [
{
"sid": 9
}
],
"SA_1011": [
{}
],
"SA_22": [
{
"sid": "Festum"
},
{
"sid": "Waldgebiet westlich von Punin"
},
{
"sid": "Punin"
}
],
"SA_1035": [
{}
],
"SA_70": [
{
"sid": "SPELL_30"
}
],
"SA_76": [
{}
],
"SA_681": [],
"SA_83": [
{}
]
},
"talents": {
"TAL_4": 7,
"TAL_8": 5,
"TAL_10": 6,
"TAL_20": 5,
"TAL_23": 7,
"TAL_27": 5,
"TAL_34": 5,
"TAL_36": 10,
"TAL_40": 4,
"TAL_41": 5,
"TAL_42": 4,
"TAL_47": 3,
"TAL_54": 4,
"TAL_16": 4,
"TAL_44": 1,
"TAL_18": 4,
"TAL_19": 4,
"TAL_32": 3,
"TAL_33": 6,
"TAL_46": 1,
"TAL_37": 1,
"TAL_38": 6,
"TAL_39": 2,
"TAL_11": 1,
"TAL_28": 12,
"TAL_3": 5,
"TAL_24": 4,
"TAL_30": 5,
"TAL_29": 6,
"TAL_50": 5,
"TAL_51": 8,
"TAL_48": 2
},
"ct": {
"CT_3": 11,
"CT_13": 13
},
"spells": {
"SPELL_6": 7,
"SPELL_7": 5,
"SPELL_9": 5,
"SPELL_20": 6,
"SPELL_30": 5,
"SPELL_35": 6,
"SPELL_40": 6,
"SPELL_113": 6,
"SPELL_81": 5,
"SPELL_43": 6
},
"cantrips": [
"CANTRIP_1",
"CANTRIP_46",
"CANTRIP_4",
"CANTRIP_17",
"CANTRIP_34",
"CANTRIP_35",
"CANTRIP_38"
],
"liturgies": {},
"blessings": [],
"belongings": {
"items": {},
"armorZones": {},
"purse": {
"d": "",
"s": "",
"h": "",
"k": ""
}
},
"rules": {
"higherParadeValues": 0,
"attributeValueLimit": false,
"enableAllRuleBooks": true,
"enabledRuleBooks": [],
"enableLanguageSpecializations": false
},
"pets": {}
}

View File

@@ -1,10 +1,13 @@
export default function Main() {
import Tabs from "./Tabs";
import SheetHeader from "../modules/sheetHeader/components/SheetHeader.tsx";
import type {CharacterData} from "../../../types/CharacterJson.ts";
export default function Main({jsonData}: { jsonData: CharacterData }) {
return (
<main>
<div className="container mx-auto">
<h1 className="text-center text-3xl font-bold">
Welcome to the React App
</h1>
<div className="mx-auto">
<SheetHeader jsonData={jsonData}/>
<Tabs/>
</div>
</main>
)

View File

@@ -0,0 +1,150 @@
import {useState} from "react";
import Skills from "../modules/skills/components/Skills.tsx";
import Combat from "../modules/combat/components/Combat.tsx";
import Equipment from "../modules/equipment/components/Equipment.tsx";
import Attributes from "../modules/attributes/components/Attributes.tsx";
import State from "../modules/state/components/State.tsx";
import Magic from "../modules/magic/components/Magic.tsx";
import Religion from "../modules/religion/components/Religion.tsx";
import Notes from "../modules/notes/components/Notes.tsx";
export default function Tabs() {
const [activeTab, setActiveTab] = useState("skills");
const handleTabClick = (tab: string) => {
setActiveTab(tab);
};
return (
<div className="w-full mt-4">
<div className="flex justify-evenly flex-wrap gap-2 border-t border-gray-400 py-5">
<button
className={`px-4 py-2 bg-transparent w-[20%] ${
activeTab === "skills"
? "text-violet-500"
: "text-gray-500"
}`}
onClick={() => handleTabClick("skills")}
>
Skills
</button>
<button
className={`px-4 py-2 bg-transparent w-[20%] ${
activeTab === "combat"
? "text-violet-500"
: "text-gray-500"
}`}
onClick={() => handleTabClick("combat")}
>
Combat
</button>
<button
className={`px-4 py-2 bg-transparent w-[20%] ${
activeTab === "attributes"
? "text-violet-500"
: "text-gray-500"
}`}
onClick={() => handleTabClick("attributes")}
>
Attributes
</button>
<button
className={`px-4 py-2 bg-transparent w-[20%] ${
activeTab === "equipment"
? "text-violet-500"
: "text-gray-500"
}`}
onClick={() => handleTabClick("equipment")}
>
Equipment
</button>
{/* Break flex row */}
<div className="basis-full"></div>
<button
className={`px-4 py-2 bg-transparent w-[20%] ${
activeTab === "state"
? "text-violet-500"
: "text-gray-500"
}`}
onClick={() => handleTabClick("state")}
>
State
</button>
<button
className={`px-4 py-2 bg-transparent w-[20%] ${
activeTab === "magic"
? "text-violet-500"
: "text-gray-500"
}`}
onClick={() => handleTabClick("magic")}
>
Magic
</button>
<button
className={`px-4 py-2 bg-transparent w-[20%] ${
activeTab === "religion"
? "text-violet-500"
: "text-gray-500"
}`}
onClick={() => handleTabClick("religion")}
>
Religion
</button>
<button
className={`px-4 py-2 bg-transparent w-[20%] ${
activeTab === "notes"
? "text-violet-500"
: "text-gray-500"
}`}
onClick={() => handleTabClick("notes")}
>
Notes
</button>
</div>
<div className="p-4">
{activeTab === "skills" && (
<div>
<Skills/>
</div>
)}
{activeTab === "combat" && (
<div>
<Combat/>
</div>
)}
{activeTab === "attributes" && (
<div>
<Attributes/>
</div>
)}
{activeTab === "equipment" && (
<div>
<Equipment/>
</div>
)}
{activeTab === "state" && (
<div>
<State/>
</div>
)}
{activeTab === "magic" && (
<div>
<Magic/>
</div>
)}
{activeTab === "religion" && (
<div>
<Religion/>
</div>
)}
{activeTab === "notes" && (
<div>
<Notes/>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,5 @@
export default function Attributes() {
return (
<h1>Attributes</h1>
)
}

View File

@@ -0,0 +1,5 @@
export default function Combat() {
return (
<h1>Combat</h1>
)
}

View File

@@ -0,0 +1,5 @@
export default function Equipment() {
return (
<h1>Equipment</h1>
)
}

View File

@@ -0,0 +1,5 @@
export default function Magic() {
return (
<h1>Magic</h1>
)
}

View File

@@ -0,0 +1,5 @@
export default function Notes() {
return (
<h1>Notes</h1>
)
}

View File

@@ -0,0 +1,5 @@
export default function Religion() {
return (
<h1>Religion</h1>
)
}

View File

@@ -0,0 +1,110 @@
import type {CharacterData} from "../../../../../types/CharacterJson.ts";
export default function SheetHeader({jsonData}: { jsonData: CharacterData }) {
console.log(jsonData)
return (
<>
<div className="flex justify-between items-center py-5 px-5">
<div className="w-[30%]">
<p className="border-b border-gray-500 font-semibold">
{ jsonData.name}
</p>
<small>Name</small>
</div>
<div className="w-[30%]">
<p className="border-b border-gray-500 font-semibold">
{jsonData.r}
</p>
<small>Spezies</small>
</div>
<div className="w-[30%]">
<p className="border-b border-gray-500 font-semibold">
{jsonData.p}
</p>
<small>Profession</small>
</div>
</div>
<div className="flex justify-between items-center py-3 px-10">
<div className="w-[20%] h-[20%] rounded-full overflow-hidden">
<img src="#" alt="char bild"/>
</div>
<div className="flex flex-col gap-2 w-[80%]">
<div className="flex justify-between">
<div>
<p>COU</p>
<p>15</p>
</div>
<div>
<p>SGC</p>
<p>15</p>
</div>
<div>
<p>INT</p>
<p>15</p>
</div>
<div>
<p>CHA</p>
<p>15</p>
</div>
<div>
<p>DEX</p>
<p>15</p>
</div>
<div>
<p>AGI</p>
<p>15</p>
</div>
<div>
<p>CON</p>
<p>15</p>
</div>
<div>
<p>STR</p>
<p>15</p>
</div>
</div>
<div className="w-[100%] flex pt-5">
<span className="relative group cursor-help">
LP
<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">
Lebenspunkte
</span>
</span>
<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">
15 / 15
</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 className="w-[100%] flex pt-5">
<span className="w-[10%] 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="w-[90%] flex items-center gap-2">
<div className="rounded-full w-5 h-5 bg-violet-400"></div>
<div className="rounded-full w-5 h-5 bg-violet-400"></div>
<div className="rounded-full w-5 h-5 bg-violet-400"></div>
</div>
</div>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,5 @@
export default function Skills() {
return (
<h1>Skills</h1>
)
}

View File

@@ -0,0 +1,5 @@
export default function State() {
return (
<h1>State</h1>
)
}

109
src/types/CharacterJson.ts Normal file
View File

@@ -0,0 +1,109 @@
export interface AttributeValue {
id: string;
value: number;
}
export interface PermanentAttribute {
lost: number;
redeemed?: number;
}
export interface Attributes {
values: AttributeValue[];
attributeAdjustmentSelected: string;
ae: number;
kp: number;
lp: number;
permanentAE: PermanentAttribute;
permanentKP: PermanentAttribute;
permanentLP: {
lost: number;
};
}
export interface PersonalDetails {
family: string;
placeofbirth: string;
age: string;
haircolor: number;
eyecolor: number;
size: string;
weight: string;
socialstatus: number;
cultureAreaKnowledge: string;
}
export interface AdventurePoints {
total: number;
}
export interface ActivatableItem {
sid?: number | string;
sid2?: number;
tier?: number;
}
export interface Activatables {
[key: string]: ActivatableItem[];
}
export interface Talents {
[key: string]: number;
}
export interface CombatTechniques {
[key: string]: number;
}
export interface Spells {
[key: string]: number;
}
export interface Purse {
d: string; // Ducat
s: string; // Silver
h: string; // Heller
k: string; // Kreuzer
}
export interface Belongings {
items: Record<string, any>;
armorZones: Record<string, any>;
purse: Purse;
}
export interface Rules {
higherParadeValues: number;
attributeValueLimit: boolean;
enableAllRuleBooks: boolean;
enabledRuleBooks: string[];
enableLanguageSpecializations: boolean;
}
export interface CharacterData {
clientVersion: string;
dateCreated: string;
dateModified: string;
id: string;
phase: number;
locale: string;
name: string;
ap: AdventurePoints;
el: string; // Experience Level
r: string; // Race
c: string; // Culture
p: string; // Profession
sex: string;
pers: PersonalDetails;
attr: Attributes;
activatable: Activatables;
talents: Talents;
ct: CombatTechniques;
spells: Spells;
cantrips: string[];
liturgies: Record<string, any>;
blessings: string[];
belongings: Belongings;
rules: Rules;
pets: Record<string, any>;
}