Files
platform/frontend-v2/src/routes/(app)/+page.svelte
Yusuf Suleman 3704687793 style: taste-skill refinements — tinted shadows, stagger animations, tactile press
Pre-redesign checkpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:51:56 -05:00

360 lines
9.6 KiB
Svelte

<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';
import IssuesModule from '$lib/components/dashboard/IssuesModule.svelte';
import TaskSlideOver from '$lib/components/dashboard/TaskSlideOver.svelte';
interface QuickTask {
id: string;
title: string;
startDate?: string;
dueDate?: string;
isAllDay?: boolean;
_projectName: string;
projectId: string;
_projectId: string;
}
const userName = $derived((page as any).data?.user?.display_name || 'there');
let inventoryIssueCount = $state(0);
let inventoryReviewCount = $state(0);
let budgetUncatCount = $state(0);
let budgetSpending = $state('');
let budgetIncome = $state('');
let fitnessCalRemaining = $state(0);
let fitnessCalLogged = $state(0);
let fitnessProtein = $state(0);
let fitnessCarbs = $state(0);
let headerTasks = $state<QuickTask[]>([]);
let headerOverdue = $state(0);
let headerTotalCount = $state(0);
let taskPanelOpen = $state(false);
function getGreeting(): string {
const h = new Date().getHours();
if (h < 12) return 'Good morning';
if (h < 17) return 'Good afternoon';
return 'Good evening';
}
function getDateString(): string {
return new Date().toLocaleDateString('en-US', {
weekday: 'long', month: 'long', day: 'numeric'
});
}
function formatTaskTime(t: QuickTask): string {
const d = t.startDate || t.dueDate;
if (!d || t.isAllDay) return '';
try {
const date = new Date(d);
const h = date.getHours(), m = date.getMinutes();
if (h === 0 && m === 0) return '';
const ampm = h >= 12 ? 'PM' : 'AM';
const hour = h % 12 || 12;
return m > 0 ? `${hour}:${String(m).padStart(2, '0')} ${ampm}` : `${hour} ${ampm}`;
} catch { return ''; }
}
async function loadTasks() {
try {
const res = await fetch('/api/tasks/today', { credentials: 'include' });
if (res.ok) {
const t = await res.json();
headerOverdue = t.overdueCount || 0;
const today = t.today || [];
const overdue = t.overdue || [];
headerTotalCount = today.length + overdue.length;
// First task for preview
headerTasks = [...overdue, ...today].slice(0, 1);
}
} catch { /* silent */ }
}
onMount(async () => {
const n = new Date();
const today = `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, '0')}-${String(n.getDate()).padStart(2, '0')}`;
try {
const [invRes, budgetRes, uncatRes, fitTotalsRes, fitGoalsRes] = await Promise.all([
fetch('/api/inventory/summary', { credentials: 'include' }),
fetch('/api/budget/summary', { credentials: 'include' }),
fetch('/api/budget/uncategorized-count', { credentials: 'include' }),
fetch(`/api/fitness/entries/totals?date=${today}`, { credentials: 'include' }),
fetch(`/api/fitness/goals/for-date?date=${today}`, { credentials: 'include' }),
]);
if (invRes.ok) {
const data = await invRes.json();
inventoryIssueCount = data.issueCount || 0;
inventoryReviewCount = data.reviewCount || 0;
}
if (budgetRes.ok) {
const data = await budgetRes.json();
budgetSpending = '$' + Math.abs(data.spendingDollars || 0).toLocaleString('en-US');
budgetIncome = '$' + Math.abs(data.incomeDollars || 0).toLocaleString('en-US');
}
if (uncatRes.ok) {
const data = await uncatRes.json();
budgetUncatCount = data.count || 0;
}
if (fitTotalsRes.ok) {
const t = await fitTotalsRes.json();
fitnessCalLogged = Math.round(t.total_calories || 0);
fitnessProtein = Math.round(t.total_protein || 0);
fitnessCarbs = Math.round(t.total_carbs || 0);
}
if (fitGoalsRes.ok) {
const g = await fitGoalsRes.json();
fitnessCalRemaining = Math.max(0, (g.calories || 2000) - fitnessCalLogged);
}
} catch { /* silent */ }
await loadTasks();
});
</script>
<div class="page">
<div class="app-surface">
<!-- Dashboard header with task trigger -->
<div class="dash-header">
<div class="dash-left">
<div class="dash-date">{getDateString()}</div>
<h1 class="dash-greeting">{getGreeting()}, <strong>{userName}</strong></h1>
</div>
<button class="task-trigger" onclick={() => taskPanelOpen = true}>
<div class="task-trigger-icon" class:has-overdue={headerOverdue > 0}>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<rect x="2" y="2" width="14" height="14" rx="3"/>
<path d="M6 9l2 2 4-4"/>
</svg>
{#if headerOverdue > 0}
<span class="task-trigger-dot"></span>
{/if}
</div>
<div class="task-trigger-text">
<span class="task-trigger-count">
{headerTotalCount} task{headerTotalCount !== 1 ? 's' : ''}
{#if headerOverdue > 0}
<span class="task-trigger-overdue">&middot; {headerOverdue} overdue</span>
{/if}
</span>
{#if headerTasks[0]}
<span class="task-trigger-next">
Next: {headerTasks[0].title}
{#if formatTaskTime(headerTasks[0])} &middot; {formatTaskTime(headerTasks[0])}{/if}
</span>
{:else}
<span class="task-trigger-next">All clear for today</span>
{/if}
</div>
<svg class="task-trigger-arrow" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M6 4l4 4-4 4"/></svg>
</button>
</div>
<TaskSlideOver bind:open={taskPanelOpen} onclose={() => { taskPanelOpen = false; loadTasks(); }} />
<!-- Action cards -->
<div class="action-cards stagger">
<DashboardActionCard
title="{budgetUncatCount} uncategorized transactions"
description="{budgetSpending} spent &middot; {budgetIncome} income"
action="Review"
variant="budget"
size="primary"
href="/budget"
/>
<DashboardActionCard
title="{inventoryIssueCount} issue{inventoryIssueCount !== 1 ? 's' : ''} · {inventoryReviewCount} needs review"
description="{inventoryIssueCount + inventoryReviewCount} items need attention"
action="View"
variant="inventory"
href="/inventory"
/>
<DashboardActionCard
title="{fitnessCalRemaining.toLocaleString()} calories remaining today"
description="{fitnessCalLogged.toLocaleString()} cal logged &middot; {fitnessProtein}g protein &middot; {fitnessCarbs}g carbs"
action="Log food"
variant="fitness"
href="/fitness"
/>
</div>
<!-- Modules -->
<div class="modules-grid">
<BudgetModule />
<div class="right-stack">
<FitnessModule />
<IssuesModule />
</div>
</div>
</div>
</div>
<style>
/* ── Dashboard header ── */
.dash-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--sp-6);
margin-bottom: var(--section-gap);
}
.dash-date {
font-size: 11px;
font-weight: 600;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: var(--sp-1);
}
.dash-greeting {
font-size: var(--text-2xl);
font-weight: 400;
color: var(--text-1);
line-height: var(--leading-tight);
letter-spacing: -0.02em;
margin: 0;
}
.dash-greeting strong { font-weight: 700; }
/* ── Task trigger button ── */
.task-trigger {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
border-radius: var(--radius);
background: var(--card);
border: 1px solid var(--border);
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
flex-shrink: 0;
max-width: 320px;
}
.task-trigger:hover {
box-shadow: var(--shadow-md);
border-color: var(--accent-border);
}
.task-trigger:active { transform: scale(0.98); }
.task-trigger-icon {
width: 36px;
height: 36px;
border-radius: var(--radius-md);
background: var(--accent-dim);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
}
.task-trigger-icon.has-overdue {
background: var(--error-dim);
color: var(--error);
}
.task-trigger-dot {
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
border-radius: var(--radius-full);
background: var(--error);
border: 2px solid var(--card);
}
.task-trigger-text {
flex: 1;
min-width: 0;
}
.task-trigger-count {
display: block;
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-1);
}
.task-trigger-overdue {
color: var(--error);
font-weight: 600;
}
.task-trigger-next {
display: block;
font-size: 12px;
color: var(--text-3);
margin-top: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.task-trigger-arrow {
color: var(--text-4);
flex-shrink: 0;
transition: transform 0.2s;
}
.task-trigger:hover .task-trigger-arrow {
transform: translateX(2px);
color: var(--accent);
}
/* ── Action cards ── */
.action-cards {
display: flex;
flex-direction: column;
gap: var(--sp-2);
margin-bottom: var(--section-gap);
}
/* ── Modules grid ── */
.modules-grid {
display: grid;
grid-template-columns: 1.3fr 0.7fr;
gap: var(--module-gap);
align-items: start;
}
.right-stack {
display: flex;
flex-direction: column;
gap: var(--module-gap);
}
/* ── Mobile ── */
@media (max-width: 900px) {
.dash-header {
flex-direction: column;
gap: var(--sp-3);
}
.dash-greeting {
font-size: var(--text-xl);
}
.task-trigger {
max-width: none;
width: 100%;
}
}
@media (max-width: 768px) {
.modules-grid {
grid-template-columns: 1fr;
}
.modules-grid > :global(*) {
min-width: 0;
max-width: 100%;
}
}
</style>