fix: move fitness goals to Settings, clean up mobile nav
- Removed fitness sub-pages from mobile More sheet (Food Library and Quick Meals are already tabs on the fitness page) - Added Fitness Goals section to Settings page with inline editing - Goals show current values with Edit Goals button - Edit mode: 2x2 grid with calorie/protein/carbs/fat inputs - Save calls PUT /api/fitness/goals - Works for both users independently
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { LayoutDashboard, DollarSign, Package, Activity, MoreVertical, MapPin, BookOpen, Library, Settings, Target, UtensilsCrossed, Zap } from '@lucide/svelte';
|
import { LayoutDashboard, DollarSign, Package, Activity, MoreVertical, MapPin, BookOpen, Library, Settings } from '@lucide/svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visibleApps?: string[];
|
visibleApps?: string[];
|
||||||
@@ -66,20 +66,6 @@
|
|||||||
Trips
|
Trips
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showApp('fitness')}
|
|
||||||
<a href="/fitness/foods" class="more-sheet-item" onclick={closeMore}>
|
|
||||||
<UtensilsCrossed size={20} />
|
|
||||||
Food Library
|
|
||||||
</a>
|
|
||||||
<a href="/fitness/goals" class="more-sheet-item" onclick={closeMore}>
|
|
||||||
<Target size={20} />
|
|
||||||
Fitness Goals
|
|
||||||
</a>
|
|
||||||
<a href="/fitness/templates" class="more-sheet-item" onclick={closeMore}>
|
|
||||||
<Zap size={20} />
|
|
||||||
Quick Meals
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
{#if showApp('reader')}
|
{#if showApp('reader')}
|
||||||
<a href="/reader" class="more-sheet-item" onclick={closeMore}>
|
<a href="/reader" class="more-sheet-item" onclick={closeMore}>
|
||||||
<BookOpen size={20} />
|
<BookOpen size={20} />
|
||||||
|
|||||||
@@ -14,6 +14,15 @@
|
|||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let darkMode = $state(false);
|
let darkMode = $state(false);
|
||||||
|
|
||||||
|
// Fitness goals
|
||||||
|
let goalCal = $state('');
|
||||||
|
let goalProtein = $state('');
|
||||||
|
let goalCarbs = $state('');
|
||||||
|
let goalFat = $state('');
|
||||||
|
let goalsLoaded = $state(false);
|
||||||
|
let editingGoals = $state(false);
|
||||||
|
let savingGoals = $state(false);
|
||||||
|
|
||||||
// Connect form state
|
// Connect form state
|
||||||
let showConnectForm = $state('');
|
let showConnectForm = $state('');
|
||||||
let connectToken = $state('');
|
let connectToken = $state('');
|
||||||
@@ -46,6 +55,21 @@
|
|||||||
}
|
}
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
|
// Load fitness goals
|
||||||
|
try {
|
||||||
|
const n = new Date();
|
||||||
|
const today = `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, '0')}-${String(n.getDate()).padStart(2, '0')}`;
|
||||||
|
const gRes = await fetch(`/api/fitness/goals/for-date?date=${today}`, { credentials: 'include' });
|
||||||
|
if (gRes.ok) {
|
||||||
|
const g = await gRes.json();
|
||||||
|
goalCal = String(Math.round(g.calories || 2000));
|
||||||
|
goalProtein = String(Math.round(g.protein || 150));
|
||||||
|
goalCarbs = String(Math.round(g.carbs || 200));
|
||||||
|
goalFat = String(Math.round(g.fat || 65));
|
||||||
|
goalsLoaded = true;
|
||||||
|
}
|
||||||
|
} catch { /* silent */ }
|
||||||
});
|
});
|
||||||
|
|
||||||
async function connectService(serviceId: string) {
|
async function connectService(serviceId: string) {
|
||||||
@@ -99,6 +123,27 @@
|
|||||||
document.documentElement.classList.toggle('dark', darkMode);
|
document.documentElement.classList.toggle('dark', darkMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveGoals() {
|
||||||
|
savingGoals = true;
|
||||||
|
try {
|
||||||
|
const n = new Date();
|
||||||
|
const today = `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, '0')}-${String(n.getDate()).padStart(2, '0')}`;
|
||||||
|
const res = await fetch('/api/fitness/goals', {
|
||||||
|
method: 'PUT', credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
calories: parseFloat(goalCal) || 2000,
|
||||||
|
protein: parseFloat(goalProtein) || 150,
|
||||||
|
carbs: parseFloat(goalCarbs) || 200,
|
||||||
|
fat: parseFloat(goalFat) || 65,
|
||||||
|
start_date: today,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (res.ok) editingGoals = false;
|
||||||
|
} catch { /* silent */ }
|
||||||
|
savingGoals = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
@@ -158,6 +203,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Fitness Goals -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<div class="section-title">Fitness Goals</div>
|
||||||
|
<div class="settings-card">
|
||||||
|
{#if editingGoals}
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="goal-edit-grid">
|
||||||
|
<div class="goal-edit-field">
|
||||||
|
<label class="goal-edit-label">Calories</label>
|
||||||
|
<input class="goal-edit-input" type="number" bind:value={goalCal} />
|
||||||
|
</div>
|
||||||
|
<div class="goal-edit-field">
|
||||||
|
<label class="goal-edit-label">Protein (g)</label>
|
||||||
|
<input class="goal-edit-input" type="number" bind:value={goalProtein} />
|
||||||
|
</div>
|
||||||
|
<div class="goal-edit-field">
|
||||||
|
<label class="goal-edit-label">Carbs (g)</label>
|
||||||
|
<input class="goal-edit-input" type="number" bind:value={goalCarbs} />
|
||||||
|
</div>
|
||||||
|
<div class="goal-edit-field">
|
||||||
|
<label class="goal-edit-label">Fat (g)</label>
|
||||||
|
<input class="goal-edit-input" type="number" bind:value={goalFat} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div></div>
|
||||||
|
<div class="goal-actions">
|
||||||
|
<button class="btn-text" onclick={() => editingGoals = false}>Cancel</button>
|
||||||
|
<button class="btn-save-goals" onclick={saveGoals} disabled={savingGoals}>{savingGoals ? 'Saving...' : 'Save'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-label">Calories</div>
|
||||||
|
<div class="setting-value">{goalsLoaded ? goalCal : '...'} kcal</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-label">Protein</div>
|
||||||
|
<div class="setting-value">{goalsLoaded ? goalProtein : '...'}g</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-label">Carbs</div>
|
||||||
|
<div class="setting-value">{goalsLoaded ? goalCarbs : '...'}g</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-label">Fat</div>
|
||||||
|
<div class="setting-value">{goalsLoaded ? goalFat : '...'}g</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div></div>
|
||||||
|
<button class="btn-edit-goals" onclick={() => editingGoals = true}>Edit Goals</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Service Connections -->
|
<!-- Service Connections -->
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<div class="section-title">Service Connections</div>
|
<div class="section-title">Service Connections</div>
|
||||||
@@ -321,6 +423,18 @@
|
|||||||
.toggle-thumb svg { width: 14px; height: 14px; color: var(--text-3); }
|
.toggle-thumb svg { width: 14px; height: 14px; color: var(--text-3); }
|
||||||
.theme-toggle.dark .toggle-thumb svg { color: var(--accent); }
|
.theme-toggle.dark .toggle-thumb svg { color: var(--accent); }
|
||||||
|
|
||||||
|
.goal-edit-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); width: 100%; }
|
||||||
|
.goal-edit-field { display: flex; flex-direction: column; gap: var(--sp-1); }
|
||||||
|
.goal-edit-label { font-size: var(--text-sm); color: var(--text-3); }
|
||||||
|
.goal-edit-input { padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--surface-secondary); color: var(--text-1); font-size: var(--text-md); font-family: var(--mono); }
|
||||||
|
.goal-edit-input:focus { outline: none; border-color: var(--accent); }
|
||||||
|
.goal-actions { display: flex; gap: var(--sp-2); }
|
||||||
|
.btn-text { background: none; border: none; color: var(--text-3); font-size: var(--text-sm); cursor: pointer; font-family: var(--font); }
|
||||||
|
.btn-text:hover { color: var(--text-1); }
|
||||||
|
.btn-save-goals { padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-md); background: var(--accent); color: white; border: none; font-size: var(--text-sm); font-weight: 600; cursor: pointer; font-family: var(--font); }
|
||||||
|
.btn-save-goals:disabled { opacity: 0.5; }
|
||||||
|
.btn-edit-goals { padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-md); background: var(--accent); color: white; border: none; font-size: var(--text-sm); font-weight: 600; cursor: pointer; font-family: var(--font); }
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.page-subtitle { font-size: var(--text-xl); }
|
.page-subtitle { font-size: var(--text-xl); }
|
||||||
.setting-row { flex-wrap: wrap; gap: var(--sp-2); }
|
.setting-row { flex-wrap: wrap; gap: var(--sp-2); }
|
||||||
|
|||||||
Reference in New Issue
Block a user