perf: parallelize DB queries across routes, clean up fitness UI
Parallelize sequential DB queries in 11 API routes and page loaders using Promise.all — measurements/latest, stats/overview, goal streak, exercises, sessions, task stats, monthly expenses, icon page, offline-db. Move calorie tracking out of /fitness/measure (now under /fitness/nutrition only). Remove fade-in entrance animations from nutrition page. Progressive streak computation: scan 3 months first, widen only if needed. Bump versions to 1.1.1 / 0.2.1.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homepage",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bocken"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"productName": "Bocken",
|
||||
"identifier": "org.bocken.app",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"build": {
|
||||
"devUrl": "http://192.168.1.4:5173",
|
||||
"frontendDist": "https://bocken.org"
|
||||
|
||||
@@ -4,13 +4,9 @@ import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favori
|
||||
export const load: PageServerLoad = async ({ fetch, locals, params }) => {
|
||||
const apiBase = `/api/${params.recipeLang}`;
|
||||
|
||||
const res_season = await fetch(`${apiBase}/items/icon/` + params.icon);
|
||||
const res_icons = await fetch(`/api/rezepte/items/icon`); // Icons are shared across languages
|
||||
const icons = await res_icons.json();
|
||||
const item_season = await res_season.json();
|
||||
|
||||
// Get user favorites and session
|
||||
const [userFavorites, session] = await Promise.all([
|
||||
const [item_season, icons, userFavorites, session] = await Promise.all([
|
||||
fetch(`${apiBase}/items/icon/` + params.icon).then(r => r.json()),
|
||||
fetch(`/api/rezepte/items/icon`).then(r => r.json()),
|
||||
getUserFavorites(fetch, locals),
|
||||
locals.auth()
|
||||
]);
|
||||
|
||||
@@ -6,15 +6,13 @@ import { dbConnect } from '$utils/db';
|
||||
export const GET: RequestHandler = async () => {
|
||||
await dbConnect();
|
||||
|
||||
// Fetch brief recipes (for lists/filtering)
|
||||
// Include images for thumbnail caching during offline sync
|
||||
const briefRecipes = await Recipe.find(
|
||||
{},
|
||||
'name short_name tags category icon description season dateModified images translations'
|
||||
).lean() as unknown as BriefRecipeType[];
|
||||
|
||||
// Fetch full recipes with populated base recipe references
|
||||
const fullRecipes = await Recipe.find({})
|
||||
// Fetch brief and full recipes in parallel
|
||||
const [briefRecipes, fullRecipes] = await Promise.all([
|
||||
Recipe.find(
|
||||
{},
|
||||
'name short_name tags category icon description season dateModified images translations'
|
||||
).lean() as unknown as Promise<BriefRecipeType[]>,
|
||||
Recipe.find({})
|
||||
.populate({
|
||||
path: 'ingredients.baseRecipeRef',
|
||||
select: 'short_name name ingredients translations',
|
||||
@@ -39,7 +37,8 @@ export const GET: RequestHandler = async () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
.lean() as unknown as RecipeModelType[];
|
||||
.lean() as unknown as Promise<RecipeModelType[]>
|
||||
]);
|
||||
|
||||
// Map populated refs to resolvedRecipe field (same as individual item endpoint)
|
||||
function mapBaseRecipeRefs(items: any[]): any[] {
|
||||
|
||||
@@ -20,20 +20,12 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
const startDate = new Date();
|
||||
startDate.setMonth(startDate.getMonth() - monthsBack);
|
||||
|
||||
const totalPayments = await Payment.countDocuments();
|
||||
const paymentsInRange = await Payment.countDocuments({
|
||||
date: {
|
||||
$gte: startDate,
|
||||
$lte: endDate
|
||||
}
|
||||
});
|
||||
const expensePayments = await Payment.countDocuments({
|
||||
date: {
|
||||
$gte: startDate,
|
||||
$lte: endDate
|
||||
},
|
||||
category: { $ne: 'settlement' }
|
||||
});
|
||||
const dateRange = { $gte: startDate, $lte: endDate };
|
||||
const [totalPayments, paymentsInRange, expensePayments] = await Promise.all([
|
||||
Payment.countDocuments(),
|
||||
Payment.countDocuments({ date: dateRange }),
|
||||
Payment.countDocuments({ date: dateRange, category: { $ne: 'settlement' } })
|
||||
]);
|
||||
|
||||
// Aggregate payments by month and category
|
||||
const pipeline = [
|
||||
|
||||
@@ -46,12 +46,11 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
exerciseQuery = exerciseQuery.sort({ name: 1 });
|
||||
}
|
||||
|
||||
const exercises = await exerciseQuery
|
||||
.limit(limit)
|
||||
.skip(offset)
|
||||
.select('exerciseId name gifUrl bodyPart equipment target difficulty');
|
||||
|
||||
const total = await Exercise.countDocuments(query);
|
||||
const [exercises, total] = await Promise.all([
|
||||
exerciseQuery.limit(limit).skip(offset)
|
||||
.select('exerciseId name gifUrl bodyPart equipment target difficulty'),
|
||||
Exercise.countDocuments(query)
|
||||
]);
|
||||
|
||||
return json({ exercises, total, limit, offset });
|
||||
} catch (error) {
|
||||
|
||||
@@ -73,62 +73,48 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
|
||||
};
|
||||
|
||||
async function computeStreak(username: string, weeklyGoal: number): Promise<number> {
|
||||
// Get weekly workout counts going back up to 2 years
|
||||
const cutoff = new Date();
|
||||
cutoff.setFullYear(cutoff.getFullYear() - 2);
|
||||
|
||||
const weeklyAgg = await WorkoutSession.aggregate([
|
||||
{
|
||||
$match: {
|
||||
createdBy: username,
|
||||
startTime: { $gte: cutoff }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
year: { $isoWeekYear: '$startTime' },
|
||||
week: { $isoWeek: '$startTime' }
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { '_id.year': -1, '_id.week': -1 }
|
||||
}
|
||||
]);
|
||||
|
||||
// Build a set of weeks that met the goal
|
||||
const metGoal = new Set<string>();
|
||||
for (const item of weeklyAgg) {
|
||||
if (item.count >= weeklyGoal) {
|
||||
metGoal.add(`${item._id.year}-${item._id.week}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Walk backwards week-by-week counting consecutive weeks that met the goal.
|
||||
// Current (incomplete) week counts if it already meets the goal, otherwise skip it.
|
||||
// Get weekly workout counts — only scan back enough to find the streak break.
|
||||
// Start with 13 weeks; if the streak fills the entire window, widen the search.
|
||||
const now = new Date();
|
||||
let streak = 0;
|
||||
|
||||
const currentKey = isoWeekKey(now);
|
||||
const currentWeekMet = metGoal.has(currentKey);
|
||||
for (let months = 3; months <= 24; months += 6) {
|
||||
const cutoff = new Date(now);
|
||||
cutoff.setMonth(cutoff.getMonth() - months);
|
||||
|
||||
// If current week already met: count it, then check previous weeks.
|
||||
// If not: start checking from last week (current week still in progress).
|
||||
if (currentWeekMet) streak = 1;
|
||||
const weeklyAgg = await WorkoutSession.aggregate([
|
||||
{ $match: { createdBy: username, startTime: { $gte: cutoff } } },
|
||||
{ $group: { _id: { year: { $isoWeekYear: '$startTime' }, week: { $isoWeek: '$startTime' } }, count: { $sum: 1 } } },
|
||||
{ $sort: { '_id.year': -1, '_id.week': -1 } }
|
||||
]);
|
||||
|
||||
for (let i = 1; i <= 104; i++) {
|
||||
const weekDate = new Date(now);
|
||||
weekDate.setDate(weekDate.getDate() - i * 7);
|
||||
if (metGoal.has(isoWeekKey(weekDate))) {
|
||||
streak++;
|
||||
} else {
|
||||
break;
|
||||
const metGoal = new Set<string>();
|
||||
for (const item of weeklyAgg) {
|
||||
if (item.count >= weeklyGoal) metGoal.add(`${item._id.year}-${item._id.week}`);
|
||||
}
|
||||
|
||||
let streak = 0;
|
||||
const currentKey = isoWeekKey(now);
|
||||
if (metGoal.has(currentKey)) streak = 1;
|
||||
|
||||
const maxWeeks = Math.ceil(months * 4.35);
|
||||
let foundBreak = false;
|
||||
for (let i = 1; i <= maxWeeks; i++) {
|
||||
const weekDate = new Date(now);
|
||||
weekDate.setDate(weekDate.getDate() - i * 7);
|
||||
if (metGoal.has(isoWeekKey(weekDate))) {
|
||||
streak++;
|
||||
} else {
|
||||
foundBreak = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found where the streak broke, we're done
|
||||
if (foundBreak || streak < maxWeeks) return streak;
|
||||
// Otherwise widen the window and try again
|
||||
}
|
||||
|
||||
return streak;
|
||||
return 104; // Max 2-year streak
|
||||
}
|
||||
|
||||
function isoWeekKey(date: Date): string {
|
||||
|
||||
@@ -22,13 +22,10 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
query.date = dateFilter;
|
||||
}
|
||||
|
||||
const measurements = await BodyMeasurement.find(query)
|
||||
.sort({ date: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
const total = await BodyMeasurement.countDocuments(query);
|
||||
const [measurements, total] = await Promise.all([
|
||||
BodyMeasurement.find(query).sort({ date: -1 }).skip(offset).limit(limit).lean(),
|
||||
BodyMeasurement.countDocuments(query)
|
||||
]);
|
||||
|
||||
return json({ measurements, total, limit, offset });
|
||||
};
|
||||
|
||||
@@ -8,38 +8,16 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
|
||||
// Get latest measurement that has each field
|
||||
const latestWithWeight = await BodyMeasurement.findOne({
|
||||
createdBy: user.nickname,
|
||||
weight: { $exists: true, $ne: null }
|
||||
})
|
||||
.sort({ date: -1 })
|
||||
.select('weight date')
|
||||
.lean();
|
||||
|
||||
const latestWithBodyFat = await BodyMeasurement.findOne({
|
||||
createdBy: user.nickname,
|
||||
bodyFatPercent: { $exists: true, $ne: null }
|
||||
})
|
||||
.sort({ date: -1 })
|
||||
.select('bodyFatPercent date')
|
||||
.lean();
|
||||
|
||||
const latestWithCalories = await BodyMeasurement.findOne({
|
||||
createdBy: user.nickname,
|
||||
caloricIntake: { $exists: true, $ne: null }
|
||||
})
|
||||
.sort({ date: -1 })
|
||||
.select('caloricIntake date')
|
||||
.lean();
|
||||
|
||||
const latestWithMeasurements = await BodyMeasurement.findOne({
|
||||
createdBy: user.nickname,
|
||||
measurements: { $exists: true, $ne: null }
|
||||
})
|
||||
.sort({ date: -1 })
|
||||
.select('measurements date')
|
||||
.lean();
|
||||
// Get latest measurement that has each field (parallel)
|
||||
const base = { createdBy: user.nickname };
|
||||
const [latestWithWeight, latestWithBodyFat, latestWithMeasurements] = await Promise.all([
|
||||
BodyMeasurement.findOne({ ...base, weight: { $exists: true, $ne: null } })
|
||||
.sort({ date: -1 }).select('weight date').lean(),
|
||||
BodyMeasurement.findOne({ ...base, bodyFatPercent: { $exists: true, $ne: null } })
|
||||
.sort({ date: -1 }).select('bodyFatPercent date').lean(),
|
||||
BodyMeasurement.findOne({ ...base, measurements: { $exists: true, $ne: null } })
|
||||
.sort({ date: -1 }).select('measurements date').lean(),
|
||||
]);
|
||||
|
||||
return json({
|
||||
weight: latestWithWeight
|
||||
@@ -48,9 +26,6 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
bodyFatPercent: latestWithBodyFat
|
||||
? { value: latestWithBodyFat.bodyFatPercent, date: latestWithBodyFat.date }
|
||||
: null,
|
||||
caloricIntake: latestWithCalories
|
||||
? { value: latestWithCalories.caloricIntake, date: latestWithCalories.date }
|
||||
: null,
|
||||
measurements: latestWithMeasurements
|
||||
? {
|
||||
value: latestWithMeasurements.measurements,
|
||||
|
||||
@@ -28,13 +28,12 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
const limit = parseInt(url.searchParams.get('limit') || '20');
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0');
|
||||
|
||||
const sessions = await WorkoutSession.find({ createdBy: session.user.nickname })
|
||||
.select('-exercises.gpsTrack -gpsTrack')
|
||||
.sort({ startTime: -1 })
|
||||
.limit(limit)
|
||||
.skip(offset);
|
||||
|
||||
const total = await WorkoutSession.countDocuments({ createdBy: session.user.nickname });
|
||||
const query = { createdBy: session.user.nickname };
|
||||
const [sessions, total] = await Promise.all([
|
||||
WorkoutSession.find(query).select('-exercises.gpsTrack -gpsTrack')
|
||||
.sort({ startTime: -1 }).limit(limit).skip(offset),
|
||||
WorkoutSession.countDocuments(query)
|
||||
]);
|
||||
|
||||
return json({ sessions, total, limit, offset });
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,31 +16,13 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
const tenWeeksAgo = new Date();
|
||||
tenWeeksAgo.setDate(tenWeeksAgo.getDate() - 70);
|
||||
|
||||
const totalWorkouts = await WorkoutSession.countDocuments({ createdBy: user.nickname });
|
||||
|
||||
const weeklyAgg = await WorkoutSession.aggregate([
|
||||
{
|
||||
$match: {
|
||||
createdBy: user.nickname,
|
||||
startTime: { $gte: tenWeeksAgo }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
year: { $isoWeekYear: '$startTime' },
|
||||
week: { $isoWeek: '$startTime' }
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { '_id.year': 1, '_id.week': 1 }
|
||||
}
|
||||
]);
|
||||
|
||||
// Fetch user demographics for kcal estimation
|
||||
const [goal, latestMeasurement] = await Promise.all([
|
||||
const [totalWorkouts, weeklyAgg, goal, latestMeasurement] = await Promise.all([
|
||||
WorkoutSession.countDocuments({ createdBy: user.nickname }),
|
||||
WorkoutSession.aggregate([
|
||||
{ $match: { createdBy: user.nickname, startTime: { $gte: tenWeeksAgo } } },
|
||||
{ $group: { _id: { year: { $isoWeekYear: '$startTime' }, week: { $isoWeek: '$startTime' } }, count: { $sum: 1 } } },
|
||||
{ $sort: { '_id.year': 1, '_id.week': 1 } }
|
||||
]),
|
||||
FitnessGoal.findOne({ username: user.nickname }).lean() as any,
|
||||
BodyMeasurement.findOne(
|
||||
{ createdBy: user.nickname, weight: { $ne: null } },
|
||||
@@ -57,10 +39,19 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
|
||||
// Lifetime totals: tonnage lifted + cardio km + kcal estimate
|
||||
// Use stored kcalEstimate when available; fall back to on-the-fly for legacy sessions
|
||||
const allSessions = await WorkoutSession.find(
|
||||
{ createdBy: user.nickname },
|
||||
{ 'exercises.exerciseId': 1, 'exercises.sets': 1, 'exercises.totalDistance': 1, kcalEstimate: 1 }
|
||||
).lean();
|
||||
const DISPLAY_LIMIT = 30;
|
||||
const SMA_LOOKBACK = 6; // w - 1 where w = 7 max
|
||||
const [allSessions, weightMeasurements] = await Promise.all([
|
||||
WorkoutSession.find(
|
||||
{ createdBy: user.nickname },
|
||||
{ 'exercises.exerciseId': 1, 'exercises.sets': 1, 'exercises.totalDistance': 1, kcalEstimate: 1 }
|
||||
).lean(),
|
||||
BodyMeasurement.find(
|
||||
{ createdBy: user.nickname, weight: { $ne: null } },
|
||||
{ date: 1, weight: 1, _id: 0 }
|
||||
).sort({ date: -1 }).limit(DISPLAY_LIMIT + SMA_LOOKBACK).lean()
|
||||
]);
|
||||
weightMeasurements.reverse(); // back to chronological order
|
||||
|
||||
let totalTonnage = 0;
|
||||
let totalCardioKm = 0;
|
||||
@@ -146,18 +137,6 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
upper: totalKcal + combinedMargin,
|
||||
};
|
||||
|
||||
// Fetch extra measurements beyond the display limit to fill the SMA lookback window
|
||||
const DISPLAY_LIMIT = 30;
|
||||
const SMA_LOOKBACK = 6; // w - 1 where w = 7 max
|
||||
const weightMeasurements = await BodyMeasurement.find(
|
||||
{ createdBy: user.nickname, weight: { $ne: null } },
|
||||
{ date: 1, weight: 1, _id: 0 }
|
||||
)
|
||||
.sort({ date: -1 })
|
||||
.limit(DISPLAY_LIMIT + SMA_LOOKBACK)
|
||||
.lean();
|
||||
weightMeasurements.reverse(); // back to chronological order
|
||||
|
||||
// Split into lookback-only (not displayed) and display portions
|
||||
const displayStart = Math.max(0, weightMeasurements.length - DISPLAY_LIMIT);
|
||||
|
||||
|
||||
@@ -9,25 +9,19 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
|
||||
await dbConnect();
|
||||
|
||||
// Completions per user
|
||||
const userStats = await TaskCompletion.aggregate([
|
||||
{ $group: { _id: '$completedBy', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
const [userStats, userStickers, recentCompletions] = await Promise.all([
|
||||
TaskCompletion.aggregate([
|
||||
{ $group: { _id: '$completedBy', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]),
|
||||
TaskCompletion.aggregate([
|
||||
{ $match: { stickerId: { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: { user: '$completedBy', sticker: '$stickerId' }, count: { $sum: 1 } } },
|
||||
{ $sort: { '_id.user': 1, count: -1 } }
|
||||
]),
|
||||
TaskCompletion.find().sort({ completedAt: -1 }).limit(500).lean()
|
||||
]);
|
||||
|
||||
// Stickers per user
|
||||
const userStickers = await TaskCompletion.aggregate([
|
||||
{ $match: { stickerId: { $exists: true, $ne: null } } },
|
||||
{ $group: { _id: { user: '$completedBy', sticker: '$stickerId' }, count: { $sum: 1 } } },
|
||||
{ $sort: { '_id.user': 1, count: -1 } }
|
||||
]);
|
||||
|
||||
// Recent completions (enough for ~3 months of calendar)
|
||||
const recentCompletions = await TaskCompletion.find()
|
||||
.sort({ completedAt: -1 })
|
||||
.limit(500)
|
||||
.lean();
|
||||
|
||||
return json({ userStats, userStickers, recentCompletions });
|
||||
};
|
||||
|
||||
|
||||
@@ -99,7 +99,6 @@
|
||||
const parts = [];
|
||||
if (m.weight != null) parts.push(`${m.weight} kg`);
|
||||
if (m.bodyFatPercent != null) parts.push(`${m.bodyFatPercent}% bf`);
|
||||
if (m.caloricIntake != null) parts.push(`${m.caloricIntake} kcal`);
|
||||
return parts.join(' · ') || t('body_measurements_only', lang);
|
||||
}
|
||||
</script>
|
||||
@@ -151,10 +150,6 @@
|
||||
<span class="stat-label">{t('body_fat', lang)}</span>
|
||||
<span class="stat-value">{latest.bodyFatPercent?.value ?? '—'}<small>%</small></span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">{t('calories', lang)}</span>
|
||||
<span class="stat-value">{latest.caloricIntake?.value ?? '—'} <small>kcal</small></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -303,7 +298,7 @@
|
||||
/* Latest */
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.stat-card {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
let formDate = $state(new Date().toISOString().slice(0, 10));
|
||||
let formWeight = $state('');
|
||||
let formBodyFat = $state('');
|
||||
let formCalories = $state('');
|
||||
let formNeck = $state('');
|
||||
let formShoulders = $state('');
|
||||
let formChest = $state('');
|
||||
@@ -35,9 +34,6 @@
|
||||
else body.weight = null;
|
||||
if (formBodyFat) body.bodyFatPercent = Number(formBodyFat);
|
||||
else body.bodyFatPercent = null;
|
||||
if (formCalories) body.caloricIntake = Number(formCalories);
|
||||
else body.caloricIntake = null;
|
||||
|
||||
/** @type {any} */
|
||||
const m = {};
|
||||
if (formNeck) m.neck = Number(formNeck);
|
||||
@@ -102,10 +98,6 @@
|
||||
<label for="m-bf">{t('body_fat_pct', lang)}</label>
|
||||
<input id="m-bf" type="number" step="0.1" bind:value={formBodyFat} placeholder="--" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="m-cal">{t('calories_kcal', lang)}</label>
|
||||
<input id="m-cal" type="number" bind:value={formCalories} placeholder="--" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{t('body_parts_cm', lang)}</h3>
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
let formDate = $state(m ? new Date(m.date).toISOString().slice(0, 10) : '');
|
||||
let formWeight = $state(m?.weight != null ? String(m.weight) : '');
|
||||
let formBodyFat = $state(m?.bodyFatPercent != null ? String(m.bodyFatPercent) : '');
|
||||
let formCalories = $state(m?.caloricIntake != null ? String(m.caloricIntake) : '');
|
||||
|
||||
const bp = m?.measurements ?? {};
|
||||
let formNeck = $state(bp.neck != null ? String(bp.neck) : '');
|
||||
let formShoulders = $state(bp.shoulders != null ? String(bp.shoulders) : '');
|
||||
@@ -43,9 +41,6 @@
|
||||
else body.weight = null;
|
||||
if (formBodyFat) body.bodyFatPercent = Number(formBodyFat);
|
||||
else body.bodyFatPercent = null;
|
||||
if (formCalories) body.caloricIntake = Number(formCalories);
|
||||
else body.caloricIntake = null;
|
||||
|
||||
/** @type {any} */
|
||||
const ms = {};
|
||||
if (formNeck) ms.neck = Number(formNeck);
|
||||
@@ -128,10 +123,6 @@
|
||||
<label for="m-bf">{t('body_fat_pct', lang)}</label>
|
||||
<input id="m-bf" type="number" step="0.1" bind:value={formBodyFat} placeholder="--" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="m-cal">{t('calories_kcal', lang)}</label>
|
||||
<input id="m-cal" type="number" bind:value={formCalories} placeholder="--" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>{t('body_parts_cm', lang)}</h3>
|
||||
|
||||
@@ -8,29 +8,33 @@ import mongoose from 'mongoose';
|
||||
export const load: PageServerLoad = async ({ fetch, url, locals }) => {
|
||||
const dateParam = url.searchParams.get('date') || new Date().toISOString().slice(0, 10);
|
||||
|
||||
const [foodRes, goalRes, weightRes] = await Promise.all([
|
||||
// Run all independent work in parallel: 3 API calls + workout kcal DB query
|
||||
const dayStart = new Date(dateParam + 'T00:00:00.000Z');
|
||||
const dayEnd = new Date(dateParam + 'T23:59:59.999Z');
|
||||
|
||||
const exercisePromise = (async () => {
|
||||
try {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
const sessions = await WorkoutSession.find({
|
||||
createdBy: user.nickname,
|
||||
startTime: { $gte: dayStart, $lte: dayEnd }
|
||||
}).select('kcalEstimate').lean();
|
||||
let kcal = 0;
|
||||
for (const s of sessions) {
|
||||
if (s.kcalEstimate?.kcal) kcal += s.kcalEstimate.kcal;
|
||||
}
|
||||
return kcal;
|
||||
} catch { return 0; }
|
||||
})();
|
||||
|
||||
const [foodRes, goalRes, weightRes, exerciseKcal] = await Promise.all([
|
||||
fetch(`/api/fitness/food-log?date=${dateParam}`),
|
||||
fetch('/api/fitness/goal'),
|
||||
fetch('/api/fitness/measurements/latest')
|
||||
fetch('/api/fitness/measurements/latest'),
|
||||
exercisePromise
|
||||
]);
|
||||
|
||||
// Fetch today's workout kcal burned
|
||||
let exerciseKcal = 0;
|
||||
try {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
const dayStart = new Date(dateParam + 'T00:00:00.000Z');
|
||||
const dayEnd = new Date(dateParam + 'T23:59:59.999Z');
|
||||
const sessions = await WorkoutSession.find({
|
||||
createdBy: user.nickname,
|
||||
startTime: { $gte: dayStart, $lte: dayEnd }
|
||||
}).select('kcalEstimate').lean();
|
||||
|
||||
for (const s of sessions) {
|
||||
if (s.kcalEstimate?.kcal) exerciseKcal += s.kcalEstimate.kcal;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const foodLog = foodRes.ok ? await foodRes.json() : { entries: [] };
|
||||
|
||||
// Resolve recipe images for entries with source=recipe
|
||||
|
||||
@@ -731,7 +731,7 @@
|
||||
{@const mealCal = mealEntries.reduce((s, e) => s + entryCalories(e), 0)}
|
||||
{@const meta = mealMeta[meal]}
|
||||
{@const MealSectionIcon = meta.icon}
|
||||
<div class="meal-section" style="--meal-color: {meta.color}; animation-delay: {mi * 60}ms">
|
||||
<div class="meal-section" style="--meal-color: {meta.color}">
|
||||
<div class="meal-header">
|
||||
<div class="meal-title">
|
||||
<div class="meal-icon">
|
||||
@@ -862,11 +862,6 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Entrance animations ── */
|
||||
@keyframes fade-up {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Date Navigator ── */
|
||||
.date-nav {
|
||||
@@ -874,7 +869,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
animation: fade-up 0.3s ease both;
|
||||
|
||||
}
|
||||
.date-btn {
|
||||
background: none;
|
||||
@@ -927,8 +922,8 @@
|
||||
padding: 1.25rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: relative;
|
||||
animation: fade-up 0.35s ease both;
|
||||
animation-delay: 50ms;
|
||||
|
||||
|
||||
}
|
||||
.daily-summary::before {
|
||||
content: '';
|
||||
@@ -1148,7 +1143,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
animation: fade-up 0.25s ease both;
|
||||
|
||||
}
|
||||
.micro-section h4 {
|
||||
margin: 0 0 0.4rem;
|
||||
@@ -1205,7 +1200,7 @@
|
||||
background: var(--color-surface);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
animation: fade-up 0.35s ease both;
|
||||
|
||||
}
|
||||
.no-goal-icon {
|
||||
display: flex;
|
||||
@@ -1247,7 +1242,7 @@
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
animation: fade-up 0.25s ease both;
|
||||
|
||||
}
|
||||
.preset-section {
|
||||
margin-bottom: 1rem;
|
||||
@@ -1370,7 +1365,7 @@
|
||||
|
||||
/* ── Meal Sections ── */
|
||||
.meal-section {
|
||||
animation: fade-up 0.35s ease both;
|
||||
|
||||
}
|
||||
.meal-header {
|
||||
display: flex;
|
||||
@@ -1568,7 +1563,7 @@
|
||||
border-radius: 10px;
|
||||
padding: 0.85rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
animation: fade-up 0.2s ease both;
|
||||
|
||||
}
|
||||
/* Search/food selection handled by FoodSearch component */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user