Files
homepage/src/lib/components/fitness/TemplateCard.svelte
Alexander Bocken 9a27e50495 fitness: add German translations for all 77 exercises
Add per-exercise de property with translated name and instructions.
Add shared term translation map for bodyPart, equipment, target, and
muscle names. Add localizeExercise() and translateTerm() helpers.
Update all display components to use localized fields (localName,
localBodyPart, localEquipment, etc.) and pass lang to search/lookup.
2026-03-23 07:44:35 +01:00

124 lines
3.2 KiB
Svelte

<script>
import { getExerciseById } from '$lib/data/exercises';
import { EllipsisVertical } from 'lucide-svelte';
import { page } from '$app/stores';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
const lang = $derived(detectFitnessLang($page.url.pathname));
/**
* @type {{
* template: { _id: string, name: string, exercises: Array<{ exerciseId: string, sets: any[] }> },
* lastUsed?: string | null,
* onStart?: (() => void) | null,
* onMenu?: ((e: MouseEvent) => void) | null
* }}
*/
let { template, lastUsed = null, onStart = null, onMenu = null } = $props();
/** @param {string} dateStr */
function formatDate(dateStr) {
const d = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - d.getTime();
const diffDays = Math.floor(diffMs / 86400000);
if (diffDays === 0) return t('today', lang);
if (diffDays === 1) return t('yesterday', lang);
if (diffDays < 7) return lang === 'en' ? `${diffDays} days ago` : `vor ${diffDays} Tagen`;
return d.toLocaleDateString(lang === 'en' ? 'en' : 'de', { month: 'short', day: 'numeric' });
}
</script>
<div class="template-card" role="button" tabindex="0" onclick={() => onStart?.()} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onStart?.(); }}}>
<div class="card-header">
<h3 class="card-title">{template.name}</h3>
{#if onMenu}
<button
class="menu-btn"
onclick={(e) => { e.stopPropagation(); onMenu?.(e); }}
aria-label="Template options"
>
<EllipsisVertical size={16} />
</button>
{/if}
</div>
<ul class="exercise-preview">
{#each template.exercises.slice(0, 4) as ex}
{@const exercise = getExerciseById(ex.exerciseId, lang)}
<li>{ex.sets.length} &times; {exercise?.localName ?? ex.exerciseId}</li>
{/each}
{#if template.exercises.length > 4}
<li class="more">+{template.exercises.length - 4} {t('more', lang)}</li>
{/if}
</ul>
{#if lastUsed}
<p class="last-used">{t('last_performed', lang)} {formatDate(lastUsed)}</p>
{/if}
</div>
<style>
.template-card {
display: flex;
flex-direction: column;
text-align: left;
background: var(--color-surface);
border: none;
border-radius: 8px;
box-shadow: var(--shadow-sm);
padding: 1rem;
cursor: pointer;
transition: transform 150ms ease, box-shadow 150ms ease;
width: 100%;
font: inherit;
color: inherit;
}
.template-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.template-card:active {
transform: translateY(0);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.card-title {
font-size: 0.95rem;
font-weight: 700;
margin: 0;
}
.menu-btn {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 0.15rem;
border-radius: 4px;
}
.menu-btn:hover {
color: var(--color-primary);
}
.exercise-preview {
list-style: none;
padding: 0;
margin: 0;
font-size: 0.8rem;
color: var(--color-text-secondary);
}
.exercise-preview li {
padding: 0.1rem 0;
}
.exercise-preview .more {
color: var(--color-primary);
font-style: italic;
}
.last-used {
margin: 0.5rem 0 0;
font-size: 0.75rem;
color: var(--color-text-secondary);
}
</style>