From aaeae68fd970e51c33b8c1e48e15141615785268 Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Wed, 21 Jan 2026 09:08:26 +0100 Subject: [PATCH] fix: TypeScript errors and add clickable class actions - Fix isEquipped -> equipped property name in export-character-html.ts - Remove unused imports in character-sheet-page.tsx - Remove unused variable in alchemy-tab.tsx - Add on-demand German translation for feats in feats.service.ts - Make class actions clickable in actions-tab with FeatDetailModal - Add (Englisch) hint for untranslated feat descriptions Co-Authored-By: Claude Opus 4.5 --- .../characters/components/actions-tab.tsx | 23 ++++++--- .../characters/components/alchemy-tab.tsx | 9 +--- .../components/character-sheet-page.tsx | 11 +--- .../components/feat-detail-modal.tsx | 4 +- .../characters/utils/export-character-html.ts | 4 +- server/src/modules/feats/feats.service.ts | 50 ++++++++++++++++++- 6 files changed, 74 insertions(+), 27 deletions(-) diff --git a/client/src/features/characters/components/actions-tab.tsx b/client/src/features/characters/components/actions-tab.tsx index 70585de..614e9d4 100644 --- a/client/src/features/characters/components/actions-tab.tsx +++ b/client/src/features/characters/components/actions-tab.tsx @@ -4,6 +4,8 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui import { Input } from '@/shared/components/ui/input'; import { Button } from '@/shared/components/ui/button'; import { ActionIcon, ActionTypeBadge } from '@/shared/components/ui/action-icon'; +import { FeatDetailModal } from './feat-detail-modal'; +import type { CharacterFeat } from '@/shared/types'; interface ActionResult { criticalSuccess?: string; @@ -133,12 +135,7 @@ function ActionCard({ action, isExpanded, onToggle }: { action: Action; isExpand } interface ActionsTabProps { - characterFeats?: Array<{ - id: string; - name: string; - nameGerman?: string | null; - source: string; - }>; + characterFeats?: CharacterFeat[]; } type CategoryKey = 'class' | 'action' | 'reaction' | 'free' | 'exploration' | 'downtime' | 'varies'; @@ -189,6 +186,7 @@ export function ActionsTab({ characterFeats = [] }: ActionsTabProps) { const [expandedActionId, setExpandedActionId] = useState(null); const [showFilters, setShowFilters] = useState(false); const [expandedCategories, setExpandedCategories] = useState>(new Set()); + const [selectedFeat, setSelectedFeat] = useState(null); const toggleCategory = (category: CategoryKey) => { setExpandedCategories((prev) => { @@ -338,6 +336,7 @@ export function ActionsTab({ characterFeats = [] }: ActionsTabProps) {
setSelectedFeat(feat)} > {feat.nameGerman || feat.name} @@ -498,6 +497,18 @@ export function ActionsTab({ characterFeats = [] }: ActionsTabProps) { )} + + {/* Feat Detail Modal */} + {selectedFeat && ( + setSelectedFeat(null)} + onRemove={() => { + // No remove functionality in actions tab - just close + setSelectedFeat(null); + }} + /> + )}
); } diff --git a/client/src/features/characters/components/alchemy-tab.tsx b/client/src/features/characters/components/alchemy-tab.tsx index 63f4483..0e6436a 100644 --- a/client/src/features/characters/components/alchemy-tab.tsx +++ b/client/src/features/characters/components/alchemy-tab.tsx @@ -1205,11 +1205,7 @@ function AddFormulaModal({

) : (
- {results.map((item) => { - const subcategoryColor = item.itemSubcategory - ? SUBCATEGORY_COLORS[item.itemSubcategory] || 'text-text-muted' - : ''; - return ( + {results.map((item) => (
- ); - })} + ))}
)} diff --git a/client/src/features/characters/components/character-sheet-page.tsx b/client/src/features/characters/components/character-sheet-page.tsx index 45b7833..645f2ad 100644 --- a/client/src/features/characters/components/character-sheet-page.tsx +++ b/client/src/features/characters/components/character-sheet-page.tsx @@ -42,7 +42,7 @@ import { RestModal } from './rest-modal'; import { AlchemyTab } from './alchemy-tab'; import { useCharacterSocket } from '@/shared/hooks/use-character-socket'; import { downloadCharacterHTML } from '../utils/export-character-html'; -import type { Character, CharacterItem, CharacterFeat, Campaign, RestResult, CharacterAlchemyState, CharacterFormula, CharacterPreparedItem } from '@/shared/types'; +import type { Character, CharacterItem, CharacterFeat, Campaign } from '@/shared/types'; type TabType = 'status' | 'skills' | 'inventory' | 'feats' | 'spells' | 'alchemy' | 'actions'; @@ -56,15 +56,6 @@ const TABS: { id: TabType; label: string; icon: React.ReactNode }[] = [ { id: 'actions', label: 'Aktionen', icon: }, ]; -const ABILITY_NAMES: Record = { - STR: 'Stärke', - DEX: 'Geschicklichkeit', - CON: 'Konstitution', - INT: 'Intelligenz', - WIS: 'Weisheit', - CHA: 'Charisma', -}; - const PROFICIENCY_NAMES: Record = { UNTRAINED: 'Ungeübt', TRAINED: 'Geübt', diff --git a/client/src/features/characters/components/feat-detail-modal.tsx b/client/src/features/characters/components/feat-detail-modal.tsx index fbc9973..39af267 100644 --- a/client/src/features/characters/components/feat-detail-modal.tsx +++ b/client/src/features/characters/components/feat-detail-modal.tsx @@ -194,7 +194,9 @@ export function FeatDetailModal({ feat, onClose, onRemove }: FeatDetailModalProp {/* Full Description */} {featDetails?.description && (
-

Vollständige Beschreibung

+

+ Vollständige Beschreibung (Englisch) +

{featDetails.description}

diff --git a/client/src/features/characters/utils/export-character-html.ts b/client/src/features/characters/utils/export-character-html.ts index 16c54bb..46afb02 100644 --- a/client/src/features/characters/utils/export-character-html.ts +++ b/client/src/features/characters/utils/export-character-html.ts @@ -161,8 +161,8 @@ export function generateCharacterHTML(character: Character): string { const hasPreparedItems = character.preparedItems && character.preparedItems.length > 0; // Group items by equipped status - const equippedItems = character.items?.filter(i => i.isEquipped) || []; - const carriedItems = character.items?.filter(i => !i.isEquipped) || []; + const equippedItems = character.items?.filter(i => i.equipped) || []; + const carriedItems = character.items?.filter(i => !i.equipped) || []; // Calculate total bulk const totalBulk = character.items?.reduce((sum, item) => { diff --git a/server/src/modules/feats/feats.service.ts b/server/src/modules/feats/feats.service.ts index 6d51791..2e5aee2 100644 --- a/server/src/modules/feats/feats.service.ts +++ b/server/src/modules/feats/feats.service.ts @@ -110,9 +110,18 @@ export class FeatsService { } async findById(id: string) { - return this.prisma.feat.findUnique({ + const feat = await this.prisma.feat.findUnique({ where: { id }, }); + + if (!feat) return null; + + // Generate translation on-demand if missing + if (!feat.nameGerman || !feat.summaryGerman) { + return this.ensureTranslation(feat); + } + + return feat; } async findByName(name: string) { @@ -133,6 +142,13 @@ export class FeatsService { }); } + if (!feat) return null; + + // Generate translation on-demand if missing + if (!feat.nameGerman || !feat.summaryGerman) { + return this.ensureTranslation(feat); + } + return feat; } @@ -179,6 +195,38 @@ export class FeatsService { return Array.from(traitsSet).sort(); } + // Ensure a feat has German translations, generating them if needed + private async ensureTranslation(feat: { + id: string; + name: string; + summary?: string | null; + nameGerman?: string | null; + summaryGerman?: string | null; + }) { + try { + const translation = await this.translationsService.getTranslation( + TranslationType.FEAT, + feat.name, + feat.summary || undefined, + ); + + // Update the database with the translation + const updated = await this.prisma.feat.update({ + where: { id: feat.id }, + data: { + nameGerman: translation.germanName, + summaryGerman: translation.germanDescription, + }, + }); + + return updated; + } catch (error) { + // If translation fails, return the original feat + console.error(`Failed to translate feat ${feat.name}:`, error); + return feat; + } + } + // Get translation for a feat (with caching) async getTranslatedFeat(feat: { name: string; summary?: string | null }) { const translation = await this.translationsService.getTranslation(