From a3331881d4ce94d3e08d4c3c9acbdd94c856a36b Mon Sep 17 00:00:00 2001 From: Sarah Date: Fri, 27 Mar 2026 15:38:49 +0000 Subject: [PATCH] =?UTF-8?q?Add=20GrubFlip=20admin=20dashboard=20=E2=80=94?= =?UTF-8?q?=20meals,=20orders,=20trades=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Built out the full restaurant owner admin dashboard: - Dashboard overview with stats, recent trades, and top meals - Meals management with card grid, add/edit/delete modals, search & filter - Orders page with status tabs, accept/ready/complete workflow - Trades activity page with search and stats overview - Shared admin-app.js module (sidebar, API client, toast, auth helpers) - All pages use existing design tokens, mobile-first responsive layout - Demo data fallback when API endpoints aren't ready yet Co-Authored-By: Claude Opus 4.6 (1M context) --- admin/css/admin.css | 216 +++++++++++++++++ admin/dashboard.html | 315 ++++++++++++++++++++++++ admin/js/admin-app.js | 180 ++++++++++++++ admin/meals.html | 542 ++++++++++++++++++++++++++++++++++++++++++ admin/orders.html | 311 ++++++++++++++++++++++++ admin/trades.html | 233 ++++++++++++++++++ 6 files changed, 1797 insertions(+) create mode 100644 admin/dashboard.html create mode 100644 admin/js/admin-app.js create mode 100644 admin/meals.html create mode 100644 admin/orders.html create mode 100644 admin/trades.html diff --git a/admin/css/admin.css b/admin/css/admin.css index 7753537..8eb2827 100644 --- a/admin/css/admin.css +++ b/admin/css/admin.css @@ -972,3 +972,219 @@ img { max-width: 100%; height: auto; } max-width: 80rem; } } + +/* ========================================== + DASHBOARD-SPECIFIC STYLES + ========================================== */ + +.dashboard-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--gf-space-6); +} + +@media (min-width: 1024px) { + .dashboard-grid { + grid-template-columns: 1.2fr 0.8fr; + } +} + +/* Top Meals List */ +.top-meals-list { + display: flex; + flex-direction: column; +} + +.top-meal-item { + display: flex; + align-items: center; + gap: var(--gf-space-3); + padding: var(--gf-space-3) 0; + border-bottom: 1px solid var(--gf-neutral-100); +} + +.top-meal-item:last-child { border-bottom: none; } + +.top-meal-rank { + width: 1.75rem; + height: 1.75rem; + border-radius: var(--gf-radius-full); + background: var(--gf-neutral-100); + color: var(--gf-neutral-600); + font-size: var(--gf-text-sm); + font-weight: var(--gf-weight-semibold); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.top-meal-item:first-child .top-meal-rank { + background: var(--gf-primary-bg); + color: var(--gf-primary); +} + +.top-meal-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.top-meal-info strong { + font-size: var(--gf-text-sm); + font-weight: var(--gf-weight-semibold); + color: var(--gf-neutral-800); +} + +/* ========================================== + MEALS PAGE STYLES + ========================================== */ + +.meals-toolbar { + display: flex; + flex-direction: column; + gap: var(--gf-space-3); + margin-bottom: var(--gf-space-6); +} + +@media (min-width: 640px) { + .meals-toolbar { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +.meals-grid { + display: grid; + grid-template-columns: 1fr; + gap: var(--gf-space-4); +} + +@media (min-width: 640px) { + .meals-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (min-width: 1024px) { + .meals-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +.meal-card { + background: var(--gf-bg); + border-radius: var(--gf-radius-lg); + box-shadow: var(--gf-shadow-card); + border: 1px solid var(--gf-neutral-200); + overflow: hidden; + transition: box-shadow var(--gf-duration-normal) var(--gf-ease), transform var(--gf-duration-normal) var(--gf-ease); +} + +.meal-card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.12); + transform: translateY(-2px); +} + +.meal-card-img { + width: 100%; + height: 10rem; + object-fit: cover; + background: var(--gf-neutral-100); + display: block; +} + +.meal-card-img-placeholder { + width: 100%; + height: 10rem; + background: var(--gf-neutral-100); + display: flex; + align-items: center; + justify-content: center; + font-size: 2.5rem; + color: var(--gf-neutral-300); +} + +.meal-card-body { + padding: var(--gf-space-4); +} + +.meal-card-title { + font-family: var(--gf-font-heading); + font-size: var(--gf-text-base); + font-weight: var(--gf-weight-semibold); + color: var(--gf-neutral-900); + margin-bottom: var(--gf-space-1); +} + +.meal-card-meta { + display: flex; + align-items: center; + gap: var(--gf-space-3); + font-size: var(--gf-text-xs); + color: var(--gf-neutral-500); + margin-bottom: var(--gf-space-3); +} + +.meal-card-desc { + font-size: var(--gf-text-sm); + color: var(--gf-neutral-600); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: var(--gf-space-3); +} + +.meal-card-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: var(--gf-space-3); + border-top: 1px solid var(--gf-neutral-100); +} + +.meal-card-footer .actions { + display: flex; + gap: var(--gf-space-2); +} + +/* Modal form image preview */ +.img-preview-wrapper { + width: 100%; + height: 8rem; + border: 2px dashed var(--gf-neutral-300); + border-radius: var(--gf-radius-md); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + cursor: pointer; + transition: border-color var(--gf-duration-fast); + background: var(--gf-neutral-50); + position: relative; +} + +.img-preview-wrapper:hover { + border-color: var(--gf-primary); +} + +.img-preview-wrapper img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.img-preview-wrapper .upload-label { + text-align: center; + color: var(--gf-neutral-400); + font-size: var(--gf-text-sm); +} + +.img-preview-wrapper .upload-label span { + display: block; + font-size: 1.5rem; + margin-bottom: var(--gf-space-1); +} diff --git a/admin/dashboard.html b/admin/dashboard.html new file mode 100644 index 0000000..0f5f01d --- /dev/null +++ b/admin/dashboard.html @@ -0,0 +1,315 @@ + + + + + + Grubflip Admin — Dashboard + + + + + + + +
+ + + + + + + +
+ +
+
+ +

Dashboard

+
+
+ + +
+
+ + +
+ + + +
+
+
🍔
+
+

Active Meals

+
0
+
--
+
+
+
+
🔄
+
+

Trades Today

+
0
+
--
+
+
+
+
+
+

Avg Rating

+
--
+
--
+
+
+
+
👥
+
+

Profile Views

+
0
+
--
+
+
+
+ + +
+ +
+
+

Recent Trades

+ View all → +
+
+
+
+
+ + +
+
+

Top Meals

+ Manage → +
+
+
+
+
+
+
+
+
+ + +
+ + + + + diff --git a/admin/js/admin-app.js b/admin/js/admin-app.js new file mode 100644 index 0000000..6b7e081 --- /dev/null +++ b/admin/js/admin-app.js @@ -0,0 +1,180 @@ +/* ============================================ + Grubflip Admin — Core Application Module + Sidebar, API client, toast, auth helpers + ============================================ */ + +const AdminApp = (() => { + 'use strict'; + + const API_BASE = '/api'; + + // --- Auth --- + function getToken() { + return localStorage.getItem('gf_token'); + } + + function getUser() { + try { + return JSON.parse(localStorage.getItem('gf_user') || '{}'); + } catch { + return {}; + } + } + + function requireAuth() { + if (!getToken()) { + window.location.href = 'index.html'; + return false; + } + return true; + } + + function logout() { + localStorage.removeItem('gf_token'); + localStorage.removeItem('gf_user'); + window.location.href = 'index.html'; + } + + // --- API Client --- + async function api(endpoint, options = {}) { + const url = `${API_BASE}${endpoint}`; + const headers = { + 'Accept': 'application/json', + ...options.headers, + }; + + const token = getToken(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (options.body && !(options.body instanceof FormData)) { + headers['Content-Type'] = 'application/json'; + if (typeof options.body === 'object') { + options.body = JSON.stringify(options.body); + } + } + + const resp = await fetch(url, { ...options, headers }); + + if (resp.status === 401) { + logout(); + return null; + } + + if (!resp.ok) { + const data = await resp.json().catch(() => ({})); + throw new Error(data.message || `Request failed (${resp.status})`); + } + + if (resp.status === 204) return null; + return resp.json(); + } + + // --- Sidebar --- + function initSidebar() { + const sidebar = document.getElementById('sidebar'); + const overlay = document.getElementById('sidebarOverlay'); + const toggle = document.getElementById('menuToggle'); + + if (!sidebar || !toggle) return; + + toggle.addEventListener('click', () => { + sidebar.classList.toggle('open'); + if (overlay) overlay.classList.toggle('visible'); + }); + + if (overlay) { + overlay.addEventListener('click', () => { + sidebar.classList.remove('open'); + overlay.classList.remove('visible'); + }); + } + + // Close on Escape + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && sidebar.classList.contains('open')) { + sidebar.classList.remove('open'); + if (overlay) overlay.classList.remove('visible'); + } + }); + } + + // --- Toast System --- + function toast(message, type = 'info', duration = 4000) { + const container = document.getElementById('toastContainer'); + if (!container) return; + + const icons = { + success: '✅', + error: '❌', + warning: '⚠️', + info: 'ℹ️' + }; + + const el = document.createElement('div'); + el.className = `toast toast-${type}`; + el.innerHTML = `${icons[type] || ''} ${escapeHtml(message)}`; + container.appendChild(el); + + setTimeout(() => { + el.classList.add('removing'); + el.addEventListener('animationend', () => el.remove()); + }, duration); + } + + // --- Utilities --- + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + function formatDate(dateStr) { + if (!dateStr) return '--'; + const d = new Date(dateStr); + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + } + + function formatTime(dateStr) { + if (!dateStr) return '--'; + const d = new Date(dateStr); + return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); + } + + function timeAgo(dateStr) { + if (!dateStr) return '--'; + const now = Date.now(); + const then = new Date(dateStr).getTime(); + const diff = Math.floor((now - then) / 1000); + + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)} min ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`; + if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; + return formatDate(dateStr); + } + + // --- Confirm Dialog --- + function confirm(message) { + return new Promise((resolve) => { + // Use native confirm for now — can replace with modal later + resolve(window.confirm(message)); + }); + } + + return { + api, + getToken, + getUser, + requireAuth, + logout, + initSidebar, + toast, + escapeHtml, + formatDate, + formatTime, + timeAgo, + confirm, + }; +})(); diff --git a/admin/meals.html b/admin/meals.html new file mode 100644 index 0000000..6269cf5 --- /dev/null +++ b/admin/meals.html @@ -0,0 +1,542 @@ + + + + + + Grubflip Admin — Meals + + + + + + + +
+ + + + + + + +
+ +
+
+ +

Meals

+
+
+ +
+
+ + +
+ + + +
+ +
+ + +
+
+ + +
+
+
+
+
+
+ + + + + + + + +
+ + + + + diff --git a/admin/orders.html b/admin/orders.html new file mode 100644 index 0000000..6048fe7 --- /dev/null +++ b/admin/orders.html @@ -0,0 +1,311 @@ + + + + + + Grubflip Admin — Orders + + + + + + + +
+ + + + +
+
+
+ +

Orders

+
+
+ +
+
+ +
+ + + +
+ + + + + +
+ + +
+
+
+
+
+
+
+
+ + + + +
+ + + + + diff --git a/admin/trades.html b/admin/trades.html new file mode 100644 index 0000000..1526bbd --- /dev/null +++ b/admin/trades.html @@ -0,0 +1,233 @@ + + + + + + Grubflip Admin — Trades + + + + + + + +
+ + + + +
+
+
+ +

Trades

+
+
+ +
+
+ +
+ + + +
+
+
🔄
+
+

Total Trades

+
159
+
+
+
+
🔥
+
+

This Week

+
47
+
+23% vs last week
+
+
+
+
+
+

Most Traded

+
Spicy Ramen
+
+
+
+
📈
+
+

Trade Rate

+
87%
+
Acceptance rate
+
+
+
+ + +
+
+

Recent Trades

+ +
+
+
+
+
+
+
+
+ +
+ + + + +