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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user