refactor(fitness): move body-parts overview to stats page
Body-parts grid now lives on /fitness/stats, and per-part history pages moved from /fitness/measure/history/<part> to /fitness/stats/history/<part>. Measure page keeps weight/BF entry and the body-parts measure launcher.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.40.3",
|
||||
"version": "1.40.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||
import SaveFab from '$lib/components/SaveFab.svelte';
|
||||
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||
import { BODY_PART_CARDS, bodyPartSlug } from '$lib/js/fitnessBodyParts';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
||||
const historySlug = $derived(lang === 'en' ? 'history' : 'verlauf');
|
||||
import { getWorkout } from '$lib/js/workout.svelte';
|
||||
import PeriodTracker from '$lib/components/fitness/PeriodTracker.svelte';
|
||||
|
||||
@@ -99,26 +97,6 @@
|
||||
{ label: t('r_calf', lang), key: 'rightCalf', value: latestBp.rightCalf }
|
||||
]);
|
||||
|
||||
/** @param {import('$lib/js/fitnessBodyParts').BodyPartCard} c */
|
||||
function currentValue(c) {
|
||||
if (c.paired) {
|
||||
const l = /** @type {number|undefined} */ (latestBp[c.dbLeft]);
|
||||
const r = /** @type {number|undefined} */ (latestBp[c.dbRight]);
|
||||
return { left: l ?? null, right: r ?? null };
|
||||
}
|
||||
const v = /** @type {number|undefined} */ (latestBp[c.db]);
|
||||
return { value: v ?? null };
|
||||
}
|
||||
|
||||
/** @param {import('$lib/js/fitnessBodyParts').BodyPartCard} c */
|
||||
function hasAny(c) {
|
||||
const v = currentValue(c);
|
||||
if (c.paired) return v.left != null || v.right != null;
|
||||
return v.value != null;
|
||||
}
|
||||
|
||||
const cardsWithData = $derived(BODY_PART_CARDS.filter(hasAny));
|
||||
|
||||
/** @param {string} id */
|
||||
async function deleteMeasurement(id) {
|
||||
if (!await confirm(t('delete_measurement_confirm', lang))) return;
|
||||
@@ -340,55 +318,6 @@
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
{#if cardsWithData.length > 0}
|
||||
<section class="body-parts-section">
|
||||
<h2>{t('body_parts', lang)}</h2>
|
||||
<div class="bp-grid">
|
||||
{#each cardsWithData as card (card.key)}
|
||||
{@const cv = currentValue(card)}
|
||||
<a
|
||||
class="bp-card"
|
||||
href="/fitness/{measureSlug}/{historySlug}/{bodyPartSlug(card, lang)}"
|
||||
>
|
||||
<div class="bp-img-wrap" aria-hidden="true">
|
||||
{#if card.img && card.img.endsWith('.svg')}
|
||||
<div
|
||||
class="bp-img bp-img-svg"
|
||||
style="--bp-svg-src: url(/fitness/measure/{card.img})"
|
||||
></div>
|
||||
{:else if card.img}
|
||||
<img src="/fitness/measure/{card.img}" alt="" class="bp-img" />
|
||||
{:else}
|
||||
<Ruler size={36} strokeWidth={1.5} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="bp-meta">
|
||||
<span class="bp-label">{t(card.labelKey, lang)}</span>
|
||||
{#if card.paired}
|
||||
{#if cv.left != null && cv.right != null && cv.left === cv.right}
|
||||
<span class="bp-value">{cv.left.toFixed(1)}<span class="bp-unit">cm</span></span>
|
||||
{:else if cv.left != null && cv.right != null}
|
||||
<span class="bp-value paired">
|
||||
<span class="bp-side"><em>L</em> {cv.left.toFixed(1)}</span>
|
||||
<span class="bp-side-sep">·</span>
|
||||
<span class="bp-side"><em>R</em> {cv.right.toFixed(1)}</span>
|
||||
<span class="bp-unit">cm</span>
|
||||
</span>
|
||||
{:else if cv.left != null}
|
||||
<span class="bp-value"><em>L</em> {cv.left.toFixed(1)}<span class="bp-unit">cm</span></span>
|
||||
{:else if cv.right != null}
|
||||
<span class="bp-value"><em>R</em> {cv.right.toFixed(1)}<span class="bp-unit">cm</span></span>
|
||||
{/if}
|
||||
{:else if cv.value != null}
|
||||
<span class="bp-value">{cv.value.toFixed(1)}<span class="bp-unit">cm</span></span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if measurements.length > 0}
|
||||
<section class="history-section">
|
||||
<button class="history-toggle" onclick={() => showWeightHistory = !showWeightHistory}>
|
||||
@@ -919,156 +848,6 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Body parts (latest) */
|
||||
.bp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.bp-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.7rem 0.5rem 0.6rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: border-color var(--transition-normal), box-shadow var(--transition-normal), transform var(--transition-normal);
|
||||
}
|
||||
.bp-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.bp-img-wrap {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.bp-img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
.bp-img-svg {
|
||||
mask-image: var(--bp-svg-src);
|
||||
-webkit-mask-image: var(--bp-svg-src);
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
-webkit-mask-position: center;
|
||||
background-color: var(--color-text-primary);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
img.bp-img { filter: invert(1); }
|
||||
}
|
||||
:global(:root[data-theme="dark"]) img.bp-img { filter: invert(1); }
|
||||
:global(:root[data-theme="light"]) img.bp-img { filter: none; }
|
||||
.bp-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.bp-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.bp-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-text-primary);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.bp-value.paired {
|
||||
font-size: 0.78rem;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.bp-value em {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 0.62rem;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-right: 0.15rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.bp-side {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bp-side-sep {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.bp-unit {
|
||||
margin-left: 0.2rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
.bp-grid { gap: 0.45rem; }
|
||||
.bp-card { padding: 0.55rem 0.35rem; }
|
||||
.bp-img-wrap { width: 2.6rem; height: 2.6rem; }
|
||||
.bp-img { width: 1.9rem; height: 1.9rem; }
|
||||
.bp-label { font-size: 0.58rem; }
|
||||
.bp-value { font-size: 0.88rem; }
|
||||
.bp-value.paired { font-size: 0.7rem; }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.bp-grid { gap: 0.85rem; }
|
||||
.bp-card {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
gap: 0.85rem;
|
||||
padding: 0.9rem 1rem;
|
||||
}
|
||||
.bp-img-wrap {
|
||||
width: 3.75rem;
|
||||
height: 3.75rem;
|
||||
}
|
||||
.bp-img {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
.bp-meta {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.bp-value.paired {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.bp-label {
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
.bp-value {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* History */
|
||||
.history-toggle {
|
||||
display: flex;
|
||||
|
||||
@@ -2,15 +2,17 @@ import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||
const session = await locals.auth();
|
||||
const [res, goalRes, heatmapRes, nutritionRes] = await Promise.all([
|
||||
const [res, goalRes, heatmapRes, nutritionRes, latestRes] = await Promise.all([
|
||||
fetch('/api/fitness/stats/overview'),
|
||||
fetch('/api/fitness/goal'),
|
||||
fetch('/api/fitness/stats/muscle-heatmap?weeks=8'),
|
||||
fetch('/api/fitness/stats/nutrition')
|
||||
fetch('/api/fitness/stats/nutrition'),
|
||||
fetch('/api/fitness/measurements/latest')
|
||||
]);
|
||||
const stats = await res.json();
|
||||
const goal = goalRes.ok ? await goalRes.json() : { weeklyWorkouts: null, streak: 0 };
|
||||
const muscleHeatmap = heatmapRes.ok ? await heatmapRes.json() : { weeks: [], totals: {}, muscleGroups: [] };
|
||||
const nutritionStats = nutritionRes.ok ? await nutritionRes.json() : null;
|
||||
return { session, stats, goal, muscleHeatmap, nutritionStats };
|
||||
const latest = latestRes.ok ? await latestRes.json() : {};
|
||||
return { session, stats, goal, muscleHeatmap, nutritionStats, latest };
|
||||
};
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
import { page } from '$app/stores';
|
||||
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
|
||||
import MuscleHeatmap from '$lib/components/fitness/MuscleHeatmap.svelte';
|
||||
import { Dumbbell, Route, Flame, Weight, Beef, Droplet, Wheat, Scale, Target, Info } from '@lucide/svelte';
|
||||
import { Dumbbell, Route, Flame, Weight, Beef, Droplet, Wheat, Scale, Target, Info, Ruler } from '@lucide/svelte';
|
||||
import FitnessStreakAura from '$lib/components/fitness/FitnessStreakAura.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||
import { toast } from '$lib/js/toast.svelte';
|
||||
import StatsRingGraph from '$lib/components/fitness/StatsRingGraph.svelte';
|
||||
import { BODY_PART_CARDS, bodyPartSlug } from '$lib/js/fitnessBodyParts';
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik');
|
||||
const historySlug = $derived(lang === 'en' ? 'history' : 'verlauf');
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -37,6 +40,28 @@
|
||||
|
||||
const stats = $derived(data.stats ?? {});
|
||||
|
||||
const latestBp = $derived(data.latest?.measurements?.value ?? {});
|
||||
|
||||
/** @param {import('$lib/js/fitnessBodyParts').BodyPartCard} c */
|
||||
function currentValue(c) {
|
||||
if (c.paired) {
|
||||
const l = /** @type {number|undefined} */ (latestBp[c.dbLeft]);
|
||||
const r = /** @type {number|undefined} */ (latestBp[c.dbRight]);
|
||||
return { left: l ?? null, right: r ?? null };
|
||||
}
|
||||
const v = /** @type {number|undefined} */ (latestBp[c.db]);
|
||||
return { value: v ?? null };
|
||||
}
|
||||
|
||||
/** @param {import('$lib/js/fitnessBodyParts').BodyPartCard} c */
|
||||
function hasAny(c) {
|
||||
const v = currentValue(c);
|
||||
if (c.paired) return v.left != null || v.right != null;
|
||||
return v.value != null;
|
||||
}
|
||||
|
||||
const cardsWithData = $derived(BODY_PART_CARDS.filter(hasAny));
|
||||
|
||||
let goalStreak = $derived(data.goal?.streak ?? 0);
|
||||
let goalWeekly = $derived(data.goal?.weeklyWorkouts ?? null);
|
||||
let showBalanceInfo = $state(false);
|
||||
@@ -356,6 +381,55 @@
|
||||
<MuscleHeatmap data={data.muscleHeatmap} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if cardsWithData.length > 0}
|
||||
<section class="body-parts-section">
|
||||
<h2>{t('body_parts', lang)}</h2>
|
||||
<div class="bp-grid">
|
||||
{#each cardsWithData as card (card.key)}
|
||||
{@const cv = currentValue(card)}
|
||||
<a
|
||||
class="bp-card"
|
||||
href="/fitness/{statsSlug}/{historySlug}/{bodyPartSlug(card, lang)}"
|
||||
>
|
||||
<div class="bp-img-wrap" aria-hidden="true">
|
||||
{#if card.img && card.img.endsWith('.svg')}
|
||||
<div
|
||||
class="bp-img bp-img-svg"
|
||||
style="--bp-svg-src: url(/fitness/measure/{card.img})"
|
||||
></div>
|
||||
{:else if card.img}
|
||||
<img src="/fitness/measure/{card.img}" alt="" class="bp-img" />
|
||||
{:else}
|
||||
<Ruler size={36} strokeWidth={1.5} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="bp-meta">
|
||||
<span class="bp-label">{t(card.labelKey, lang)}</span>
|
||||
{#if card.paired}
|
||||
{#if cv.left != null && cv.right != null && cv.left === cv.right}
|
||||
<span class="bp-value">{cv.left.toFixed(1)}<span class="bp-unit">cm</span></span>
|
||||
{:else if cv.left != null && cv.right != null}
|
||||
<span class="bp-value paired">
|
||||
<span class="bp-side"><em>L</em> {cv.left.toFixed(1)}</span>
|
||||
<span class="bp-side-sep">·</span>
|
||||
<span class="bp-side"><em>R</em> {cv.right.toFixed(1)}</span>
|
||||
<span class="bp-unit">cm</span>
|
||||
</span>
|
||||
{:else if cv.left != null}
|
||||
<span class="bp-value"><em>L</em> {cv.left.toFixed(1)}<span class="bp-unit">cm</span></span>
|
||||
{:else if cv.right != null}
|
||||
<span class="bp-value"><em>R</em> {cv.right.toFixed(1)}<span class="bp-unit">cm</span></span>
|
||||
{/if}
|
||||
{:else if cv.value != null}
|
||||
<span class="bp-value">{cv.value.toFixed(1)}<span class="bp-unit">cm</span></span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -890,4 +964,157 @@
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.body-parts-section h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.bp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.bp-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.7rem 0.5rem 0.6rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
transition: border-color var(--transition-normal), box-shadow var(--transition-normal), transform var(--transition-normal);
|
||||
}
|
||||
.bp-card:hover {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.bp-img-wrap {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.bp-img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
.bp-img-svg {
|
||||
mask-image: var(--bp-svg-src);
|
||||
-webkit-mask-image: var(--bp-svg-src);
|
||||
mask-size: contain;
|
||||
-webkit-mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
-webkit-mask-position: center;
|
||||
background-color: var(--color-text-primary);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
img.bp-img { filter: invert(1); }
|
||||
}
|
||||
:global(:root[data-theme="dark"]) img.bp-img { filter: invert(1); }
|
||||
:global(:root[data-theme="light"]) img.bp-img { filter: none; }
|
||||
.bp-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.bp-label {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.bp-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-text-primary);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.bp-value.paired {
|
||||
font-size: 0.78rem;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.bp-value em {
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 0.62rem;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-right: 0.15rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.bp-side {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bp-side-sep {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.bp-unit {
|
||||
margin-left: 0.2rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
.bp-grid { gap: 0.45rem; }
|
||||
.bp-card { padding: 0.55rem 0.35rem; }
|
||||
.bp-img-wrap { width: 2.6rem; height: 2.6rem; }
|
||||
.bp-img { width: 1.9rem; height: 1.9rem; }
|
||||
.bp-label { font-size: 0.58rem; }
|
||||
.bp-value { font-size: 0.88rem; }
|
||||
.bp-value.paired { font-size: 0.7rem; }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.bp-grid { gap: 0.85rem; }
|
||||
.bp-card {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
gap: 0.85rem;
|
||||
padding: 0.9rem 1rem;
|
||||
}
|
||||
.bp-img-wrap {
|
||||
width: 3.75rem;
|
||||
height: 3.75rem;
|
||||
}
|
||||
.bp-img {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
.bp-meta {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.bp-value.paired {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.bp-label {
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
.bp-value {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@
|
||||
let { data } = $props();
|
||||
|
||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||
const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik');
|
||||
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
||||
const card = $derived(data.card);
|
||||
|
||||
@@ -112,7 +113,7 @@
|
||||
|
||||
<div class="detail-page">
|
||||
<header class="detail-header">
|
||||
<a class="back-link" href="/fitness/{measureSlug}" aria-label={t('back', lang)}>
|
||||
<a class="back-link" href="/fitness/{statsSlug}" aria-label={t('back', lang)}>
|
||||
<ArrowLeft size={18} />
|
||||
</a>
|
||||
<div class="head-text">
|
||||
Reference in New Issue
Block a user