- NestJS backend with JWT auth, Prisma ORM, Swagger docs - Vite + React 19 frontend with TypeScript - Tailwind CSS v4 with custom dark theme design system - Auth module: Login, Register, Protected routes - Campaigns module: CRUD, Member management - Full Prisma schema for PF2e campaign management - Docker Compose for PostgreSQL Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { X } from 'lucide-react';
|
|
import { Button, Input, Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/shared/components/ui';
|
|
import { api } from '@/shared/lib/api';
|
|
|
|
interface CreateCampaignModalProps {
|
|
onClose: () => void;
|
|
onCreated: () => void;
|
|
}
|
|
|
|
export function CreateCampaignModal({ onClose, onCreated }: CreateCampaignModalProps) {
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
if (!name.trim()) {
|
|
setError('Bitte einen Namen eingeben');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
try {
|
|
await api.createCampaign({
|
|
name: name.trim(),
|
|
description: description.trim() || undefined,
|
|
});
|
|
onCreated();
|
|
} catch (err) {
|
|
console.error('Failed to create campaign:', err);
|
|
setError('Kampagne konnte nicht erstellt werden');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* Modal */}
|
|
<Card className="relative z-10 w-full max-w-md mx-4 animate-slide-up">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>Neue Kampagne</CardTitle>
|
|
<button
|
|
onClick={onClose}
|
|
className="h-8 w-8 rounded-lg hover:bg-bg-tertiary flex items-center justify-center transition-colors"
|
|
>
|
|
<X className="h-4 w-4 text-text-secondary" />
|
|
</button>
|
|
</CardHeader>
|
|
<form onSubmit={handleSubmit}>
|
|
<CardContent className="space-y-4">
|
|
{error && (
|
|
<div className="p-3 rounded-lg bg-error-500/10 border border-error-500/20 text-error-500 text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<Input
|
|
label="Name"
|
|
type="text"
|
|
placeholder="z.B. Rise of the Runelords"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
disabled={isLoading}
|
|
autoFocus
|
|
/>
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-secondary mb-1.5">
|
|
Beschreibung (optional)
|
|
</label>
|
|
<textarea
|
|
className="flex min-h-[100px] w-full rounded-lg border border-border bg-bg-secondary px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-none"
|
|
placeholder="Eine kurze Beschreibung der Kampagne..."
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="gap-3">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={onClose}
|
|
disabled={isLoading}
|
|
className="flex-1"
|
|
>
|
|
Abbrechen
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
isLoading={isLoading}
|
|
className="flex-1"
|
|
>
|
|
Erstellen
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|