diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2acf542 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npx tsc:*)", + "Bash(npm install:*)" + ] + } +} diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..d08c968 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,140 @@ +# Dimension 47 - Projektfortschritt + +## Abgeschlossen + +### Backend (NestJS + Prisma 7) + +- **Auth-Modul** + - JWT-basierte Authentifizierung + - Login/Register Endpoints + - Benutzerprofile mit Avatar + - Benutzersuche + - Globale Auth Guards + +- **Kampagnen-Modul** + - CRUD-Operationen für Kampagnen + - Mitgliederverwaltung (hinzufügen/entfernen) + - GM-Berechtigungen + +- **Charaktere-Modul** + - CRUD-Operationen für Charaktere (PC/NPC) + - HP-Tracking (aktuell, max, temporär) + - Zustände (Conditions) verwalten + - Pathbuilder 2e Import mit automatischer Übersetzung + - Abilities, Skills, Feats, Items, Resources Import + +- **Übersetzungs-Modul** + - Claude API Integration für Deutsch-Übersetzungen + - Datenbank-Caching für Übersetzungen + - Batch-Übersetzung für Performance + +- **Prisma Schema** + - User, Campaign, CampaignMember + - Character mit allen Relationen + - CharacterAbility, CharacterSkill, CharacterFeat + - CharacterItem, CharacterResource, CharacterCondition + - Translation (Cache) + +### Frontend (React + Vite + TypeScript) + +- **Auth** + - Login-Seite mit Logo + - Registrierung + - Auth-Store (Zustand) + - Protected Routes + +- **Layout** + - Navbar mit Logo + "Dimension 47" (Cinzel Font) + - Footer mit Zeasy Logo (verlinkt) + - Responsive Design + - Dark Theme + +- **Kampagnen** + - Dashboard mit Kampagnen-Übersicht + - Kampagnen-Detailseite + - Kampagne erstellen/bearbeiten/löschen + - Mitglieder verwalten + +- **Charaktere** + - Charakter erstellen (manuell) + - Pathbuilder JSON Import (Datei-Upload + Paste) + - Charakter-Vorschau vor Import + - Charakterliste in Kampagne + +- **UI-Komponenten** + - Button, Card, Input, Spinner + - Modal-System + - Einheitliches Design-System + +--- + +## In Arbeit / Geplant + +### Hohe Priorität + +- [ ] **Charakterbogen-Ansicht** + - Vollständige Anzeige aller importierten Daten + - Abilities, Skills, Feats, Items anzeigen + - HP bearbeiten + - Zustände hinzufügen/entfernen + +- [ ] **Kampfbildschirm (Battle Tracker)** + - Initiative-Tracking + - Runden-Management + - HP-Änderungen im Kampf + - Zustände im Kampf verwalten + +### Mittlere Priorität + +- [ ] **Zauber-System** + - Zauberslots verwalten + - Zauber casten/vorbereiten + - Fokuspunkte tracking + +- [ ] **Inventar-Management** + - Items hinzufügen/entfernen + - Bulk-Berechnung + - Geld verwalten + +- [ ] **Würfel-System** + - Würfelwürfe mit Modifikatoren + - Skill-Checks + - Angriffswürfe + +### Niedrige Priorität + +- [ ] **Dokumente** + - Kampagnen-Dokumente hochladen + - Notizen teilen + +- [ ] **Notizen-System** + - Persönliche Notizen + - Geteilte Kampagnen-Notizen + +- [ ] **WebSocket Integration** + - Echtzeit-Updates für alle Spieler + - Live HP/Zustands-Änderungen + +- [ ] **Encounter Builder** + - Monster/NPCs aus Datenbank + - Encounter-Schwierigkeit berechnen + +--- + +## Technische Schulden + +- [ ] Unit Tests hinzufügen +- [ ] E2E Tests hinzufügen +- [ ] API-Dokumentation (Swagger) +- [ ] Error Boundary im Frontend +- [ ] Logging verbessern + +--- + +## Letzte Änderungen + +**2025-01-18** +- Pathbuilder Import implementiert +- Claude API für Übersetzungen integriert +- Import-Modal im Frontend erstellt +- Logo-Integration abgeschlossen diff --git a/client/Logos/Logo_Horizontal.svg b/client/Logos/Logo_Horizontal.svg new file mode 100644 index 0000000..ad7bec8 --- /dev/null +++ b/client/Logos/Logo_Horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/Logos/Logo_Vertikal.svg b/client/Logos/Logo_Vertikal.svg new file mode 100644 index 0000000..3270feb --- /dev/null +++ b/client/Logos/Logo_Vertikal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/Logos/Logo_ohne_Text.svg b/client/Logos/Logo_ohne_Text.svg new file mode 100644 index 0000000..a0b81a6 --- /dev/null +++ b/client/Logos/Logo_ohne_Text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/Logos/Zeasy/logo.svg b/client/Logos/Zeasy/logo.svg new file mode 100644 index 0000000..d6eee44 --- /dev/null +++ b/client/Logos/Zeasy/logo.svg @@ -0,0 +1,2 @@ + + diff --git a/client/Logos/Zeasy/logo_text_horizontal.svg b/client/Logos/Zeasy/logo_text_horizontal.svg new file mode 100644 index 0000000..a50da9f --- /dev/null +++ b/client/Logos/Zeasy/logo_text_horizontal.svg @@ -0,0 +1,2 @@ + + diff --git a/client/Logos/Zeasy/logo_text_vertical.svg b/client/Logos/Zeasy/logo_text_vertical.svg new file mode 100644 index 0000000..99a620e --- /dev/null +++ b/client/Logos/Zeasy/logo_text_vertical.svg @@ -0,0 +1,2 @@ + + diff --git a/client/index.html b/client/index.html index 94e7284..5e933da 100644 --- a/client/index.html +++ b/client/index.html @@ -8,7 +8,7 @@ - + Dimension47 diff --git a/client/package-lock.json b/client/package-lock.json index 70a1480..7a2714f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -68,7 +68,6 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1747,7 +1746,6 @@ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1758,7 +1756,6 @@ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1818,7 +1815,6 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -2070,7 +2066,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2193,7 +2188,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2575,7 +2569,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3763,7 +3756,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3831,7 +3823,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3841,7 +3832,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4147,7 +4137,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4234,7 +4223,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4385,7 +4373,6 @@ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/client/src/App.tsx b/client/src/App.tsx index b8bc55e..755a16f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,7 +2,8 @@ import { useEffect } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { LoginPage, RegisterPage, useAuthStore } from '@/features/auth'; -import { CampaignsPage } from '@/features/campaigns'; +import { CampaignsPage, CampaignDetailPage } from '@/features/campaigns'; +import { CharacterSheetPage } from '@/features/characters'; import { ProtectedRoute } from '@/shared/components/protected-route'; import { Layout } from '@/shared/components/layout'; @@ -43,7 +44,8 @@ function AppContent() { }> }> } /> - Campaign Detail (TODO)} /> + } /> + } /> Library (TODO)} /> diff --git a/client/src/features/auth/components/login-page.tsx b/client/src/features/auth/components/login-page.tsx index 1092523..0431313 100644 --- a/client/src/features/auth/components/login-page.tsx +++ b/client/src/features/auth/components/login-page.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { Button, Input, Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/shared/components/ui'; import { useAuthStore } from '../hooks/use-auth-store'; +import LogoVertical from '../../../../Logos/Logo_Vertikal.svg'; export function LoginPage() { const navigate = useNavigate(); @@ -30,26 +31,17 @@ export function LoginPage() { }; return ( -
+
+ {/* Logo */} + Dimension 47 + -
- - - -
- Willkommen zur\u00fcck + Willkommen zurück Melde dich an, um fortzufahren diff --git a/client/src/features/auth/components/register-page.tsx b/client/src/features/auth/components/register-page.tsx index 3c36328..0cbca4c 100644 --- a/client/src/features/auth/components/register-page.tsx +++ b/client/src/features/auth/components/register-page.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { Button, Input, Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/shared/components/ui'; import { useAuthStore } from '../hooks/use-auth-store'; +import LogoVertical from '../../../../Logos/Logo_Vertikal.svg'; export function RegisterPage() { const navigate = useNavigate(); @@ -41,25 +42,16 @@ export function RegisterPage() { }; return ( -
+
+ {/* Logo */} + Dimension 47 + -
- - - -
Konto erstellen Registriere dich f\u00fcr Dimension47 diff --git a/client/src/features/campaigns/components/add-member-modal.tsx b/client/src/features/campaigns/components/add-member-modal.tsx new file mode 100644 index 0000000..a2f8ca3 --- /dev/null +++ b/client/src/features/campaigns/components/add-member-modal.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react'; +import { X, Search, UserPlus } from 'lucide-react'; +import { Button, Input, Spinner } from '@/shared/components/ui'; +import { api } from '@/shared/lib/api'; +import type { User } from '@/shared/types'; + +interface AddMemberModalProps { + campaignId: string; + onClose: () => void; + onMemberAdded: () => void; +} + +export function AddMemberModal({ campaignId, onClose, onMemberAdded }: AddMemberModalProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [isAdding, setIsAdding] = useState(null); + + const handleSearch = async () => { + if (searchQuery.length < 2) return; + setIsSearching(true); + try { + const results = await api.searchUsers(searchQuery); + setSearchResults(results); + } catch (error) { + console.error('Failed to search users:', error); + } finally { + setIsSearching(false); + } + }; + + const handleAddMember = async (userId: string) => { + setIsAdding(userId); + try { + await api.addCampaignMember(campaignId, userId); + onMemberAdded(); + onClose(); + } catch (error) { + console.error('Failed to add member:', error); + setIsAdding(null); + } + }; + + return ( +
+
+
+
+

Mitglied hinzufügen

+ +
+ +
+
+ setSearchQuery(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + /> + +
+ + {searchResults.length > 0 && ( +
+ {searchResults.map((user) => ( +
+
+
+ + {user.username.charAt(0).toUpperCase()} + +
+
+

{user.username}

+

{user.email}

+
+
+ +
+ ))} +
+ )} + + {searchQuery.length >= 2 && searchResults.length === 0 && !isSearching && ( +

+ Keine Benutzer gefunden +

+ )} +
+
+
+ ); +} diff --git a/client/src/features/campaigns/components/campaign-detail-page.tsx b/client/src/features/campaigns/components/campaign-detail-page.tsx new file mode 100644 index 0000000..6d6c62f --- /dev/null +++ b/client/src/features/campaigns/components/campaign-detail-page.tsx @@ -0,0 +1,337 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + ArrowLeft, + Settings, + Users, + Swords, + UserPlus, + Trash2, + Crown, + Shield, + Heart, + FileJson +} from 'lucide-react'; +import { + Button, + Card, + CardHeader, + CardTitle, + CardContent, + Spinner +} from '@/shared/components/ui'; +import { api } from '@/shared/lib/api'; +import { useAuthStore } from '@/features/auth'; +import type { Campaign, CharacterSummary } from '@/shared/types'; +import { AddMemberModal } from './add-member-modal'; +import { EditCampaignModal } from './edit-campaign-modal'; +import { CreateCharacterModal, ImportCharacterModal } from '@/features/characters'; + +export function CampaignDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { user } = useAuthStore(); + + const [campaign, setCampaign] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [showAddMember, setShowAddMember] = useState(false); + const [showEditCampaign, setShowEditCampaign] = useState(false); + const [showCreateCharacter, setShowCreateCharacter] = useState(false); + const [showImportCharacter, setShowImportCharacter] = useState(false); + + const isGM = campaign?.gmId === user?.id; + const isAdmin = user?.role === 'ADMIN'; + const canManage = isGM || isAdmin; + + const fetchCampaign = async () => { + if (!id) return; + try { + const data = await api.getCampaign(id); + setCampaign(data); + } catch (error) { + console.error('Failed to fetch campaign:', error); + navigate('/'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchCampaign(); + }, [id]); + + const handleRemoveMember = async (userId: string) => { + if (!id || !confirm('Mitglied wirklich entfernen?')) return; + try { + await api.removeCampaignMember(id, userId); + fetchCampaign(); + } catch (error) { + console.error('Failed to remove member:', error); + } + }; + + const handleDeleteCampaign = async () => { + if (!id || !confirm('Kampagne wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.')) return; + try { + await api.deleteCampaign(id); + navigate('/'); + } catch (error) { + console.error('Failed to delete campaign:', error); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!campaign) { + return ( +
+

Kampagne nicht gefunden

+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

{campaign.name}

+ {campaign.description && ( +

{campaign.description}

+ )} +
+ + GM: {campaign.gm.username} +
+
+
+ {canManage && ( +
+ + +
+ )} +
+ + {/* Campaign Image */} + {campaign.imageUrl && ( +
+ {campaign.name} +
+ )} + +
+ {/* Members Section */} + + + + + Mitglieder ({campaign.members.length}) + + {canManage && ( + + )} + + +
+ {campaign.members.map((member) => ( +
+
+
+ {member.user.avatarUrl ? ( + {member.user.username} + ) : ( + + {member.user.username.charAt(0).toUpperCase()} + + )} +
+
+

+ {member.user.username} + {member.userId === campaign.gmId && ( + + )} +

+

{member.user.email}

+
+
+ {canManage && member.userId !== campaign.gmId && ( + + )} +
+ ))} +
+
+
+ + {/* Characters Section */} + + + + + Charaktere ({campaign.characters?.length || 0}) + +
+ + +
+
+ + {!campaign.characters?.length ? ( +
+ +

Noch keine Charaktere

+
+ ) : ( +
+ {campaign.characters.map((character: CharacterSummary) => ( +
navigate(`/campaigns/${id}/characters/${character.id}`)} + > +
+
+ {character.avatarUrl ? ( + {character.name} + ) : ( + + )} +
+
+

{character.name}

+

+ Level {character.level} {character.type} + {character.owner && ` • ${character.owner.username}`} +

+
+
+
+ + + {character.hpCurrent}/{character.hpMax} + +
+
+ ))} +
+ )} +
+
+
+ + {/* Quick Actions */} +
+ navigate(`/campaigns/${id}/battle`)}> +
+
+ +
+
+

Kampfbildschirm

+

Kämpfe verwalten

+
+
+
+ +
+
+ +
+
+

Dokumente

+

Bald verfügbar

+
+
+
+ +
+
+ +
+
+

Notizen

+

Bald verfügbar

+
+
+
+
+ + {/* Modals */} + {showAddMember && ( + setShowAddMember(false)} + onMemberAdded={fetchCampaign} + /> + )} + {showEditCampaign && ( + setShowEditCampaign(false)} + onUpdated={fetchCampaign} + /> + )} + {showCreateCharacter && ( + setShowCreateCharacter(false)} + onCreated={fetchCampaign} + /> + )} + {showImportCharacter && ( + setShowImportCharacter(false)} + onImported={fetchCampaign} + /> + )} +
+ ); +} diff --git a/client/src/features/campaigns/components/edit-campaign-modal.tsx b/client/src/features/campaigns/components/edit-campaign-modal.tsx new file mode 100644 index 0000000..b9937cf --- /dev/null +++ b/client/src/features/campaigns/components/edit-campaign-modal.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import { X } from 'lucide-react'; +import { Button, Input, Spinner } from '@/shared/components/ui'; +import { api } from '@/shared/lib/api'; +import type { Campaign } from '@/shared/types'; + +interface EditCampaignModalProps { + campaign: Campaign; + onClose: () => void; + onUpdated: () => void; +} + +export function EditCampaignModal({ campaign, onClose, onUpdated }: EditCampaignModalProps) { + const [name, setName] = useState(campaign.name); + const [description, setDescription] = useState(campaign.description || ''); + const [imageUrl, setImageUrl] = useState(campaign.imageUrl || ''); + 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.updateCampaign(campaign.id, { + name: name.trim(), + description: description.trim() || undefined, + imageUrl: imageUrl.trim() || undefined, + }); + onUpdated(); + onClose(); + } catch (err) { + setError('Fehler beim Aktualisieren der Kampagne'); + console.error('Failed to update campaign:', err); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+
+

Kampagne bearbeiten

+ +
+ +
+
+ + setName(e.target.value)} + placeholder="Name der Kampagne" + /> +
+ +
+ +