196 lines
6.5 KiB
TypeScript
196 lines
6.5 KiB
TypeScript
import { useRef, useEffect } from "react";
|
|
import type { Character } from "../types";
|
|
import TalentList from "./TalentList";
|
|
import styles from "./InfoPanel.module.css";
|
|
|
|
const CLASSES = ["Fighter", "Priest", "Thief", "Wizard"];
|
|
const ANCESTRIES = ["Human", "Elf", "Dwarf", "Halfling", "Goblin", "Half-Orc"];
|
|
const ALIGNMENTS = ["Lawful", "Neutral", "Chaotic"];
|
|
|
|
interface InfoPanelProps {
|
|
character: Character;
|
|
mode: "view" | "edit";
|
|
onUpdate: (id: number, data: Partial<Character>) => void;
|
|
onAddTalent: (
|
|
characterId: number,
|
|
data: {
|
|
name: string;
|
|
description: string;
|
|
effect?: Record<string, unknown>;
|
|
game_talent_id?: number | null;
|
|
},
|
|
) => void;
|
|
onRemoveTalent: (characterId: number, talentId: number) => void;
|
|
}
|
|
|
|
export default function InfoPanel({
|
|
character,
|
|
mode,
|
|
onUpdate,
|
|
onAddTalent,
|
|
onRemoveTalent,
|
|
}: InfoPanelProps) {
|
|
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
};
|
|
}, []);
|
|
|
|
function handleField(field: string, value: string | number) {
|
|
if (typeof value === "string") {
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
debounceRef.current = setTimeout(() => {
|
|
onUpdate(character.id, { [field]: value });
|
|
}, 400);
|
|
} else {
|
|
onUpdate(character.id, { [field]: value });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={styles.panel}>
|
|
<TalentList
|
|
talents={character.talents}
|
|
onAdd={(data) => onAddTalent(character.id, data)}
|
|
onRemove={(id) => onRemoveTalent(character.id, id)}
|
|
mode={mode}
|
|
/>
|
|
|
|
<div className={styles.sectionTitle} style={{ marginTop: "0.75rem" }}>
|
|
Info
|
|
</div>
|
|
|
|
{mode === "view" ? (
|
|
<div className={styles.infoGrid}>
|
|
{character.background && (
|
|
<div className={styles.infoRow}>
|
|
<span className={styles.infoLabel}>Background</span>
|
|
<span className={styles.infoValue}>{character.background}</span>
|
|
</div>
|
|
)}
|
|
{character.deity && (
|
|
<div className={styles.infoRow}>
|
|
<span className={styles.infoLabel}>Deity</span>
|
|
<span className={styles.infoValue}>{character.deity}</span>
|
|
</div>
|
|
)}
|
|
{character.languages && (
|
|
<div className={styles.infoRow}>
|
|
<span className={styles.infoLabel}>Languages</span>
|
|
<span className={styles.infoValue}>{character.languages}</span>
|
|
</div>
|
|
)}
|
|
<div className={styles.infoRow}>
|
|
<span className={styles.infoLabel}>Alignment</span>
|
|
<span className={styles.infoValue}>{character.alignment}</span>
|
|
</div>
|
|
{character.notes && (
|
|
<>
|
|
<div className={styles.infoRow}>
|
|
<span className={styles.infoLabel}>Notes</span>
|
|
</div>
|
|
<div className={styles.notes}>{character.notes}</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className={styles.infoGrid}>
|
|
<div className={styles.editRow}>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>Class</label>
|
|
<select
|
|
className={styles.editSelect}
|
|
value={character.class}
|
|
onChange={(e) => handleField("class", e.target.value)}
|
|
>
|
|
{CLASSES.map((c) => (
|
|
<option key={c} value={c}>
|
|
{c}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>Ancestry</label>
|
|
<select
|
|
className={styles.editSelect}
|
|
value={character.ancestry}
|
|
onChange={(e) => handleField("ancestry", e.target.value)}
|
|
>
|
|
{ANCESTRIES.map((a) => (
|
|
<option key={a} value={a}>
|
|
{a}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className={styles.editRow}>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>Level</label>
|
|
<input
|
|
className={styles.editField}
|
|
type="number"
|
|
min={0}
|
|
value={character.level}
|
|
onChange={(e) => handleField("level", Number(e.target.value))}
|
|
/>
|
|
</div>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>Alignment</label>
|
|
<select
|
|
className={styles.editSelect}
|
|
value={character.alignment}
|
|
onChange={(e) => handleField("alignment", e.target.value)}
|
|
>
|
|
{ALIGNMENTS.map((a) => (
|
|
<option key={a} value={a}>
|
|
{a}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>Background</label>
|
|
<input
|
|
className={styles.editField}
|
|
value={character.background}
|
|
placeholder="Urchin..."
|
|
onChange={(e) => handleField("background", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>Deity</label>
|
|
<input
|
|
className={styles.editField}
|
|
value={character.deity}
|
|
placeholder="None..."
|
|
onChange={(e) => handleField("deity", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>Languages</label>
|
|
<input
|
|
className={styles.editField}
|
|
value={character.languages}
|
|
placeholder="Common, Elvish..."
|
|
onChange={(e) => handleField("languages", e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className={styles.field}>
|
|
<label className={styles.fieldLabel}>Notes</label>
|
|
<textarea
|
|
className={styles.notesEdit}
|
|
value={character.notes}
|
|
onChange={(e) => handleField("notes", e.target.value)}
|
|
placeholder="Freeform notes..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|