Enhance SheetHeader component: add error handling, implement loading states, support conditional rendering for attributes, and improve state management with useCallback.

This commit is contained in:
2025-07-12 20:13:48 +02:00
parent 44f60270eb
commit 2a1d0a470f
3 changed files with 168 additions and 97 deletions

View File

@@ -61,19 +61,23 @@
} }
], ],
"attributeAdjustmentSelected": "ATTR_3", "attributeAdjustmentSelected": "ATTR_3",
"ae": 0, "ae": 15,
"kp": 0, "kp": 15,
"lp": 0, "lp": 15,
"sp": 2,
"permanentAE": { "permanentAE": {
"lost": 0, "lost": 3,
"redeemed": 0 "redeemed": 0
}, },
"permanentKP": { "permanentKP": {
"lost": 0, "lost": 2,
"redeemed": 0 "redeemed": 0
}, },
"permanentLP": { "permanentLP": {
"lost": 0 "lost": 12
},
"permanentSP": {
"lost": 1
} }
}, },
"activatable": { "activatable": {

View File

@@ -6,58 +6,90 @@ import {
loadRace, loadRace,
loadRaceVariant loadRaceVariant
} from "@/utils/loaders"; } from "@/utils/loaders";
import {useEffect, useState} from 'react'; 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 [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);
const [error, setError] = useState<string | null>(null);
const resetToDefaults = useCallback(() => {
setRaceName(jsonData.r);
setRaceVariantName(jsonData.rv || '');
setProfessionName(jsonData.p);
setAttributes([]);
}, [jsonData.r, jsonData.rv, jsonData.p]);
const loadData = useCallback(async (signal: AbortSignal) => {
setLoading(true);
setError(null);
try {
// Load race and profession using the new loader functions
const [race, raceVariant, profession, loadedAttributes] = await Promise.all([
loadRace(jsonData.r),
loadRaceVariant(jsonData.rv || ''),
loadProfession(jsonData.p),
loadAttributesWithValues(jsonData.attr.values),
]);
// Check if component is still mounted and request wasn't cancelled
if (signal.aborted) return;
// Update state with loaded data
setRaceName(race?.name || jsonData.r);
setRaceVariantName(raceVariant?.name || '');
// Handle profession name with gender preference
if (profession?.name) {
setProfessionName(profession.name.m || profession.name.f || jsonData.p);
} else {
setProfessionName(jsonData.p);
}
setAttributes(loadedAttributes);
} 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(() => { useEffect(() => {
let isMounted = true; const abortController = new AbortController();
const loadData = async () => { loadData(abortController.signal);
try {
// Load race and profession using the new loader functions
const [race, raceVariant, profession] = await Promise.all([
loadRace(jsonData.r),
loadRaceVariant(jsonData.rv || ''),
loadProfession(jsonData.p)
]);
// Process attributes using the new loadAttributesWithValues function
const loadedAttributes = await loadAttributesWithValues(jsonData.attr.values);
if (isMounted) {
setRaceName(race?.name || jsonData.r);
setRaceVariantName(raceVariant?.name || '');
// For profession, handle gendered name
if (profession?.name) {
setProfessionName(profession.name.m || profession.name.f || jsonData.p);
} else {
setProfessionName(jsonData.p);
}
// Set the attributes with their values, names, and short forms
setAttributes(loadedAttributes);
}
} catch (error) {
console.error('Error loading data:', error);
if (isMounted) {
setRaceName(jsonData.r);
setProfessionName(jsonData.p);
}
}
};
loadData();
return () => { return () => {
isMounted = false; abortController.abort();
}; };
}, [jsonData.r, jsonData.rv, 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>
);
}
return ( return (
<> <>
@@ -66,19 +98,19 @@ export default function SheetHeader({jsonData}: { jsonData: CharacterData }) {
<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} {raceVariantName ? <>({raceVariantName})</> : ''} {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>
@@ -103,7 +135,8 @@ export default function SheetHeader({jsonData}: { jsonData: CharacterData }) {
))} ))}
</div> </div>
<div className="w-[100%] flex pt-2"> {jsonData.attr.lp && (
<div className="w-[100%] flex pt-2">
<span className="relative group cursor-help"> <span className="relative group cursor-help">
LP LP
<span <span
@@ -111,54 +144,88 @@ export default function SheetHeader({jsonData}: { jsonData: CharacterData }) {
Lebenspunkte Lebenspunkte
</span> </span>
</span> </span>
<div <div
className="ms-5 w-[100%] bg-gradient-to-r from-red-900 to-red-600 rounded-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
</div> className="bg-gradient-to-r from-red-900 to-red-600 h-full transition-all duration-300"
</div> style={{width: `${Math.min(((jsonData.attr.lp - jsonData.attr.permanentLP.lost) / jsonData.attr.lp) * 100, 100)}%`}}
/>
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
{jsonData.attr.lp} / {jsonData.attr.lp - jsonData.attr.permanentLP.lost}
</div>
</div>
</div>
)}
<div className="w-[100%] flex pt-2"> {jsonData.attr.ae > 0 && (
<span className="relative group cursor-help"> <div className="w-[100%] flex pt-2">
AP <span className="relative group cursor-help">
<span AP
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"> <span
Astralpunkte 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">
</span> Astralpunkte
</span> </span>
<div </span>
className="ms-5 w-[100%] bg-gradient-to-r from-blue-900 to-blue-500 rounded-full h-6 flex items-center justify-center text-white text-sm font-medium"> <div
16 / 16 className="ms-5 w-[100%] bg-gray-600 rounded-full h-6 flex items-center overflow-hidden relative">
</div> <div
</div> className="bg-gradient-to-r from-blue-900 to-blue-500 h-full transition-all duration-300"
style={{width: `${Math.min(((jsonData.attr.ae - jsonData.attr.permanentAE.lost) / jsonData.attr.ae) * 100, 100)}%`}}
/>
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
{jsonData.attr.ae} / {jsonData.attr.ae - jsonData.attr.permanentAE.lost}
</div>
</div>
</div>
)}
<div className="w-[100%] flex pt-2"> {jsonData.attr.kp > 0 && (
<span className="relative group cursor-help"> <div className="w-[100%] flex pt-2">
KP <span className="relative group cursor-help">
<span KP
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"> <span
Karmapunkte 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">
</span> Karmapunkte
</span> </span>
<div </span>
className="ms-5 w-[100%] bg-gradient-to-r from-emerald-900 to-emerald-500 rounded-full h-6 flex items-center justify-center text-white text-sm font-medium"> <div
16 / 16 className="ms-5 w-[100%] bg-gray-600 rounded-full h-6 flex items-center overflow-hidden relative">
</div> <div
</div> className="bg-gradient-to-r from-emerald-900 to-emerald-500 h-full transition-all duration-300"
style={{width: `${Math.min(((jsonData.attr.kp - jsonData.attr.permanentKP.lost) / jsonData.attr.kp) * 100, 100)}%`}}
/>
<div className="absolute inset-0 flex items-center justify-center text-white text-sm font-medium">
{jsonData.attr.kp} / {jsonData.attr.kp - jsonData.attr.permanentKP.lost}
</div>
</div>
</div>
)}
<div className="w-[100%] flex pt-2"> {jsonData.attr.sp > 0 && (
<span className="w-[10%] relative group cursor-help"> <div className="w-[100%] flex pt-2">
SP <span className="relative group cursor-help">
<span SP
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"> <span
Schicksalspunkte 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">
</span> Schicksalspunkte
</span> </span>
<div className="w-[90%] flex items-center gap-2"> </span>
<div className="rounded-full w-5 h-5 bg-gradient-to-br from-violet-700 to-violet-500"></div> <div className="ms-5 w-[100%] flex items-center gap-2">
<div className="rounded-full w-5 h-5 bg-gradient-to-br from-violet-700 to-violet-500"></div> {Array.from({length: jsonData.attr.sp - jsonData.attr.permanentSP.lost}, (_, index) => (
<div className="rounded-full w-5 h-5 bg-gradient-to-br from-violet-700 to-violet-500"></div> <div
</div> key={`sp-available-${index}`}
</div> className="rounded-full w-5 h-5 bg-gradient-to-br from-violet-700 to-violet-500"
/>
))}
{Array.from({length: jsonData.attr.permanentSP.lost}, (_, index) => (
<div
key={`sp-lost-${index}`}
className="rounded-full w-5 h-5 bg-gradient-to-br from-gray-700 to-gray-500"
/>
))}
</div>
</div>
)}
</div> </div>
</div> </div>
</> </>

View File

@@ -14,11 +14,11 @@ 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; permanentSP: PermanentAttribute;
};
} }
export interface PersonalDetails { export interface PersonalDetails {