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(