payfrit-portal/dashboard.html
Zara f8ea11601e Add Payfrit User Portal — scanner, dashboard, compare pages
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>
2026-03-26 05:54:19 +00:00

1046 lines
37 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 (&lt;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>