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",
|
"name": "homepage",
|
||||||
"version": "1.40.3",
|
"version": "1.40.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -6,11 +6,9 @@
|
|||||||
import { confirm } from '$lib/js/confirmDialog.svelte';
|
import { confirm } from '$lib/js/confirmDialog.svelte';
|
||||||
import SaveFab from '$lib/components/SaveFab.svelte';
|
import SaveFab from '$lib/components/SaveFab.svelte';
|
||||||
import DatePicker from '$lib/components/DatePicker.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 lang = $derived(detectFitnessLang($page.url.pathname));
|
||||||
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
||||||
const historySlug = $derived(lang === 'en' ? 'history' : 'verlauf');
|
|
||||||
import { getWorkout } from '$lib/js/workout.svelte';
|
import { getWorkout } from '$lib/js/workout.svelte';
|
||||||
import PeriodTracker from '$lib/components/fitness/PeriodTracker.svelte';
|
import PeriodTracker from '$lib/components/fitness/PeriodTracker.svelte';
|
||||||
|
|
||||||
@@ -99,26 +97,6 @@
|
|||||||
{ label: t('r_calf', lang), key: 'rightCalf', value: latestBp.rightCalf }
|
{ 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 */
|
/** @param {string} id */
|
||||||
async function deleteMeasurement(id) {
|
async function deleteMeasurement(id) {
|
||||||
if (!await confirm(t('delete_measurement_confirm', lang))) return;
|
if (!await confirm(t('delete_measurement_confirm', lang))) return;
|
||||||
@@ -340,55 +318,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</form>
|
</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}
|
{#if measurements.length > 0}
|
||||||
<section class="history-section">
|
<section class="history-section">
|
||||||
<button class="history-toggle" onclick={() => showWeightHistory = !showWeightHistory}>
|
<button class="history-toggle" onclick={() => showWeightHistory = !showWeightHistory}>
|
||||||
@@ -919,156 +848,6 @@
|
|||||||
white-space: nowrap;
|
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 */
|
||||||
.history-toggle {
|
.history-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import type { PageServerLoad } from './$types';
|
|||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
const session = await locals.auth();
|
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/stats/overview'),
|
||||||
fetch('/api/fitness/goal'),
|
fetch('/api/fitness/goal'),
|
||||||
fetch('/api/fitness/stats/muscle-heatmap?weeks=8'),
|
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 stats = await res.json();
|
||||||
const goal = goalRes.ok ? await goalRes.json() : { weeklyWorkouts: null, streak: 0 };
|
const goal = goalRes.ok ? await goalRes.json() : { weeklyWorkouts: null, streak: 0 };
|
||||||
const muscleHeatmap = heatmapRes.ok ? await heatmapRes.json() : { weeks: [], totals: {}, muscleGroups: [] };
|
const muscleHeatmap = heatmapRes.ok ? await heatmapRes.json() : { weeks: [], totals: {}, muscleGroups: [] };
|
||||||
const nutritionStats = nutritionRes.ok ? await nutritionRes.json() : null;
|
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 { page } from '$app/stores';
|
||||||
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
|
import FitnessChart from '$lib/components/fitness/FitnessChart.svelte';
|
||||||
import MuscleHeatmap from '$lib/components/fitness/MuscleHeatmap.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 FitnessStreakAura from '$lib/components/fitness/FitnessStreakAura.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n';
|
||||||
import { toast } from '$lib/js/toast.svelte';
|
import { toast } from '$lib/js/toast.svelte';
|
||||||
import StatsRingGraph from '$lib/components/fitness/StatsRingGraph.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 lang = $derived(detectFitnessLang($page.url.pathname));
|
||||||
|
const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik');
|
||||||
|
const historySlug = $derived(lang === 'en' ? 'history' : 'verlauf');
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -37,6 +40,28 @@
|
|||||||
|
|
||||||
const stats = $derived(data.stats ?? {});
|
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 goalStreak = $derived(data.goal?.streak ?? 0);
|
||||||
let goalWeekly = $derived(data.goal?.weeklyWorkouts ?? null);
|
let goalWeekly = $derived(data.goal?.weeklyWorkouts ?? null);
|
||||||
let showBalanceInfo = $state(false);
|
let showBalanceInfo = $state(false);
|
||||||
@@ -356,6 +381,55 @@
|
|||||||
<MuscleHeatmap data={data.muscleHeatmap} />
|
<MuscleHeatmap data={data.muscleHeatmap} />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -890,4 +964,157 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
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>
|
</style>
|
||||||
|
|||||||
+2
-1
@@ -7,6 +7,7 @@
|
|||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
const lang = $derived(detectFitnessLang($page.url.pathname));
|
const lang = $derived(detectFitnessLang($page.url.pathname));
|
||||||
|
const statsSlug = $derived(lang === 'en' ? 'stats' : 'statistik');
|
||||||
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
|
||||||
const card = $derived(data.card);
|
const card = $derived(data.card);
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
|
|
||||||
<div class="detail-page">
|
<div class="detail-page">
|
||||||
<header class="detail-header">
|
<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} />
|
<ArrowLeft size={18} />
|
||||||
</a>
|
</a>
|
||||||
<div class="head-text">
|
<div class="head-text">
|
||||||
Reference in New Issue
Block a user