From 96f1d0a6e54d591bd0177806408f3d9cb86ae8ad Mon Sep 17 00:00:00 2001 From: Santhosh Janardhanan Date: Wed, 18 Feb 2026 18:14:57 -0500 Subject: [PATCH] feat(dashboard): Enhance dashboard with PageHeader, StatCard, and auth fixes - Create PageHeader component with title, description, and action slots - Create StatCard component with trend indicators and icons - Update dashboard with KPI cards, Quick Actions, and Allocation Preview - Polish login page with branding and centered layout - Fix auth redirect: authenticated users accessing /login go to dashboard - Fix page refresh: auth state persists, no blank page - Fix sidebar: visible after login, toggle works, state persists - Fix CSS import: add app.css to layout, fix DaisyUI import path - Fix breadcrumbs: home icon links to /dashboard - Add comprehensive E2E and unit tests Refs: openspec/changes/p03-dashboard-enhancement Closes: p03-dashboard-enhancement --- frontend/src/app.css | 2 +- frontend/src/app.html | 2 +- .../lib/components/common/StatCard.spec.ts | 110 ++++++++++++++ .../src/lib/components/common/StatCard.svelte | 59 ++++++++ .../lib/components/layout/Breadcrumbs.svelte | 4 +- .../lib/components/layout/PageHeader.spec.ts | 36 +++++ .../lib/components/layout/PageHeader.svelte | 27 ++++ .../src/lib/components/layout/Sidebar.svelte | 23 ++- frontend/src/lib/stores/layout.ts | 53 +++++-- frontend/src/routes/+layout.svelte | 19 +-- frontend/src/routes/dashboard/+page.svelte | 139 +++++++++++++----- frontend/src/routes/login/+page.svelte | 41 ++++-- frontend/src/routes/login/+page.ts | 15 ++ frontend/tests/component/pageheader.spec.ts | 8 + frontend/tests/component/statcard.spec.ts | 8 + frontend/tests/e2e/auth.spec.js | 20 +++ frontend/tests/e2e/dashboard.spec.ts | 49 ++++++ frontend/tests/e2e/layout.spec.ts | 115 +++++++++++++++ frontend/tests/e2e/login.spec.ts | 62 ++++++++ frontend/tests/unit/layout.store.test.ts | 5 +- .../p03-dashboard-enhancement/tasks.md | 78 +++++----- openspec/config.yaml | 33 ++--- 22 files changed, 770 insertions(+), 138 deletions(-) create mode 100644 frontend/src/lib/components/common/StatCard.spec.ts create mode 100644 frontend/src/lib/components/common/StatCard.svelte create mode 100644 frontend/src/lib/components/layout/PageHeader.spec.ts create mode 100644 frontend/src/lib/components/layout/PageHeader.svelte create mode 100644 frontend/tests/component/pageheader.spec.ts create mode 100644 frontend/tests/component/statcard.spec.ts create mode 100644 frontend/tests/e2e/dashboard.spec.ts create mode 100644 frontend/tests/e2e/login.spec.ts diff --git a/frontend/src/app.css b/frontend/src/app.css index 0477fb42..c5f267f6 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,5 +1,5 @@ @import "tailwindcss"; -@import "daisyui"; +@import "daisyui/daisyui.css"; :root { --sidebar-width-expanded: 240px; diff --git a/frontend/src/app.html b/frontend/src/app.html index a95435fa..53671bdb 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/frontend/src/lib/components/common/StatCard.spec.ts b/frontend/src/lib/components/common/StatCard.spec.ts new file mode 100644 index 00000000..ee33708f --- /dev/null +++ b/frontend/src/lib/components/common/StatCard.spec.ts @@ -0,0 +1,110 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import StatCard from './StatCard.svelte'; +import { Folder } from 'lucide-svelte'; + +describe('StatCard', () => { + it('renders value', () => { + render(StatCard, { + props: { + title: 'Active Projects', + value: 14 + } + }); + expect(screen.getByText('14')).toBeInTheDocument(); + expect(screen.getByText('Active Projects')).toBeInTheDocument(); + }); + + it('renders string value', () => { + render(StatCard, { + props: { + title: 'Utilization', + value: '87%' + } + }); + expect(screen.getByText('87%')).toBeInTheDocument(); + }); + + it('renders description when provided', () => { + render(StatCard, { + props: { + title: 'Team Members', + value: 8, + description: 'active' + } + }); + expect(screen.getByText('active')).toBeInTheDocument(); + }); + + it('applies correct trend colors for up trend', () => { + const { container } = render(StatCard, { + props: { + title: 'Projects', + value: 14, + trend: 'up', + trendValue: '+2' + } + }); + + const trendElement = container.querySelector('.text-success'); + expect(trendElement).toBeInTheDocument(); + expect(screen.getByText('+2')).toBeInTheDocument(); + }); + + it('applies correct trend colors for down trend', () => { + const { container } = render(StatCard, { + props: { + title: 'Allocations', + value: 186, + trend: 'down', + trendValue: '-12' + } + }); + + const trendElement = container.querySelector('.text-error'); + expect(trendElement).toBeInTheDocument(); + expect(screen.getByText('-12')).toBeInTheDocument(); + }); + + it('applies neutral trend color when trend is neutral', () => { + const { container } = render(StatCard, { + props: { + title: 'Team', + value: 8, + trend: 'neutral', + trendValue: '0' + } + }); + + const trendElement = container.querySelector('.text-base-content\\/50'); + expect(trendElement).toBeInTheDocument(); + }); + + it('renders icon when provided', () => { + const { container } = render(StatCard, { + props: { + title: 'Projects', + value: 14, + icon: Folder + } + }); + + // Icon should render an SVG + const svg = container.querySelector('svg'); + expect(svg).toBeInTheDocument(); + }); + + it('uses DaisyUI card classes', () => { + const { container } = render(StatCard, { + props: { + title: 'Test', + value: 100 + } + }); + + const card = container.querySelector('.card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveClass('bg-base-100'); + expect(card).toHaveClass('shadow-sm'); + }); +}); diff --git a/frontend/src/lib/components/common/StatCard.svelte b/frontend/src/lib/components/common/StatCard.svelte new file mode 100644 index 00000000..7cc17731 --- /dev/null +++ b/frontend/src/lib/components/common/StatCard.svelte @@ -0,0 +1,59 @@ + + +
+
+
+
{title}
+ {#if Icon} +
+ +
+ {/if} +
+
{value}
+
+ {#if trendValue} + + + + {trendValue} + {/if} + {#if description} + {description} + {/if} +
+
+
diff --git a/frontend/src/lib/components/layout/Breadcrumbs.svelte b/frontend/src/lib/components/layout/Breadcrumbs.svelte index 5fe5b0a4..8793c513 100644 --- a/frontend/src/lib/components/layout/Breadcrumbs.svelte +++ b/frontend/src/lib/components/layout/Breadcrumbs.svelte @@ -6,7 +6,7 @@ export function generateBreadcrumbs(pathname: string): Breadcrumb[] { const segments = pathname.split('/').filter(Boolean); - const crumbs: Breadcrumb[] = [{ label: 'Home', href: '/' }]; + const crumbs: Breadcrumb[] = [{ label: 'Home', href: '/dashboard' }]; let current = ''; for (const segment of segments) { @@ -33,7 +33,7 @@