diff --git a/client/src/App.tsx b/client/src/App.tsx index 49e954d..d6cf4c4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,6 +5,7 @@ import { LoginPage, RegisterPage, useAuthStore } from '@/features/auth'; import { CampaignsPage, CampaignDetailPage } from '@/features/campaigns'; import { CharacterSheetPage } from '@/features/characters'; import { BattlePage } from '@/features/battle'; +import { LibraryPage } from '@/features/library'; import { ProtectedRoute } from '@/shared/components/protected-route'; import { Layout } from '@/shared/components/layout'; @@ -48,7 +49,7 @@ function AppContent() { } /> } /> } /> - Library (TODO)} /> + } /> diff --git a/client/src/features/campaigns/components/campaign-detail-page.tsx b/client/src/features/campaigns/components/campaign-detail-page.tsx index bfeee1e..237d77a 100644 --- a/client/src/features/campaigns/components/campaign-detail-page.tsx +++ b/client/src/features/campaigns/components/campaign-detail-page.tsx @@ -10,7 +10,8 @@ import { Crown, Shield, Heart, - FileJson + FileJson, + Library } from 'lucide-react'; import { Button, @@ -268,7 +269,7 @@ export function CampaignDetailPage() { {/* Quick Actions */} -
+
navigate(`/campaigns/${id}/battle`)}>
@@ -280,6 +281,19 @@ export function CampaignDetailPage() {
+ {canManage && ( + navigate(`/campaigns/${id}/library`)}> +
+
+ +
+
+

Bibliothek

+

Karten & NPCs

+
+
+
+ )}
diff --git a/client/src/features/library/components/create-combatant-modal.tsx b/client/src/features/library/components/create-combatant-modal.tsx new file mode 100644 index 0000000..c22ca9d --- /dev/null +++ b/client/src/features/library/components/create-combatant-modal.tsx @@ -0,0 +1,390 @@ +import { useState } from 'react'; +import { X, Plus, Trash2 } from 'lucide-react'; +import { cn } from '@/shared/lib/utils'; + +interface CombatantAbility { + name: string; + actionCost: number; + actionType: 'ACTION' | 'REACTION' | 'FREE'; + description: string; + damage?: string; + traits?: string[]; +} + +interface CreateCombatantModalProps { + onClose: () => void; + onCreate: (data: { + name: string; + type: 'NPC' | 'MONSTER'; + level: number; + hpMax: number; + ac: number; + fortitude: number; + reflex: number; + will: number; + perception: number; + speed: number; + description?: string; + abilities: CombatantAbility[]; + }) => void; + isLoading?: boolean; +} + +export function CreateCombatantModal({ onClose, onCreate, isLoading }: CreateCombatantModalProps) { + const [name, setName] = useState(''); + const [type, setType] = useState<'NPC' | 'MONSTER'>('NPC'); + const [level, setLevel] = useState(1); + const [hpMax, setHpMax] = useState(20); + const [ac, setAc] = useState(15); + const [fortitude, setFortitude] = useState(5); + const [reflex, setReflex] = useState(5); + const [will, setWill] = useState(5); + const [perception, setPerception] = useState(5); + const [speed, setSpeed] = useState(25); + const [description, setDescription] = useState(''); + const [abilities, setAbilities] = useState([]); + const [showAddAbility, setShowAddAbility] = useState(false); + + // Ability form state + const [abilityName, setAbilityName] = useState(''); + const [abilityActionCost, setAbilityActionCost] = useState(1); + const [abilityActionType, setAbilityActionType] = useState<'ACTION' | 'REACTION' | 'FREE'>('ACTION'); + const [abilityDescription, setAbilityDescription] = useState(''); + const [abilityDamage, setAbilityDamage] = useState(''); + + const handleAddAbility = () => { + if (!abilityName.trim() || !abilityDescription.trim()) return; + + setAbilities([ + ...abilities, + { + name: abilityName.trim(), + actionCost: abilityActionCost, + actionType: abilityActionType, + description: abilityDescription.trim(), + damage: abilityDamage.trim() || undefined, + }, + ]); + + // Reset ability form + setAbilityName(''); + setAbilityActionCost(1); + setAbilityActionType('ACTION'); + setAbilityDescription(''); + setAbilityDamage(''); + setShowAddAbility(false); + }; + + const handleRemoveAbility = (index: number) => { + setAbilities(abilities.filter((_, i) => i !== index)); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!name.trim()) return; + + onCreate({ + name: name.trim(), + type, + level, + hpMax, + ac, + fortitude, + reflex, + will, + perception, + speed, + description: description.trim() || undefined, + abilities, + }); + }; + + return ( +
+
+ {/* Header */} +
+

NPC/Monster erstellen

+ +
+ + {/* Form */} +
+ {/* Basic Info */} +
+
+ + setName(e.target.value)} + placeholder="z.B. Goblin Krieger" + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary placeholder:text-text-secondary focus:outline-none focus:border-primary" + required + /> +
+
+ + +
+
+ + {/* Stats Row 1 */} +
+
+ + setLevel(parseInt(e.target.value) || 0)} + min={-1} + max={30} + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-primary" + /> +
+
+ + setHpMax(parseInt(e.target.value) || 1)} + min={1} + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-primary" + /> +
+
+ + setAc(parseInt(e.target.value) || 0)} + min={0} + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-primary" + /> +
+
+ + {/* Saves */} +
+
+ + setFortitude(parseInt(e.target.value) || 0)} + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-primary" + /> +
+
+ + setReflex(parseInt(e.target.value) || 0)} + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-primary" + /> +
+
+ + setWill(parseInt(e.target.value) || 0)} + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-primary" + /> +
+
+ + {/* Perception & Speed */} +
+
+ + setPerception(parseInt(e.target.value) || 0)} + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-primary" + /> +
+
+ + setSpeed(parseInt(e.target.value) || 0)} + min={0} + step={5} + className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded-lg text-text-primary focus:outline-none focus:border-primary" + /> +
+
+ + {/* Description */} +
+ +