Backend: - Characters-Modul (CRUD, HP-Tracking, Conditions) - Pathbuilder 2e JSON Import Service - Claude API Integration für automatische Übersetzungen - Translations-Modul mit Datenbank-Caching - Prisma Schema erweitert (Character, Abilities, Skills, Feats, Items, Resources) Frontend: - Kampagnen-Detailseite mit Mitglieder- und Charakterverwaltung - Charakter erstellen Modal - Pathbuilder Import Modal (Datei-Upload + JSON-Paste) - Logo-Integration (Dimension 47 + Zeasy) - Cinzel Font für Branding Weitere Änderungen: - Auth 401 Redirect Fix für Login-Seite - PROGRESS.md mit Projektfortschritt Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { Button, Input, Spinner } from '@/shared/components/ui';
|
|
import { api } from '@/shared/lib/api';
|
|
|
|
interface CreateCharacterModalProps {
|
|
campaignId: string;
|
|
onClose: () => void;
|
|
onCreated: () => void;
|
|
}
|
|
|
|
export function CreateCharacterModal({ campaignId, onClose, onCreated }: CreateCharacterModalProps) {
|
|
const [name, setName] = useState('');
|
|
const [type, setType] = useState<'PC' | 'NPC'>('PC');
|
|
const [level, setLevel] = useState(1);
|
|
const [hpMax, setHpMax] = useState(20);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!name.trim()) {
|
|
setError('Name ist erforderlich');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
setError('');
|
|
|
|
try {
|
|
await api.createCharacter(campaignId, {
|
|
name: name.trim(),
|
|
type,
|
|
level,
|
|
hpCurrent: hpMax,
|
|
hpMax,
|
|
});
|
|
onCreated();
|
|
onClose();
|
|
} catch (err) {
|
|
setError('Fehler beim Erstellen des Charakters');
|
|
console.error('Failed to create character:', err);
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
<div className="relative bg-bg-primary border border-border rounded-xl p-6 w-full max-w-md mx-4 shadow-xl">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-lg font-semibold text-text-primary">Neuer Charakter</h2>
|
|
<Button variant="ghost" size="icon" onClick={onClose}>
|
|
<X className="h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-primary mb-1.5">
|
|
Name *
|
|
</label>
|
|
<Input
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Name des Charakters"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-primary mb-1.5">
|
|
Typ
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant={type === 'PC' ? 'default' : 'outline'}
|
|
onClick={() => setType('PC')}
|
|
className="flex-1"
|
|
>
|
|
Spielercharakter (PC)
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant={type === 'NPC' ? 'default' : 'outline'}
|
|
onClick={() => setType('NPC')}
|
|
className="flex-1"
|
|
>
|
|
NPC
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-primary mb-1.5">
|
|
Level
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
min={1}
|
|
max={20}
|
|
value={level}
|
|
onChange={(e) => setLevel(parseInt(e.target.value) || 1)}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-primary mb-1.5">
|
|
Max HP
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
min={1}
|
|
value={hpMax}
|
|
onChange={(e) => setHpMax(parseInt(e.target.value) || 1)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<p className="text-sm text-red-500">{error}</p>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3 pt-2">
|
|
<Button type="button" variant="outline" onClick={onClose}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
{isSubmitting ? <Spinner size="sm" /> : 'Erstellen'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|