Consumer-facing portal with mobile-first design: - scan.html: Barcode scanner with product search, health score ring animation, and healthier alternatives display - dashboard.html: User dashboard with recent scans, health trends chart, favorites grid, Connected Apps section, and settings - compare.html: Side-by-side product comparison with winner banner, nutrition breakdown, and ingredient highlights - products.json: Sample product data with scores, descriptions, and alternatives Connected Apps section lets users manage third-party integrations (Apple Health, MyFitnessPal, Google Fit, etc.) with permission controls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1046 lines
37 KiB
HTML
1046 lines
37 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>My Dashboard — Payfrit</title>
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
:root {
|
||
--primary: #2563eb;
|
||
--primary-light: #dbeafe;
|
||
--green: #16a34a;
|
||
--yellow: #ca8a04;
|
||
--red: #dc2626;
|
||
--gray-50: #f9fafb;
|
||
--gray-100: #f3f4f6;
|
||
--gray-200: #e5e7eb;
|
||
--gray-300: #d1d5db;
|
||
--gray-500: #6b7280;
|
||
--gray-700: #374151;
|
||
--gray-900: #111827;
|
||
--radius: 12px;
|
||
--shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
|
||
--shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: var(--gray-50);
|
||
color: var(--gray-900);
|
||
line-height: 1.5;
|
||
min-height: 100vh;
|
||
padding-bottom: 80px;
|
||
}
|
||
|
||
/* Header */
|
||
.header {
|
||
background: #fff;
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid var(--gray-200);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
.header-inner {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.header h1 {
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
}
|
||
.header-avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: var(--primary-light);
|
||
color: var(--primary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* Tabs */
|
||
.tabs {
|
||
background: #fff;
|
||
border-bottom: 1px solid var(--gray-200);
|
||
position: sticky;
|
||
top: 61px;
|
||
z-index: 99;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
scrollbar-width: none;
|
||
}
|
||
.tabs::-webkit-scrollbar { display: none; }
|
||
.tabs-inner {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
padding: 0 12px;
|
||
gap: 4px;
|
||
}
|
||
.tab-btn {
|
||
flex-shrink: 0;
|
||
padding: 12px 16px;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--gray-500);
|
||
background: none;
|
||
border: none;
|
||
border-bottom: 3px solid transparent;
|
||
cursor: pointer;
|
||
transition: color 0.2s, border-color 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
.tab-btn.active {
|
||
color: var(--primary);
|
||
border-bottom-color: var(--primary);
|
||
}
|
||
.tab-btn:hover { color: var(--gray-700); }
|
||
|
||
/* Main content */
|
||
.main {
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
padding: 16px;
|
||
}
|
||
.tab-panel { display: none; }
|
||
.tab-panel.active { display: block; }
|
||
|
||
/* Cards */
|
||
.card {
|
||
background: #fff;
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.card-title {
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
color: var(--gray-500);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* Score badge */
|
||
.score-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
font-weight: 700;
|
||
font-size: 0.875rem;
|
||
color: #fff;
|
||
flex-shrink: 0;
|
||
}
|
||
.score-good { background: var(--green); }
|
||
.score-mid { background: var(--yellow); }
|
||
.score-bad { background: var(--red); }
|
||
|
||
/* Scan list */
|
||
.scan-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
}
|
||
.scan-item:last-child { border-bottom: none; }
|
||
.scan-item:active { background: var(--gray-50); }
|
||
.scan-info { flex: 1; min-width: 0; }
|
||
.scan-name {
|
||
font-weight: 600;
|
||
font-size: 0.95rem;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.scan-meta {
|
||
font-size: 0.8rem;
|
||
color: var(--gray-500);
|
||
margin-top: 2px;
|
||
}
|
||
.scan-arrow {
|
||
color: var(--gray-300);
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
/* Health trend chart placeholder */
|
||
.chart-placeholder {
|
||
background: var(--gray-50);
|
||
border: 2px dashed var(--gray-200);
|
||
border-radius: 8px;
|
||
height: 200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--gray-500);
|
||
font-size: 0.875rem;
|
||
}
|
||
.chart-placeholder svg {
|
||
width: 40px;
|
||
height: 40px;
|
||
margin-bottom: 8px;
|
||
stroke: var(--gray-300);
|
||
}
|
||
/* Simple bar chart */
|
||
.mini-chart {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 6px;
|
||
height: 120px;
|
||
padding: 12px 0;
|
||
}
|
||
.mini-chart-bar-wrapper {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
height: 100%;
|
||
justify-content: flex-end;
|
||
}
|
||
.mini-chart-bar {
|
||
width: 100%;
|
||
max-width: 36px;
|
||
border-radius: 4px 4px 0 0;
|
||
transition: height 0.5s ease;
|
||
}
|
||
.mini-chart-label {
|
||
font-size: 0.65rem;
|
||
color: var(--gray-500);
|
||
margin-top: 4px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Stats row */
|
||
.stats-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
.stat-card {
|
||
background: #fff;
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
padding: 14px;
|
||
text-align: center;
|
||
}
|
||
.stat-value {
|
||
font-size: 1.5rem;
|
||
font-weight: 800;
|
||
color: var(--primary);
|
||
}
|
||
.stat-label {
|
||
font-size: 0.75rem;
|
||
color: var(--gray-500);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* Favorites grid */
|
||
.fav-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
}
|
||
.fav-item {
|
||
background: var(--gray-50);
|
||
border-radius: 8px;
|
||
padding: 14px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: box-shadow 0.2s, transform 0.15s;
|
||
border: 1px solid var(--gray-100);
|
||
}
|
||
.fav-item:active {
|
||
transform: scale(0.97);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
.fav-emoji {
|
||
font-size: 2rem;
|
||
margin-bottom: 6px;
|
||
}
|
||
.fav-name {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.fav-score {
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
margin-top: 4px;
|
||
}
|
||
.fav-remove {
|
||
font-size: 0.7rem;
|
||
color: var(--gray-500);
|
||
margin-top: 6px;
|
||
cursor: pointer;
|
||
background: none;
|
||
border: none;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* Settings */
|
||
.setting-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
.setting-group-title {
|
||
font-size: 0.9rem;
|
||
font-weight: 700;
|
||
margin-bottom: 10px;
|
||
}
|
||
.setting-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
}
|
||
.setting-row:last-child { border-bottom: none; }
|
||
.setting-label {
|
||
font-size: 0.9rem;
|
||
}
|
||
.setting-desc {
|
||
font-size: 0.75rem;
|
||
color: var(--gray-500);
|
||
margin-top: 2px;
|
||
}
|
||
/* Toggle switch */
|
||
.toggle {
|
||
position: relative;
|
||
width: 48px;
|
||
height: 28px;
|
||
flex-shrink: 0;
|
||
}
|
||
.toggle input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
.toggle-slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
inset: 0;
|
||
background: var(--gray-300);
|
||
border-radius: 28px;
|
||
transition: background 0.2s;
|
||
}
|
||
.toggle-slider::before {
|
||
content: '';
|
||
position: absolute;
|
||
height: 22px;
|
||
width: 22px;
|
||
left: 3px;
|
||
bottom: 3px;
|
||
background: #fff;
|
||
border-radius: 50%;
|
||
transition: transform 0.2s;
|
||
}
|
||
.toggle input:checked + .toggle-slider {
|
||
background: var(--primary);
|
||
}
|
||
.toggle input:checked + .toggle-slider::before {
|
||
transform: translateX(20px);
|
||
}
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 10px 20px;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
border-radius: 8px;
|
||
border: none;
|
||
cursor: pointer;
|
||
transition: background 0.2s, transform 0.1s;
|
||
}
|
||
.btn:active { transform: scale(0.97); }
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
color: #fff;
|
||
}
|
||
.btn-danger {
|
||
background: #fef2f2;
|
||
color: var(--red);
|
||
border: 1px solid #fecaca;
|
||
}
|
||
.btn-block {
|
||
width: 100%;
|
||
}
|
||
|
||
/* Connected Apps */
|
||
.app-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 14px 0;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
}
|
||
.app-item:last-child { border-bottom: none; }
|
||
.app-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.3rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.app-details { flex: 1; min-width: 0; }
|
||
.app-name { font-weight: 600; font-size: 0.95rem; }
|
||
.app-desc { font-size: 0.8rem; color: var(--gray-500); margin-top: 2px; }
|
||
.app-perms {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
margin-top: 6px;
|
||
}
|
||
.app-perm-tag {
|
||
font-size: 0.65rem;
|
||
font-weight: 600;
|
||
background: var(--gray-100);
|
||
color: var(--gray-500);
|
||
border-radius: 20px;
|
||
padding: 2px 8px;
|
||
}
|
||
.app-perm-tag.read { background: #dbeafe; color: #2563eb; }
|
||
.app-perm-tag.write { background: #fef3c7; color: #ca8a04; }
|
||
.app-action-btn {
|
||
flex-shrink: 0;
|
||
padding: 6px 14px;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
border-radius: 8px;
|
||
border: none;
|
||
cursor: pointer;
|
||
transition: background 0.2s, transform 0.1s;
|
||
}
|
||
.app-action-btn:active { transform: scale(0.95); }
|
||
.app-action-btn.disconnect {
|
||
background: #fef2f2;
|
||
color: var(--red);
|
||
border: 1px solid #fecaca;
|
||
}
|
||
.app-action-btn.connect {
|
||
background: var(--primary);
|
||
color: #fff;
|
||
}
|
||
.app-status {
|
||
font-size: 0.7rem;
|
||
font-weight: 600;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
.app-status.active { color: var(--green); }
|
||
.app-status.active::before {
|
||
content: '';
|
||
width: 6px;
|
||
height: 6px;
|
||
background: var(--green);
|
||
border-radius: 50%;
|
||
}
|
||
|
||
/* Empty state */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: var(--gray-500);
|
||
}
|
||
.empty-state svg {
|
||
width: 48px;
|
||
height: 48px;
|
||
stroke: var(--gray-300);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* Bottom nav */
|
||
.bottom-nav {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: #fff;
|
||
border-top: 1px solid var(--gray-200);
|
||
display: flex;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||
}
|
||
.bottom-nav-inner {
|
||
max-width: 600px;
|
||
width: 100%;
|
||
display: flex;
|
||
}
|
||
.nav-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 8px 4px 10px;
|
||
font-size: 0.65rem;
|
||
font-weight: 600;
|
||
color: var(--gray-500);
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
}
|
||
.nav-item.active { color: var(--primary); }
|
||
.nav-item svg {
|
||
width: 22px;
|
||
height: 22px;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (min-width: 640px) {
|
||
.main { padding: 24px; }
|
||
.stats-row { gap: 16px; }
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(8px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
.tab-panel.active {
|
||
animation: fadeIn 0.25s ease;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<div class="header-inner">
|
||
<h1>Payfrit</h1>
|
||
<div class="header-avatar" id="userAvatar">JD</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Tabs -->
|
||
<nav class="tabs" role="tablist">
|
||
<div class="tabs-inner">
|
||
<button class="tab-btn active" role="tab" aria-selected="true" data-tab="scans">Recent Scans</button>
|
||
<button class="tab-btn" role="tab" aria-selected="false" data-tab="trends">Health Trends</button>
|
||
<button class="tab-btn" role="tab" aria-selected="false" data-tab="favorites">Favorites</button>
|
||
<button class="tab-btn" role="tab" aria-selected="false" data-tab="apps">Connected Apps</button>
|
||
<button class="tab-btn" role="tab" aria-selected="false" data-tab="settings">Settings</button>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Main -->
|
||
<main class="main">
|
||
|
||
<!-- TAB: Recent Scans -->
|
||
<div class="tab-panel active" id="panel-scans" role="tabpanel">
|
||
<div class="stats-row">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="statScans">24</div>
|
||
<div class="stat-label">Scans</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="statAvg">72</div>
|
||
<div class="stat-label">Avg Score</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="statSwaps">8</div>
|
||
<div class="stat-label">Swaps Made</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-title">Recent Scans</div>
|
||
<div id="scanList">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Health Trends -->
|
||
<div class="tab-panel" id="panel-trends" role="tabpanel">
|
||
<div class="card">
|
||
<div class="card-title">Weekly Health Score Average</div>
|
||
<div class="mini-chart" id="weeklyChart">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-title">Monthly Trend</div>
|
||
<div class="chart-placeholder">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M3 12l4-4 4 6 4-8 6 10" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
<span>Monthly chart coming soon</span>
|
||
<span style="font-size:0.75rem;color:var(--gray-400);margin-top:4px;">Scan more products to unlock</span>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-title">Score Breakdown</div>
|
||
<div id="scoreBreakdown">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Favorites -->
|
||
<div class="tab-panel" id="panel-favorites" role="tabpanel">
|
||
<div class="card">
|
||
<div class="card-title">Your Favorite Products</div>
|
||
<div class="fav-grid" id="favGrid">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Connected Apps -->
|
||
<div class="tab-panel" id="panel-apps" role="tabpanel">
|
||
<div class="card">
|
||
<div class="card-title">Your Connected Apps</div>
|
||
<p style="font-size:0.85rem;color:var(--gray-500);margin-bottom:16px">
|
||
Apps that can access your Payfrit data. You control what each app can see.
|
||
</p>
|
||
<div id="connectedAppsList"></div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-title">Available Integrations</div>
|
||
<div id="availableAppsList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TAB: Settings -->
|
||
<div class="tab-panel" id="panel-settings" role="tabpanel">
|
||
<div class="card">
|
||
<div class="setting-group">
|
||
<div class="setting-group-title">Notifications</div>
|
||
<div class="setting-row">
|
||
<div>
|
||
<div class="setting-label">Scan Alerts</div>
|
||
<div class="setting-desc">Notify when a scanned product has recalls</div>
|
||
</div>
|
||
<label class="toggle">
|
||
<input type="checkbox" checked id="toggleAlerts">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="setting-row">
|
||
<div>
|
||
<div class="setting-label">Weekly Summary</div>
|
||
<div class="setting-desc">Get a weekly health score report</div>
|
||
</div>
|
||
<label class="toggle">
|
||
<input type="checkbox" checked id="toggleWeekly">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="setting-row">
|
||
<div>
|
||
<div class="setting-label">Healthier Alternatives</div>
|
||
<div class="setting-desc">Suggest swaps when scanning low-score items</div>
|
||
</div>
|
||
<label class="toggle">
|
||
<input type="checkbox" checked id="toggleAlternatives">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="setting-group">
|
||
<div class="setting-group-title">Account</div>
|
||
<div class="setting-row">
|
||
<div>
|
||
<div class="setting-label">Display Name</div>
|
||
<div class="setting-desc" id="displayName">Jane Doe</div>
|
||
</div>
|
||
<button class="btn btn-primary" style="padding:6px 14px;font-size:0.8rem" onclick="editName()">Edit</button>
|
||
</div>
|
||
<div class="setting-row">
|
||
<div>
|
||
<div class="setting-label">Email</div>
|
||
<div class="setting-desc" id="displayEmail">jane@example.com</div>
|
||
</div>
|
||
<button class="btn btn-primary" style="padding:6px 14px;font-size:0.8rem" onclick="editEmail()">Edit</button>
|
||
</div>
|
||
<div class="setting-row">
|
||
<div>
|
||
<div class="setting-label">Diet Preferences</div>
|
||
<div class="setting-desc" id="displayDiet">None set</div>
|
||
</div>
|
||
<button class="btn btn-primary" style="padding:6px 14px;font-size:0.8rem" onclick="editDiet()">Edit</button>
|
||
</div>
|
||
</div>
|
||
<div class="setting-group">
|
||
<div class="setting-group-title">Data</div>
|
||
<button class="btn btn-danger btn-block" onclick="clearHistory()">Clear Scan History</button>
|
||
<div style="height:10px"></div>
|
||
<button class="btn btn-danger btn-block" onclick="deleteAccount()">Delete Account</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Bottom Navigation -->
|
||
<nav class="bottom-nav">
|
||
<div class="bottom-nav-inner">
|
||
<a class="nav-item active" data-tab="scans">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h4l3-9 4 18 3-9h4"/></svg>
|
||
Dashboard
|
||
</a>
|
||
<a class="nav-item" onclick="window.location.href='/portal/scan.html'">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
||
Scan
|
||
</a>
|
||
<a class="nav-item" data-tab="favorites">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
|
||
Favorites
|
||
</a>
|
||
<a class="nav-item" data-tab="settings">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||
Settings
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<script>
|
||
// ===== Sample Data =====
|
||
const recentScans = [
|
||
{ name: 'Organic Valley Whole Milk', brand: 'Organic Valley', score: 82, time: '2 min ago', barcode: '093966000016' },
|
||
{ name: 'Cheerios Original', brand: 'General Mills', score: 65, time: '1 hr ago', barcode: '016000275263' },
|
||
{ name: 'Coca-Cola Classic', brand: 'Coca-Cola', score: 22, time: '3 hrs ago', barcode: '049000006346' },
|
||
{ name: 'Kind Dark Chocolate Nuts', brand: 'Kind', score: 74, time: 'Yesterday', barcode: '602652171215' },
|
||
{ name: 'Lay\'s Classic Chips', brand: 'Frito-Lay', score: 31, time: 'Yesterday', barcode: '028400064521' },
|
||
{ name: 'Chobani Greek Yogurt', brand: 'Chobani', score: 88, time: '2 days ago', barcode: '818290010001' },
|
||
{ name: 'Dave\'s Killer Bread', brand: 'Dave\'s', score: 79, time: '2 days ago', barcode: '013764025110' },
|
||
{ name: 'Doritos Nacho Cheese', brand: 'Frito-Lay', score: 28, time: '3 days ago', barcode: '028400443685' },
|
||
];
|
||
|
||
const weeklyScores = [
|
||
{ day: 'Mon', score: 68 },
|
||
{ day: 'Tue', score: 72 },
|
||
{ day: 'Wed', score: 55 },
|
||
{ day: 'Thu', score: 80 },
|
||
{ day: 'Fri', score: 74 },
|
||
{ day: 'Sat', score: 62 },
|
||
{ day: 'Sun', score: 78 },
|
||
];
|
||
|
||
const favorites = [
|
||
{ name: 'Chobani Yogurt', emoji: '🥛', score: 88 },
|
||
{ name: 'Dave\'s Bread', emoji: '🍞', score: 79 },
|
||
{ name: 'Organic Milk', emoji: '🥛', score: 82 },
|
||
{ name: 'Kind Bars', emoji: '🍫', score: 74 },
|
||
{ name: 'Banana', emoji: '🍌', score: 95 },
|
||
{ name: 'Baby Spinach', emoji: '🥬', score: 97 },
|
||
];
|
||
|
||
// ===== Score Helpers =====
|
||
function scoreClass(score) {
|
||
if (score >= 70) return 'score-good';
|
||
if (score >= 45) return 'score-mid';
|
||
return 'score-bad';
|
||
}
|
||
function scoreColor(score) {
|
||
if (score >= 70) return 'var(--green)';
|
||
if (score >= 45) return 'var(--yellow)';
|
||
return 'var(--red)';
|
||
}
|
||
|
||
// ===== Render Recent Scans =====
|
||
function renderScans() {
|
||
const container = document.getElementById('scanList');
|
||
if (recentScans.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="empty-state">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
|
||
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
|
||
</svg>
|
||
<p>No scans yet. Scan a barcode to get started!</p>
|
||
</div>`;
|
||
return;
|
||
}
|
||
container.innerHTML = recentScans.map(s => `
|
||
<div class="scan-item" onclick="viewProduct('${s.barcode}')">
|
||
<div class="score-badge ${scoreClass(s.score)}">${s.score}</div>
|
||
<div class="scan-info">
|
||
<div class="scan-name">${s.name}</div>
|
||
<div class="scan-meta">${s.brand} · ${s.time}</div>
|
||
</div>
|
||
<span class="scan-arrow">›</span>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// ===== Render Weekly Chart =====
|
||
function renderWeeklyChart() {
|
||
const container = document.getElementById('weeklyChart');
|
||
const maxScore = 100;
|
||
container.innerHTML = weeklyScores.map(d => `
|
||
<div class="mini-chart-bar-wrapper">
|
||
<div class="mini-chart-bar" style="height:${(d.score / maxScore) * 100}%;background:${scoreColor(d.score)}"></div>
|
||
<div class="mini-chart-label">${d.day}<br><strong>${d.score}</strong></div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// ===== Render Score Breakdown =====
|
||
function renderScoreBreakdown() {
|
||
const container = document.getElementById('scoreBreakdown');
|
||
const good = recentScans.filter(s => s.score >= 70).length;
|
||
const mid = recentScans.filter(s => s.score >= 45 && s.score < 70).length;
|
||
const bad = recentScans.filter(s => s.score < 45).length;
|
||
const total = recentScans.length || 1;
|
||
container.innerHTML = `
|
||
<div style="margin-bottom:10px">
|
||
<div style="display:flex;justify-content:space-between;font-size:0.85rem;margin-bottom:4px">
|
||
<span style="color:var(--green);font-weight:600">Good (70+)</span><span>${good} products</span>
|
||
</div>
|
||
<div style="background:var(--gray-100);border-radius:4px;height:8px;overflow:hidden">
|
||
<div style="background:var(--green);height:100%;width:${(good/total)*100}%;border-radius:4px;transition:width 0.5s"></div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-bottom:10px">
|
||
<div style="display:flex;justify-content:space-between;font-size:0.85rem;margin-bottom:4px">
|
||
<span style="color:var(--yellow);font-weight:600">Okay (45-69)</span><span>${mid} products</span>
|
||
</div>
|
||
<div style="background:var(--gray-100);border-radius:4px;height:8px;overflow:hidden">
|
||
<div style="background:var(--yellow);height:100%;width:${(mid/total)*100}%;border-radius:4px;transition:width 0.5s"></div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="display:flex;justify-content:space-between;font-size:0.85rem;margin-bottom:4px">
|
||
<span style="color:var(--red);font-weight:600">Poor (<45)</span><span>${bad} products</span>
|
||
</div>
|
||
<div style="background:var(--gray-100);border-radius:4px;height:8px;overflow:hidden">
|
||
<div style="background:var(--red);height:100%;width:${(bad/total)*100}%;border-radius:4px;transition:width 0.5s"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ===== Render Favorites =====
|
||
function renderFavorites() {
|
||
const container = document.getElementById('favGrid');
|
||
if (favorites.length === 0) {
|
||
container.innerHTML = `<div class="empty-state" style="grid-column:1/-1">
|
||
<p>No favorites yet. Tap the heart on any product to save it here.</p>
|
||
</div>`;
|
||
return;
|
||
}
|
||
container.innerHTML = favorites.map((f, i) => `
|
||
<div class="fav-item" onclick="viewProduct('')">
|
||
<div class="fav-emoji">${f.emoji}</div>
|
||
<div class="fav-name">${f.name}</div>
|
||
<div class="fav-score" style="color:${scoreColor(f.score)}">Score: ${f.score}</div>
|
||
<button class="fav-remove" onclick="event.stopPropagation();removeFav(${i})">Remove</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// ===== Tab Switching =====
|
||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||
const navItems = document.querySelectorAll('.nav-item[data-tab]');
|
||
const panels = document.querySelectorAll('.tab-panel');
|
||
|
||
function switchTab(tabId) {
|
||
tabBtns.forEach(b => {
|
||
b.classList.toggle('active', b.dataset.tab === tabId);
|
||
b.setAttribute('aria-selected', b.dataset.tab === tabId);
|
||
});
|
||
navItems.forEach(n => n.classList.toggle('active', n.dataset.tab === tabId));
|
||
panels.forEach(p => p.classList.toggle('active', p.id === `panel-${tabId}`));
|
||
}
|
||
|
||
tabBtns.forEach(btn => btn.addEventListener('click', () => switchTab(btn.dataset.tab)));
|
||
navItems.forEach(nav => nav.addEventListener('click', (e) => {
|
||
if (nav.dataset.tab) {
|
||
e.preventDefault();
|
||
switchTab(nav.dataset.tab);
|
||
}
|
||
}));
|
||
|
||
// ===== Actions =====
|
||
function viewProduct(barcode) {
|
||
// Navigate to product detail — would integrate with Payfrit API
|
||
if (barcode) {
|
||
// window.location.href = `/portal/product.html?barcode=${barcode}`;
|
||
console.log('View product:', barcode);
|
||
}
|
||
}
|
||
|
||
function removeFav(index) {
|
||
if (confirm('Remove from favorites?')) {
|
||
favorites.splice(index, 1);
|
||
renderFavorites();
|
||
}
|
||
}
|
||
|
||
function editName() {
|
||
const name = prompt('Enter your display name:', document.getElementById('displayName').textContent);
|
||
if (name && name.trim()) {
|
||
document.getElementById('displayName').textContent = name.trim();
|
||
document.getElementById('userAvatar').textContent = name.trim().split(' ').map(w => w[0]).join('').substring(0, 2).toUpperCase();
|
||
}
|
||
}
|
||
|
||
function editEmail() {
|
||
const email = prompt('Enter your email:', document.getElementById('displayEmail').textContent);
|
||
if (email && email.trim()) {
|
||
document.getElementById('displayEmail').textContent = email.trim();
|
||
}
|
||
}
|
||
|
||
function editDiet() {
|
||
const diet = prompt('Enter diet preferences (e.g., Vegan, Gluten-free, Keto):', document.getElementById('displayDiet').textContent);
|
||
if (diet && diet.trim()) {
|
||
document.getElementById('displayDiet').textContent = diet.trim();
|
||
}
|
||
}
|
||
|
||
function clearHistory() {
|
||
if (confirm('Clear all scan history? This cannot be undone.')) {
|
||
recentScans.length = 0;
|
||
renderScans();
|
||
document.getElementById('statScans').textContent = '0';
|
||
}
|
||
}
|
||
|
||
function deleteAccount() {
|
||
if (confirm('Are you sure you want to delete your account? This is permanent.')) {
|
||
alert('Account deletion requested. You will receive a confirmation email.');
|
||
}
|
||
}
|
||
|
||
// ===== Connected Apps Data =====
|
||
const connectedApps = [
|
||
{
|
||
name: 'Apple Health',
|
||
icon: '❤️',
|
||
iconBg: '#fef2f2',
|
||
desc: 'Syncs nutrition data to Apple Health',
|
||
perms: [{ label: 'Read Scans', type: 'read' }, { label: 'Write Nutrition', type: 'write' }],
|
||
connected: true,
|
||
lastSync: '2 hrs ago'
|
||
},
|
||
{
|
||
name: 'MyFitnessPal',
|
||
icon: '🔥',
|
||
iconBg: '#dbeafe',
|
||
desc: 'Log scanned food to your calorie tracker',
|
||
perms: [{ label: 'Read Scans', type: 'read' }, { label: 'Read Favorites', type: 'read' }],
|
||
connected: true,
|
||
lastSync: '1 day ago'
|
||
}
|
||
];
|
||
|
||
const availableApps = [
|
||
{
|
||
name: 'Google Fit',
|
||
icon: '💚',
|
||
iconBg: '#dcfce7',
|
||
desc: 'Sync nutrition and health score data',
|
||
perms: [{ label: 'Read Scans', type: 'read' }, { label: 'Write Nutrition', type: 'write' }]
|
||
},
|
||
{
|
||
name: 'Samsung Health',
|
||
icon: '💙',
|
||
iconBg: '#dbeafe',
|
||
desc: 'Track food health alongside activity',
|
||
perms: [{ label: 'Read Scans', type: 'read' }]
|
||
},
|
||
{
|
||
name: 'Instacart',
|
||
icon: '🛒',
|
||
iconBg: '#fef3c7',
|
||
desc: 'Auto-add healthier alternatives to your cart',
|
||
perms: [{ label: 'Read Alternatives', type: 'read' }, { label: 'Write Cart', type: 'write' }]
|
||
}
|
||
];
|
||
|
||
// ===== Render Connected Apps =====
|
||
function renderConnectedApps() {
|
||
const container = document.getElementById('connectedAppsList');
|
||
if (connectedApps.length === 0) {
|
||
container.innerHTML = '<div class="empty-state"><p>No apps connected yet.</p></div>';
|
||
return;
|
||
}
|
||
container.innerHTML = connectedApps.map((app, i) => `
|
||
<div class="app-item">
|
||
<div class="app-icon" style="background:${app.iconBg}">${app.icon}</div>
|
||
<div class="app-details">
|
||
<div class="app-name">${app.name}</div>
|
||
<div class="app-desc">${app.desc}</div>
|
||
<div class="app-perms">
|
||
${app.perms.map(p => `<span class="app-perm-tag ${p.type}">${p.label}</span>`).join('')}
|
||
</div>
|
||
<div class="app-status active" style="margin-top:6px">Connected · Synced ${app.lastSync}</div>
|
||
</div>
|
||
<button class="app-action-btn disconnect" onclick="disconnectApp(${i})">Disconnect</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderAvailableApps() {
|
||
const container = document.getElementById('availableAppsList');
|
||
container.innerHTML = availableApps.map((app, i) => `
|
||
<div class="app-item">
|
||
<div class="app-icon" style="background:${app.iconBg}">${app.icon}</div>
|
||
<div class="app-details">
|
||
<div class="app-name">${app.name}</div>
|
||
<div class="app-desc">${app.desc}</div>
|
||
<div class="app-perms">
|
||
${app.perms.map(p => `<span class="app-perm-tag ${p.type}">${p.label}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
<button class="app-action-btn connect" onclick="connectApp(${i})">Connect</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function disconnectApp(index) {
|
||
if (confirm(`Disconnect ${connectedApps[index].name}? It will lose access to your Payfrit data.`)) {
|
||
const app = connectedApps.splice(index, 1)[0];
|
||
delete app.connected;
|
||
delete app.lastSync;
|
||
availableApps.push(app);
|
||
renderConnectedApps();
|
||
renderAvailableApps();
|
||
}
|
||
}
|
||
|
||
function connectApp(index) {
|
||
const app = availableApps.splice(index, 1)[0];
|
||
app.connected = true;
|
||
app.lastSync = 'just now';
|
||
connectedApps.push(app);
|
||
renderConnectedApps();
|
||
renderAvailableApps();
|
||
}
|
||
|
||
// ===== Init =====
|
||
renderScans();
|
||
renderWeeklyChart();
|
||
renderScoreBreakdown();
|
||
renderFavorites();
|
||
renderConnectedApps();
|
||
renderAvailableApps();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|