feat(web): implement weapons section with add/edit/detail modals and notes
Adds WeaponCard with inline ammo stepper and status dropdown, AddWeaponModal, WeaponDetailModal with full editing, and debounced NotesSection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
100
web/src/components/weapons/AddWeaponModal.tsx
Normal file
100
web/src/components/weapons/AddWeaponModal.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useState } from 'react';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onCreate: (weapon: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
export default function AddWeaponModal({ open, onClose, onCreate }: Props) {
|
||||
const [name, setName] = useState('');
|
||||
const [damage, setDamage] = useState('');
|
||||
const [attackMod, setAttackMod] = useState('');
|
||||
const [range, setRange] = useState('');
|
||||
const [ammoMax, setAmmoMax] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
|
||||
const aMax = parseInt(ammoMax) || null;
|
||||
onCreate({
|
||||
name: name.trim(),
|
||||
damage: damage.trim() || null,
|
||||
attack_mod: parseInt(attackMod) || null,
|
||||
range: range.trim() || null,
|
||||
ammo_max: aMax,
|
||||
ammo_current: aMax,
|
||||
status: 'ok',
|
||||
});
|
||||
|
||||
setName('');
|
||||
setDamage('');
|
||||
setAttackMod('');
|
||||
setRange('');
|
||||
setAmmoMax('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} title="Add Weapon">
|
||||
<form onSubmit={handleSubmit} className="modal-form">
|
||||
<label className="form-label">
|
||||
Name *
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g. Ballista"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="form-label">
|
||||
Damage
|
||||
<input
|
||||
type="text"
|
||||
value={damage}
|
||||
onChange={(e) => setDamage(e.target.value)}
|
||||
placeholder="e.g. 3d10"
|
||||
/>
|
||||
</label>
|
||||
<label className="form-label">
|
||||
Attack Modifier
|
||||
<input
|
||||
type="number"
|
||||
value={attackMod}
|
||||
onChange={(e) => setAttackMod(e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-label">
|
||||
Range
|
||||
<input
|
||||
type="text"
|
||||
value={range}
|
||||
onChange={(e) => setRange(e.target.value)}
|
||||
placeholder="e.g. 120/480"
|
||||
/>
|
||||
</label>
|
||||
<label className="form-label">
|
||||
Ammo Max (leave empty for unlimited)
|
||||
<input
|
||||
type="number"
|
||||
value={ammoMax}
|
||||
onChange={(e) => setAmmoMax(e.target.value)}
|
||||
min="0"
|
||||
/>
|
||||
</label>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn-secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn-primary" disabled={!name.trim()}>
|
||||
Add Weapon
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user