feat: multi-user support, goals editing, shared food library
Multi-user: - Madiha account with per-user nav visibility - Dashboard greeting uses actual user display name - Navbar and MobileTabBar accept visibleApps prop - Madiha sees: Dashboard, Trips, Fitness, Budget, Media (no Inventory, Reader) Goals editing: - Goals page now has Edit Goals mode with inline number inputs - Saves via PUT /api/fitness/goals - Shows "No goals set" state for new users Food library: - Default view shows all shared foods (not just user's recent) - Both users see the same food database - Cleaned up duplicates: archived Eggs (kept Egg), Green Grapes (kept Grapes), duplicate Bellwether Yogurt, Latte Macchiato (kept Madiha's Caramel Latte) Add to meal buttons: - "Add to breakfast/lunch/dinner/snack" now focuses the resolve input and sets the meal type so AI logs to the correct meal
This commit is contained in:
97
claude_code_partials_detailed_prompt.txt
Normal file
97
claude_code_partials_detailed_prompt.txt
Normal file
@@ -0,0 +1,97 @@
|
||||
Work in the `platform` repo and continue from the current remediation state.
|
||||
|
||||
Use Gitea issues as the source of truth:
|
||||
- `#1` umbrella
|
||||
- `#5` Gateway Trust Model
|
||||
- `#8` Dependency Security
|
||||
- `#9` Performance Hardening
|
||||
|
||||
Important instruction:
|
||||
- Do NOT rotate or change the admin password during this pass.
|
||||
- Treat admin password rotation as a final manual ops step after all code and config fixes are complete and verified.
|
||||
- If you mention password rotation in comments or summaries, explicitly mark it as "LAST STEP".
|
||||
|
||||
First, re-verify the repo state before changing anything. Do not trust prior summaries blindly.
|
||||
|
||||
Current verified status:
|
||||
- Completed: `#2`, `#3`, `#4`, `#6`, `#7`, `#10`
|
||||
- Partial: `#5`, `#8`, `#9`
|
||||
|
||||
Remaining work by issue:
|
||||
|
||||
`#5 Gateway Trust Model`
|
||||
Current state:
|
||||
- Token validation is improved and uses protected endpoints.
|
||||
- Inventory `/debug-nocodb` has been removed.
|
||||
- Inventory search sanitization is better.
|
||||
- The gateway still has a service-global trust model for gateway-key services.
|
||||
|
||||
What remains:
|
||||
- Re-check whether the current gateway-key service model is acceptable as-is or should be narrowed further.
|
||||
- If it stays, document it precisely and avoid claiming it was eliminated.
|
||||
- Review inventory and similar internal services for any remaining permissive/debug/admin-style surfaces.
|
||||
- Review whether service-global access should be limited at route level, method level, or by explicit allowlist.
|
||||
- Make sure issue comments and final summary describe the trust model accurately, not optimistically.
|
||||
|
||||
Acceptance bar:
|
||||
- No remaining accidental debug endpoint exposure.
|
||||
- Remaining gateway-key trust assumptions are explicit, minimal, and documented.
|
||||
- No false claim that per-user auth exists where it does not.
|
||||
|
||||
`#8 Dependency Security`
|
||||
Current state:
|
||||
- Budget dependency audit is clean.
|
||||
- `.gitea/workflows/security.yml` exists.
|
||||
|
||||
What remains:
|
||||
- Review the workflow for correctness and realism.
|
||||
- Tighten the workflow if needed so repo-side enforcement is actually meaningful.
|
||||
- Verify whether secret scanning and dependency checks cover the important paths.
|
||||
- Do not mark this issue complete if a Gitea Actions runner is still required for execution.
|
||||
- Clearly separate "repo-side complete" from "operationally active".
|
||||
|
||||
Acceptance bar:
|
||||
- Workflow file is committed and sane.
|
||||
- Remaining runner dependency is clearly documented.
|
||||
- Issue remains partial or blocked if execution infrastructure is missing.
|
||||
|
||||
`#9 Performance Hardening`
|
||||
Current state:
|
||||
- Gateway dashboard response is cached.
|
||||
- Budget summary is cached.
|
||||
- Inventory `/issues` and `/needs-review-count` no longer full-scan all rows.
|
||||
|
||||
What remains:
|
||||
- Re-check inventory endpoints for any other repeated full-table fetches.
|
||||
- Re-check budget endpoints for repeated account fan-out, especially `/transactions/recent`.
|
||||
- If Actual Budget API forces per-account queries, document that constraint explicitly.
|
||||
- Prefer targeted improvements such as short-TTL caching, narrower query windows, or reused lookups over broad refactors.
|
||||
- Do not mark this issue complete unless the remaining hot paths are either fixed or clearly bounded and documented.
|
||||
|
||||
Acceptance bar:
|
||||
- The worst remaining repeated-scan or repeated-fan-out paths are either reduced or documented with clear justification.
|
||||
- Final status does not overstate completion.
|
||||
|
||||
Instructions:
|
||||
- Make minimal, production-oriented fixes.
|
||||
- Preserve unrelated user changes.
|
||||
- After each issue-sized change:
|
||||
- verify it with direct checks
|
||||
- comment on the relevant Gitea issue with:
|
||||
- what changed
|
||||
- files touched
|
||||
- verification performed
|
||||
- what remains
|
||||
- Do not close `#5`, `#8`, or `#9` unless the actual code and behavior support it.
|
||||
- If an issue is still partial, say so directly.
|
||||
- Avoid renaming something and then claiming the underlying architectural concern is solved.
|
||||
|
||||
Manual ops note:
|
||||
- Admin password rotation is intentionally deferred.
|
||||
- If referenced, mark it exactly as: `LAST STEP: rotate admin password after all remaining fixes are complete and verified.`
|
||||
|
||||
Final output format:
|
||||
- `Completed:`
|
||||
- `Partial:`
|
||||
- `Blocked:`
|
||||
- `Manual ops actions:`
|
||||
@@ -2,6 +2,15 @@
|
||||
import { page } from '$app/state';
|
||||
import { LayoutDashboard, DollarSign, Package, Activity, MoreVertical, MapPin, BookOpen, Library, Settings } from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
visibleApps?: string[];
|
||||
}
|
||||
let { visibleApps = ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media'] }: Props = $props();
|
||||
|
||||
function showApp(id: string): boolean {
|
||||
return visibleApps.includes(id);
|
||||
}
|
||||
|
||||
let moreOpen = $state(false);
|
||||
|
||||
function isActive(path: string): boolean {
|
||||
@@ -20,18 +29,24 @@
|
||||
<LayoutDashboard size={22} />
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/budget" class="mobile-tab" class:active={isActive('/budget')}>
|
||||
<DollarSign size={22} />
|
||||
Budget
|
||||
</a>
|
||||
<a href="/inventory" class="mobile-tab" class:active={isActive('/inventory')}>
|
||||
<Package size={22} />
|
||||
Inventory
|
||||
</a>
|
||||
<a href="/fitness" class="mobile-tab" class:active={isActive('/fitness')}>
|
||||
<Activity size={22} />
|
||||
Fitness
|
||||
</a>
|
||||
{#if showApp('budget')}
|
||||
<a href="/budget" class="mobile-tab" class:active={isActive('/budget')}>
|
||||
<DollarSign size={22} />
|
||||
Budget
|
||||
</a>
|
||||
{/if}
|
||||
{#if showApp('inventory')}
|
||||
<a href="/inventory" class="mobile-tab" class:active={isActive('/inventory')}>
|
||||
<Package size={22} />
|
||||
Inventory
|
||||
</a>
|
||||
{/if}
|
||||
{#if showApp('fitness')}
|
||||
<a href="/fitness" class="mobile-tab" class:active={isActive('/fitness')}>
|
||||
<Activity size={22} />
|
||||
Fitness
|
||||
</a>
|
||||
{/if}
|
||||
<button class="mobile-tab" class:active={moreOpen} onclick={() => moreOpen = true}>
|
||||
<MoreVertical size={22} />
|
||||
More
|
||||
@@ -45,18 +60,24 @@
|
||||
<div class="more-sheet-overlay open" onclick={(e) => { if (e.target === e.currentTarget) closeMore(); }} onkeydown={() => {}}>
|
||||
<div class="more-sheet">
|
||||
<div class="more-sheet-handle"></div>
|
||||
<a href="/trips" class="more-sheet-item" onclick={closeMore}>
|
||||
<MapPin size={20} />
|
||||
Trips
|
||||
</a>
|
||||
<a href="/reader" class="more-sheet-item" onclick={closeMore}>
|
||||
<BookOpen size={20} />
|
||||
Reader
|
||||
</a>
|
||||
<a href="/media" class="more-sheet-item" onclick={closeMore}>
|
||||
<Library size={20} />
|
||||
Media
|
||||
</a>
|
||||
{#if showApp('trips')}
|
||||
<a href="/trips" class="more-sheet-item" onclick={closeMore}>
|
||||
<MapPin size={20} />
|
||||
Trips
|
||||
</a>
|
||||
{/if}
|
||||
{#if showApp('reader')}
|
||||
<a href="/reader" class="more-sheet-item" onclick={closeMore}>
|
||||
<BookOpen size={20} />
|
||||
Reader
|
||||
</a>
|
||||
{/if}
|
||||
{#if showApp('media')}
|
||||
<a href="/media" class="more-sheet-item" onclick={closeMore}>
|
||||
<Library size={20} />
|
||||
Media
|
||||
</a>
|
||||
{/if}
|
||||
<a href="/settings" class="more-sheet-item" onclick={closeMore}>
|
||||
<Settings size={20} />
|
||||
Settings
|
||||
|
||||
@@ -5,9 +5,14 @@
|
||||
|
||||
interface Props {
|
||||
onOpenCommand?: () => void;
|
||||
visibleApps?: string[];
|
||||
}
|
||||
|
||||
let { onOpenCommand }: Props = $props();
|
||||
let { onOpenCommand, visibleApps = ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media'] }: Props = $props();
|
||||
|
||||
function showApp(id: string): boolean {
|
||||
return visibleApps.includes(id);
|
||||
}
|
||||
|
||||
let tripsOpen = $state(false);
|
||||
let fitnessOpen = $state(false);
|
||||
@@ -34,30 +39,40 @@
|
||||
<div class="navbar-links">
|
||||
<a href="/" class="navbar-link" class:active={page.url.pathname === '/'}>Dashboard</a>
|
||||
|
||||
<!-- Trips dropdown -->
|
||||
<a href="/trips" class="navbar-link" class:active={isActive('/trips')}>Trips</a>
|
||||
{#if showApp('trips')}
|
||||
<a href="/trips" class="navbar-link" class:active={isActive('/trips')}>Trips</a>
|
||||
{/if}
|
||||
|
||||
<!-- Fitness dropdown -->
|
||||
<div class="nav-dropdown" role="menu">
|
||||
<button
|
||||
class="navbar-link"
|
||||
class:active={isActive('/fitness')}
|
||||
onclick={(e) => { e.stopPropagation(); fitnessOpen = !fitnessOpen; tripsOpen = false; }}
|
||||
>Fitness</button>
|
||||
{#if fitnessOpen}
|
||||
<div class="nav-dropdown-menu" onclick={(e) => e.stopPropagation()}>
|
||||
<a href="/fitness" class="nav-dropdown-item" onclick={closeDropdowns}>Dashboard</a>
|
||||
<a href="/fitness/foods" class="nav-dropdown-item" onclick={closeDropdowns}>Foods</a>
|
||||
<a href="/fitness/goals" class="nav-dropdown-item" onclick={closeDropdowns}>Goals</a>
|
||||
<a href="/fitness/templates" class="nav-dropdown-item" onclick={closeDropdowns}>Templates</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showApp('fitness')}
|
||||
<div class="nav-dropdown" role="menu">
|
||||
<button
|
||||
class="navbar-link"
|
||||
class:active={isActive('/fitness')}
|
||||
onclick={(e) => { e.stopPropagation(); fitnessOpen = !fitnessOpen; tripsOpen = false; }}
|
||||
>Fitness</button>
|
||||
{#if fitnessOpen}
|
||||
<div class="nav-dropdown-menu" onclick={(e) => e.stopPropagation()}>
|
||||
<a href="/fitness" class="nav-dropdown-item" onclick={closeDropdowns}>Dashboard</a>
|
||||
<a href="/fitness/foods" class="nav-dropdown-item" onclick={closeDropdowns}>Foods</a>
|
||||
<a href="/fitness/goals" class="nav-dropdown-item" onclick={closeDropdowns}>Goals</a>
|
||||
<a href="/fitness/templates" class="nav-dropdown-item" onclick={closeDropdowns}>Templates</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<a href="/inventory" class="navbar-link" class:active={isActive('/inventory')}>Inventory</a>
|
||||
<a href="/budget" class="navbar-link" class:active={isActive('/budget')}>Budget</a>
|
||||
<a href="/reader" class="navbar-link" class:active={isActive('/reader')}>Reader</a>
|
||||
<a href="/media" class="navbar-link" class:active={isActive('/media')}>Media</a>
|
||||
{#if showApp('inventory')}
|
||||
<a href="/inventory" class="navbar-link" class:active={isActive('/inventory')}>Inventory</a>
|
||||
{/if}
|
||||
{#if showApp('budget')}
|
||||
<a href="/budget" class="navbar-link" class:active={isActive('/budget')}>Budget</a>
|
||||
{/if}
|
||||
{#if showApp('reader')}
|
||||
<a href="/reader" class="navbar-link" class:active={isActive('/reader')}>Reader</a>
|
||||
{/if}
|
||||
{#if showApp('media')}
|
||||
<a href="/media" class="navbar-link" class:active={isActive('/media')}>Media</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button class="search-trigger" onclick={onOpenCommand}>
|
||||
|
||||
@@ -18,7 +18,15 @@ export const load: LayoutServerLoad = async ({ cookies, url }) => {
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.authenticated) {
|
||||
return { user: data.user };
|
||||
// Per-user nav visibility — hide apps not relevant to this user
|
||||
// Apps not in this list are hidden from nav (but still accessible via URL)
|
||||
const allApps = ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media'];
|
||||
const hiddenByUser: Record<string, string[]> = {
|
||||
'madiha': ['inventory', 'reader'],
|
||||
};
|
||||
const hidden = hiddenByUser[data.user.username] || [];
|
||||
const visibleApps = allApps.filter(a => !hidden.includes(a));
|
||||
return { user: data.user, visibleApps };
|
||||
}
|
||||
}
|
||||
} catch { /* gateway down — let client handle */ }
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import MobileTabBar from '$lib/components/layout/MobileTabBar.svelte';
|
||||
import CommandPalette from '$lib/components/layout/CommandPalette.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
let { children, data } = $props();
|
||||
let commandOpen = $state(false);
|
||||
const visibleApps = data?.visibleApps || ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media'];
|
||||
const userName = data?.user?.display_name || '';
|
||||
|
||||
function openCommand() {
|
||||
commandOpen = true;
|
||||
@@ -25,13 +27,13 @@
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="app">
|
||||
<Navbar onOpenCommand={openCommand} />
|
||||
<Navbar onOpenCommand={openCommand} {visibleApps} />
|
||||
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<MobileTabBar />
|
||||
<MobileTabBar {visibleApps} />
|
||||
<CommandPalette bind:open={commandOpen} onclose={closeCommand} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import DashboardActionCard from '$lib/components/dashboard/DashboardActionCard.svelte';
|
||||
import BudgetModule from '$lib/components/dashboard/BudgetModule.svelte';
|
||||
import FitnessModule from '$lib/components/dashboard/FitnessModule.svelte';
|
||||
|
||||
const userName = $derived((page as any).data?.user?.display_name || 'there');
|
||||
import IssuesModule from '$lib/components/dashboard/IssuesModule.svelte';
|
||||
|
||||
let inventoryIssueCount = $state(0);
|
||||
@@ -59,7 +62,7 @@
|
||||
<div class="app-surface">
|
||||
<div class="page-header">
|
||||
<div class="page-title">Dashboard</div>
|
||||
<div class="page-greeting">Good to see you, <strong>Yusuf</strong></div>
|
||||
<div class="page-greeting">Good to see you, <strong>{userName}</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="action-cards">
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
try {
|
||||
const url = query
|
||||
? `/api/fitness/foods/search?q=${encodeURIComponent(query)}&limit=30`
|
||||
: `/api/fitness/foods/recent?limit=30`;
|
||||
: `/api/fitness/foods?limit=100`;
|
||||
const res = await fetch(url, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const raw = await res.json();
|
||||
@@ -959,7 +959,7 @@
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<button class="add-food-btn">
|
||||
<button class="add-food-btn" onclick={() => { resolveMeal = meal; const input = document.querySelector('.resolve-input') as HTMLInputElement; if (input) { input.focus(); input.placeholder = `Add to ${meal}...`; } }}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Add to {meal}
|
||||
</button>
|
||||
|
||||
@@ -1,34 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let goals = $state([
|
||||
{ label: 'Calories', value: '...', unit: 'kcal/day' },
|
||||
{ label: 'Protein', value: '...', unit: 'grams/day' },
|
||||
{ label: 'Carbs', value: '...', unit: 'grams/day' },
|
||||
{ label: 'Fat', value: '...', unit: 'grams/day' }
|
||||
]);
|
||||
let calories = $state(2000);
|
||||
let protein = $state(150);
|
||||
let carbs = $state(200);
|
||||
let fat = $state(65);
|
||||
let startDate = $state('');
|
||||
let loading = $state(true);
|
||||
let editing = $state(false);
|
||||
let saving = $state(false);
|
||||
let hasGoal = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
// Edit state
|
||||
let editCal = $state('2000');
|
||||
let editProtein = $state('150');
|
||||
let editCarbs = $state('200');
|
||||
let editFat = $state('65');
|
||||
|
||||
function today(): string {
|
||||
const n = new Date();
|
||||
return `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, '0')}-${String(n.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
async function loadGoals() {
|
||||
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/for-date?date=${today}`, { credentials: 'include' });
|
||||
const res = await fetch(`/api/fitness/goals/for-date?date=${today()}`, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const g = await res.json();
|
||||
goals = [
|
||||
{ label: 'Calories', value: (g.calories || 0).toLocaleString(), unit: 'kcal/day' },
|
||||
{ label: 'Protein', value: String(g.protein || 0), unit: 'grams/day' },
|
||||
{ label: 'Carbs', value: String(g.carbs || 0), unit: 'grams/day' },
|
||||
{ label: 'Fat', value: String(g.fat || 0), unit: 'grams/day' },
|
||||
];
|
||||
calories = g.calories || 2000;
|
||||
protein = g.protein || 150;
|
||||
carbs = g.carbs || 200;
|
||||
fat = g.fat || 65;
|
||||
hasGoal = true;
|
||||
if (g.start_date) {
|
||||
const d = new Date(g.start_date + 'T00:00:00');
|
||||
startDate = d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
}
|
||||
} catch { /* silent */ }
|
||||
});
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function startEdit() {
|
||||
editCal = String(Math.round(calories));
|
||||
editProtein = String(Math.round(protein));
|
||||
editCarbs = String(Math.round(carbs));
|
||||
editFat = String(Math.round(fat));
|
||||
editing = true;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing = false;
|
||||
}
|
||||
|
||||
async function saveGoals() {
|
||||
saving = true;
|
||||
try {
|
||||
const res = await fetch('/api/fitness/goals', {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
calories: parseFloat(editCal) || 2000,
|
||||
protein: parseFloat(editProtein) || 150,
|
||||
carbs: parseFloat(editCarbs) || 200,
|
||||
fat: parseFloat(editFat) || 65,
|
||||
start_date: today(),
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
editing = false;
|
||||
await loadGoals();
|
||||
}
|
||||
} catch { /* silent */ }
|
||||
saving = false;
|
||||
}
|
||||
|
||||
onMount(loadGoals);
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
@@ -41,117 +89,113 @@
|
||||
<div class="module">
|
||||
<div class="module-header">
|
||||
<div class="module-title">CURRENT GOALS</div>
|
||||
<button class="module-action">Edit Goals</button>
|
||||
{#if !editing}
|
||||
<button class="module-action" onclick={startEdit}>Edit Goals</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="goals-grid">
|
||||
{#each goals as goal}
|
||||
<div class="goal-card">
|
||||
<div class="goal-label">{goal.label}</div>
|
||||
<div class="goal-value">{goal.value}</div>
|
||||
<div class="goal-unit">{goal.unit}</div>
|
||||
{#if editing}
|
||||
<div class="edit-grid">
|
||||
<div class="edit-field">
|
||||
<label class="edit-label">Calories (kcal/day)</label>
|
||||
<input class="edit-input" type="number" bind:value={editCal} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label">Protein (g/day)</label>
|
||||
<input class="edit-input" type="number" bind:value={editProtein} />
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label">Carbs (g/day)</label>
|
||||
<input class="edit-input" type="number" bind:value={editCarbs} />
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label">Fat (g/day)</label>
|
||||
<input class="edit-input" type="number" bind:value={editFat} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-actions">
|
||||
<button class="btn-cancel" onclick={cancelEdit}>Cancel</button>
|
||||
<button class="btn-save" onclick={saveGoals} disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save Goals'}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="goals-grid">
|
||||
<div class="goal-card">
|
||||
<div class="goal-label">Calories</div>
|
||||
<div class="goal-value">{loading ? '...' : Math.round(calories).toLocaleString()}</div>
|
||||
<div class="goal-unit">kcal/day</div>
|
||||
</div>
|
||||
<div class="goal-card">
|
||||
<div class="goal-label">Protein</div>
|
||||
<div class="goal-value">{loading ? '...' : Math.round(protein)}</div>
|
||||
<div class="goal-unit">grams/day</div>
|
||||
</div>
|
||||
<div class="goal-card">
|
||||
<div class="goal-label">Carbs</div>
|
||||
<div class="goal-value">{loading ? '...' : Math.round(carbs)}</div>
|
||||
<div class="goal-unit">grams/day</div>
|
||||
</div>
|
||||
<div class="goal-card">
|
||||
<div class="goal-label">Fat</div>
|
||||
<div class="goal-value">{loading ? '...' : Math.round(fat)}</div>
|
||||
<div class="goal-unit">grams/day</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="start-date">
|
||||
<span class="start-date-label">Start date</span>
|
||||
<span class="start-date-value">{startDate || '—'}</span>
|
||||
</div>
|
||||
{#if startDate}
|
||||
<div class="start-date">
|
||||
<span class="start-date-label">Active since</span>
|
||||
<span class="start-date-value">{startDate}</span>
|
||||
</div>
|
||||
{:else if !loading && !hasGoal}
|
||||
<div class="no-goal">No goals set yet. Tap "Edit Goals" to get started.</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-subtitle {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 300;
|
||||
color: var(--text-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.page-subtitle { font-size: var(--text-2xl); font-weight: 300; color: var(--text-1); line-height: 1.2; }
|
||||
|
||||
.module {
|
||||
background: var(--card);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--card-pad-primary);
|
||||
box-shadow: var(--card-shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.module-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--sp-5);
|
||||
}
|
||||
.module-title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text-3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.module-action {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.module { background: var(--card); border-radius: var(--radius); padding: var(--card-pad-primary); box-shadow: var(--card-shadow); border: 1px solid var(--border); }
|
||||
.module-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--sp-5); }
|
||||
.module-title { font-size: var(--text-sm); font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.module-action { font-size: var(--text-sm); color: var(--accent); font-weight: 500; cursor: pointer; background: none; border: none; padding: 0; }
|
||||
.module-action:hover { text-decoration: underline; }
|
||||
|
||||
.goals-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--row-gap);
|
||||
margin-bottom: var(--sp-5);
|
||||
}
|
||||
.goal-card {
|
||||
background: var(--surface-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--sp-4);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.goal-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: var(--sp-2);
|
||||
}
|
||||
.goal-value {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 500;
|
||||
font-family: var(--mono);
|
||||
color: var(--text-1);
|
||||
line-height: 1;
|
||||
}
|
||||
.goal-unit {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-3);
|
||||
margin-top: var(--sp-1);
|
||||
}
|
||||
.goals-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--row-gap); margin-bottom: var(--sp-5); }
|
||||
.goal-card { background: var(--surface-secondary); border-radius: var(--radius-sm); padding: var(--sp-4); border: 1px solid var(--border); }
|
||||
.goal-label { font-size: var(--text-sm); font-weight: 500; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: var(--sp-2); }
|
||||
.goal-value { font-size: var(--text-xl); font-weight: 500; font-family: var(--mono); color: var(--text-1); line-height: 1; }
|
||||
.goal-unit { font-size: var(--text-sm); color: var(--text-3); margin-top: var(--sp-1); }
|
||||
|
||||
.start-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: var(--sp-4);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.start-date-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-3);
|
||||
}
|
||||
.start-date-value {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
.start-date { display: flex; align-items: center; justify-content: space-between; padding-top: var(--sp-4); border-top: 1px solid var(--border); }
|
||||
.start-date-label { font-size: var(--text-sm); color: var(--text-3); }
|
||||
.start-date-value { font-size: var(--text-sm); color: var(--text-2); font-weight: 500; }
|
||||
.no-goal { font-size: var(--text-sm); color: var(--text-3); text-align: center; padding: var(--sp-4) 0; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-subtitle { font-size: var(--text-xl); }
|
||||
.edit-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); margin-bottom: var(--sp-5); }
|
||||
.edit-field { display: flex; flex-direction: column; gap: var(--sp-1); }
|
||||
.edit-label { font-size: var(--text-sm); font-weight: 500; color: var(--text-3); }
|
||||
.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-lg); font-family: var(--mono);
|
||||
}
|
||||
.edit-input:focus { outline: none; border-color: var(--accent); }
|
||||
.edit-actions { display: flex; gap: var(--sp-2); justify-content: flex-end; }
|
||||
.btn-cancel {
|
||||
padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-md);
|
||||
background: var(--card-secondary); color: var(--text-2); border: 1px solid var(--border);
|
||||
font-size: var(--text-sm); font-weight: 500; cursor: pointer; font-family: var(--font);
|
||||
}
|
||||
.btn-save {
|
||||
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:disabled { opacity: 0.5; }
|
||||
|
||||
@media (max-width: 768px) { .page-subtitle { font-size: var(--text-xl); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user