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 <noreply@anthropic.com>
This commit is contained in:
Alexander Zielonka
2026-01-21 09:08:26 +01:00
parent b3dc773fbf
commit aaeae68fd9
6 changed files with 74 additions and 27 deletions

View File

@@ -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<string | null>(null);
const [showFilters, setShowFilters] = useState(false);
const [expandedCategories, setExpandedCategories] = useState<Set<CategoryKey>>(new Set());
const [selectedFeat, setSelectedFeat] = useState<CharacterFeat | null>(null);
const toggleCategory = (category: CategoryKey) => {
setExpandedCategories((prev) => {
@@ -338,6 +336,7 @@ export function ActionsTab({ characterFeats = [] }: ActionsTabProps) {
<div
key={feat.id}
className="p-3 rounded-lg bg-bg-secondary hover:bg-bg-tertiary cursor-pointer"
onClick={() => setSelectedFeat(feat)}
>
<span className="font-medium text-text-primary">
{feat.nameGerman || feat.name}
@@ -498,6 +497,18 @@ export function ActionsTab({ characterFeats = [] }: ActionsTabProps) {
</CardContent>
</Card>
)}
{/* Feat Detail Modal */}
{selectedFeat && (
<FeatDetailModal
feat={selectedFeat}
onClose={() => setSelectedFeat(null)}
onRemove={() => {
// No remove functionality in actions tab - just close
setSelectedFeat(null);
}}
/>
)}
</div>
);
}

View File

@@ -1205,11 +1205,7 @@ function AddFormulaModal({
</p>
) : (
<div className="space-y-2">
{results.map((item) => {
const subcategoryColor = item.itemSubcategory
? SUBCATEGORY_COLORS[item.itemSubcategory] || 'text-text-muted'
: '';
return (
{results.map((item) => (
<div
key={item.id}
className="flex items-center justify-between p-3 rounded-lg bg-bg-secondary hover:bg-bg-tertiary"
@@ -1239,8 +1235,7 @@ function AddFormulaModal({
<Plus className="h-4 w-4" />
</Button>
</div>
);
})}
))}
</div>
)}
</div>

View File

@@ -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: <Swords className="h-4 w-4" /> },
];
const ABILITY_NAMES: Record<string, string> = {
STR: 'Stärke',
DEX: 'Geschicklichkeit',
CON: 'Konstitution',
INT: 'Intelligenz',
WIS: 'Weisheit',
CHA: 'Charisma',
};
const PROFICIENCY_NAMES: Record<string, string> = {
UNTRAINED: 'Ungeübt',
TRAINED: 'Geübt',

View File

@@ -194,7 +194,9 @@ export function FeatDetailModal({ feat, onClose, onRemove }: FeatDetailModalProp
{/* Full Description */}
{featDetails?.description && (
<div className="p-3 rounded-lg bg-bg-secondary">
<p className="text-xs text-text-secondary mb-1">Vollständige Beschreibung</p>
<p className="text-xs text-text-secondary mb-1">
Vollständige Beschreibung <span className="text-text-muted">(Englisch)</span>
</p>
<p className="text-sm text-text-primary leading-relaxed whitespace-pre-wrap">
{featDetails.description}
</p>

View File

@@ -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) => {

View File

@@ -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(