RetailPro Full Source Code
RetailPro Full Source Code
RetailPro
Online Retail Inventory & Sales Management System
Complete Source Code — All Files
Balasubramaniyam S (U22IT006)
Mohammed Sitthik A (U22IT026)
Tharuneeshwaran A S (U22IT049)
Guided by: Dr. R Yogesh Rajkumar M.E, Ph.D — Dept. of IT, BIHER
Contents
1. backend/.env — Backend environment configuration
2. backend/[Link] — Express server entry point
3. backend/models/[Link] — MongoDB user schema
4. backend/routes/[Link] — Register and login API routes
5. css/[Link] — CSS design tokens
6. css/[Link] — Login/register page styles
7. css/[Link] — Shopkeeper dashboard styles
8. css/[Link] — Customer portal styles
9. [Link] — Login/register page HTML
10. pages/[Link] — Shopkeeper dashboard HTML
11. pages/[Link] — Customer portal HTML
12. js/[Link] — Login/register JavaScript
13. js/[Link] — Shopkeeper dashboard JavaScript
14. js/[Link] — Customer portal JavaScript
1. backend / .env
📄 File: backend/.env
Lines of code: 3
PORT=5000
MONGO_URI=mongodb://localhost:27017/retailpro
JWT_SECRET=retailpro_super_secret_key_change_this_in_production_2026
2. backend / [Link]
📄 File: backend/[Link]
Lines of code: 31
// Middleware
[Link](cors());
[Link]([Link]());
// Test route
[Link]("/", (req, res) => {
[Link]("Server running 🚀");
});
// DB connect
[Link]([Link].MONGO_URI)
.then(() => [Link]("MongoDB Connected ✅"))
.catch(err => [Link]("DB Error ❌", err));
// Routes
[Link]("/api/auth", require("./routes/auth"));
// Port
const PORT = [Link] || 5000;
// Start server
[Link](PORT, () => {
[Link](`Server running on port ${PORT}`);
});
// REGISTER
[Link]("/register", async (req, res) => {
try {
const { name, email, password, role } = [Link];
// LOGIN
[Link]("/login", async (req, res) => {
try {
const { email, password, role } = [Link];
if (!user) {
return [Link](400).json({ message: "Invalid credentials" });
}
[Link]({
message: "Login successful",
user: {
name: [Link],
email: [Link],
role: [Link]
}
});
} catch (err) {
[Link](err);
[Link](500).json({ message: "Server error" });
}
});
[Link] = router;
5. css / [Link]
📄 File: css/[Link]
Lines of code: 62
/* ============================================================
RetailPro — CSS Variables (Design Tokens)
File: css/[Link]
Used by: [Link], pages/[Link], pages/[Link]
============================================================ */
@import url('[Link]
family=Sora:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
:root {
/* ── Background layers ── */
--navy: #0D1117;
--navy2: #161B22;
--navy3: #1C2333;
--navy4: #21262D;
/* ── Borders ── */
--border: #30363D;
/* ── Semantic colours ── */
--rose: #FF4D6D;
--rose-dim: rgba(255, 77, 109, 0.12);
--amber: #F5A623;
--amber-dim: rgba(245, 166, 35, 0.10);
--sky: #38BDF8;
--sky-dim: rgba(56, 189, 248, 0.10);
--violet: #A78BFA;
--violet-dim: rgba(167, 139, 250, 0.12);
/* ── Text ── */
--text1: #F0F6FC;
--text2: #8B949E;
--text3: #484F58;
/* ── Typography ── */
--font: 'Sora', sans-serif;
--mono: 'JetBrains Mono', monospace;
/* ── Border radius ── */
--r4: 4px;
--r8: 8px;
--r12: 12px;
--r16: 16px;
--r20: 20px;
/* ── Shadows ── */
--shadow: 0 8px 32px rgba(0, 0, 0, 0.50);
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.60);
6. css / [Link]
📄 File: css/[Link]
Lines of code: 653
/* ============================================================
RetailPro — Auth Page Styles
File: css/[Link]
Covers: reset, layout, left panel, right panel,
forms, tabs, role toggle, toast, responsive.
============================================================ */
/* ── Reset ─────────────────────────────────────────────── */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* ════════════════════════════════════════════════════════
LEFT PANEL
════════════════════════════════════════════════════════ */
.left-panel {
flex: 1;
background: var(--navy2);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 48px 52px;
position: relative;
overflow: hidden;
}
.left-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
height: 100%;
}
/* ── Brand ──────────────────────────────────────────────── */
.brand-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 56px;
}
.brand-logo {
width: 42px;
height: 42px;
background: linear-gradient(135deg, var(--indigo), var(--emerald));
border-radius: var(--r12);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
box-shadow: 0 4px 20px rgba(91,99,254,0.3);
flex-shrink: 0;
}
.brand-name {
font-size: 20px;
font-weight: 800;
color: var(--text1);
letter-spacing: -0.5px;
}
.brand-tag {
font-size: 11px;
color: var(--text2);
letter-spacing: 0.05em;
}
/* ════════════════════════════════════════════════════════
RIGHT PANEL
════════════════════════════════════════════════════════ */
.right-panel {
width: 480px;
flex-shrink: 0;
background: var(--navy);
display: flex;
align-items: center;
justify-content: center;
padding: 32px 40px;
position: relative;
overflow-y: auto;
}
.auth-card {
width: 100%;
max-width: 400px;
animation: slideIn 0.4s cubic-bezier(0.34, 1.2, 0.64, 1);
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(16px); }
.form-input {
width: 100%;
background: var(--navy2);
border: 1px solid var(--border);
border-radius: var(--r8);
padding: 11px 14px;
font-size: 13px;
color: var(--text1);
font-family: var(--font);
outline: none;
transition: border 0.2s, box-shadow 0.2s;
}
.form-input:focus {
border-color: var(--indigo);
box-shadow: 0 0 0 3px rgba(91,99,254,0.12);
}
.[Link]:focus {
border-color: var(--emerald);
box-shadow: 0 0 0 3px rgba(16,217,160,0.10);
}
.form-input::placeholder { color: var(--text3); }
.[Link] { border-color: var(--rose); }
/* Validation messages */
.error-msg {
font-size: 11px;
color: var(--rose);
.success-msg {
font-size: 11px;
color: var(--emerald);
display: none;
margin-top: 4px;
}
.[Link] { display: block; }
/* ════════════════════════════════════════════════════════
TOAST NOTIFICATIONS
════════════════════════════════════════════════════════ */
.toast-container {
position: fixed;
bottom: 24px;
right: 24px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 300;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
background: var(--navy2);
border: 1px solid var(--border);
border-radius: var(--r12);
padding: 12px 16px;
box-shadow: var(--shadow);
min-width: 280px;
pointer-events: auto;
animation: toastIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
transition: opacity 0.3s, transform 0.3s;
}
@keyframes toastIn {
from { opacity: 0; transform: translateX(40px); }
to { opacity: 1; transform: translateX(0); }
}
.[Link] {
opacity: 0;
transform: translateX(40px);
}
.toast-icon { font-size: 16px; flex-shrink: 0; }
/* ════════════════════════════════════════════════════════
RESPONSIVE
════════════════════════════════════════════════════════ */
.toast-container {
bottom: 12px;
right: 12px;
left: 12px;
}
.toast { min-width: unset; width: 100%; }
}
7. css / [Link]
📄 File: css/[Link]
Lines of code: 1571
/* ============================================================
RetailPro — Shopkeeper Dashboard Styles
File: css/[Link]
Requires: css/[Link] (imported first)
Sections:
1. Reset & Base
2. Layout Shell (sidebar + main)
3. Sidebar
4. Top Navbar
5. Content area
6. KPI Cards
7. Charts section
8. Tables
9. Badges & Pills
10. Forms & Inputs (drawer / modal)
11. Modal / Drawer
12. Buttons
13. Toast
14. Empty States
15. Specific page sections
— Products — Inventory — Orders
— Customers — Analytics — Settings
16. Utilities
17. Responsive (tablet → mobile)
============================================================ */
/* ════════════════════════════════════════════════════════
1. RESET & BASE
════════════════════════════════════════════════════════ */
*,
*::before,
*::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font);
background: var(--navy);
color: var(--text1);
min-height: 100vh;
overflow: hidden;
}
/* ════════════════════════════════════════════════════════
2. LAYOUT SHELL
════════════════════════════════════════════════════════ */
#app {
display: flex;
height: 100vh;
/* ════════════════════════════════════════════════════════
3. SIDEBAR
════════════════════════════════════════════════════════ */
#sidebar {
width: 240px;
flex-shrink: 0;
background: var(--navy2);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
position: relative;
z-index: 30;
}
#[Link] { width: 64px; }
/* Brand */
.sb-brand {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 18px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
overflow: hidden;
}
.sb-logo-icon {
width: 38px;
height: 38px;
border-radius: var(--r12);
background: linear-gradient(135deg, var(--indigo), var(--indigo-l));
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
box-shadow: 0 4px 16px rgba(91,99,254,0.3);
}
.sb-brand-text { overflow: hidden; white-space: nowrap; }
.sb-brand-name { font-size: 15px; font-weight: 800; color: var(--text1); letter-spacing: -0.4px; }
.sb-brand-sub { font-size: 10px; color: var(--text3); font-weight: 600; letter-spacing: 1.2px; text-
transform: uppercase; margin-top: 1px; }
/* Nav */
.sb-nav {
flex: 1;
padding: 12px 8px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1px;
}
.sb-section-label {
font-size: 9.5px;
font-weight: 800;
color: var(--text3);
letter-spacing: 1.6px;
text-transform: uppercase;
padding: 8px 11px 4px;
white-space: nowrap;
overflow: hidden;
}
.sb-divider { height: 1px; background: var(--border); margin: 6px 4px; }
.sb-item {
display: flex;
align-items: center;
/* ════════════════════════════════════════════════════════
4. TOP NAVBAR
════════════════════════════════════════════════════════ */
#navbar {
height: 56px;
background: var(--navy2);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
padding: 0 20px;
flex-shrink: 0;
}
.nb-toggle {
width: 34px;
height: 34px;
background: var(--navy3);
border: 1px solid var(--border);
border-radius: var(--r8);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text2);
flex-shrink: 0;
transition: all 0.15s;
}
.nb-toggle:hover { background: var(--navy4); }
.nb-search-wrap {
flex: 1;
max-width: 360px;
position: relative;
}
.nb-search-icon {
position: absolute;
left: 11px;
top: 50%;
transform: translateY(-50%);
color: var(--text3);
pointer-events: none;
}
.nb-search {
width: 100%;
background: var(--navy3);
border: 1px solid var(--border);
border-radius: var(--r8);
padding: 8px 12px 8px 36px;
font-size: 13px;
color: var(--text1);
outline: none;
transition: border 0.2s, box-shadow 0.2s;
}
.nb-search:focus {
border-color: var(--indigo);
box-shadow: 0 0 0 3px var(--indigo-dim);
}
.nb-search::placeholder { color: var(--text3); }
.nb-right {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
.nb-date {
font-size: 11.5px;
color: var(--text3);
font-weight: 500;
white-space: nowrap;
}
.nb-vdiv { width: 1px; height: 18px; background: var(--border); }
/* Icon button */
.nb-ic-btn {
width: 34px;
height: 34px;
background: var(--navy3);
border: 1px solid var(--border);
border-radius: var(--r8);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text2);
position: relative;
transition: all 0.15s;
flex-shrink: 0;
}
.nb-ic-btn:hover { background: var(--navy4); }
/* Notification dot */
.notif-dot {
position: absolute;
top: 6px;
right: 7px;
width: 7px;
height: 7px;
background: var(--rose);
border-radius: 50%;
border: 1.5px solid var(--navy2);
animation: pulse-dot 2s infinite;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.notif-item {
display: flex;
gap: 11px;
align-items: flex-start;
padding: 12px 16px;
border-bottom: 1px solid rgba(48,54,61,0.5);
transition: background 0.12s;
cursor: pointer;
}
.notif-item:hover { background: var(--navy3); }
.[Link] { background: rgba(91,99,254,0.04); }
.notif-ic {
width: 30px;
height: 30px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 1px;
font-size: 14px;
}
.notif-body { flex: 1; }
.notif-text { font-size: 12.5px; color: var(--text2); line-height: 1.45; }
.notif-time { font-size: 10.5px; color: var(--text3); margin-top: 3px; }
.notif-unread-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--indigo);
flex-shrink: 0;
margin-top: 5px;
}
/* Quick-add button */
.btn-quick-add {
display: flex;
align-items: center;
gap: 6px;
background: linear-gradient(135deg, var(--indigo), var(--indigo-l));
border: none;
border-radius: var(--r8);
color: #fff;
font-size: 13px;
font-weight: 700;
padding: 7px 15px;
cursor: pointer;
box-shadow: 0 3px 12px rgba(91,99,254,0.3);
transition: all 0.2s;
white-space: nowrap;
flex-shrink: 0;
}
.btn-quick-add:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(91,99,254,0.45);
}
/* ════════════════════════════════════════════════════════
5. MAIN & CONTENT AREA
#content {
flex: 1;
overflow-y: auto;
padding: 24px 22px;
}
/* Page sections */
.page-section { display: none; }
.[Link] { display: block; animation: fadeUp 0.28s ease; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* Page header */
.page-hd { margin-bottom: 22px; }
.page-title { font-size: 22px; font-weight: 900; color: var(--text1); letter-spacing: -0.5px; }
.page-sub { font-size: 13px; color: var(--text3); margin-top: 4px; }
.section-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 18px;
flex-wrap: wrap;
}
/* ════════════════════════════════════════════════════════
6. KPI CARDS
════════════════════════════════════════════════════════ */
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 22px;
}
.kpi-card {
background: var(--navy2);
border: 1px solid var(--border);
border-radius: var(--r16);
padding: 18px;
position: relative;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
}
.kpi-card:hover {
transform: translateY(-2px);
border-color: var(--indigo);
box-shadow: 0 8px 30px rgba(91,99,254,0.12);
}
.kpi-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2.5px;
border-radius: var(--r16) var(--r16) 0 0;
}
.[Link]::before { background: linear-gradient(90deg, var(--indigo), var(--indigo-l)); }
.kpi-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.kpi-label {
font-size: 10.5px;
font-weight: 800;
color: var(--text3);
letter-spacing: 0.6px;
text-transform: uppercase;
}
.kpi-icon-wrap {
width: 34px;
height: 34px;
border-radius: var(--r8);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.[Link] { background: var(--indigo-dim); }
.[Link] { background: var(--emerald-dim); }
.[Link] { background: var(--amber-dim); }
.[Link] { background: var(--rose-dim); }
.[Link] { background: var(--sky-dim); }
.kpi-value {
font-size: 28px;
font-weight: 900;
color: var(--text1);
letter-spacing: -1.5px;
line-height: 1;
margin-bottom: 10px;
font-variant-numeric: tabular-nums;
font-family: var(--mono);
}
.kpi-footer {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.kpi-change {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
font-weight: 700;
padding: 2px 7px;
border-radius: 99px;
}
.[Link] { background: var(--emerald-dim); color: var(--emerald); }
.[Link] { background: var(--rose-dim); color: var(--rose); }
.[Link] { background: var(--amber-dim); color: var(--amber); }
.kpi-note { font-size: 11px; color: var(--text3); }
/* ── Grid variants ── */
.g2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.g3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; }
.g4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 14px; }
.g-main-side { display: grid; grid-template-columns: 1fr 300px; gap: 16px; }
.g-main-wide { display: grid; grid-template-columns: 1fr 340px; gap: 16px; }
.chart-wrap {
position: relative;
width: 100%;
height: 220px;
}
.time-tabs {
display: flex;
gap: 5px;
}
.time-tab {
padding: 5px 13px;
border-radius: var(--r8);
border: none;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
background: var(--navy3);
color: var(--text3);
}
.[Link] { background: var(--indigo); color: #fff; }
/* ════════════════════════════════════════════════════════
8. TABLES
════════════════════════════════════════════════════════ */
.tbl-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
tbody tr {
border-bottom: 1px solid rgba(48,54,61,0.4);
cursor: pointer;
transition: background 0.12s;
}
tbody tr:hover { background: rgba(255,255,255,0.015); }
tbody tr:last-child { border-bottom: none; }
tbody td {
padding: 12px 13px;
font-size: 13.5px;
color: var(--text1);
vertical-align: middle;
}
tbody td:first-child { padding-left: 0; }
/* ════════════════════════════════════════════════════════
9. BADGES & PILLS
════════════════════════════════════════════════════════ */
.badge {
display: inline-flex;
align-items: center;
padding: 2.5px 9px;
border-radius: 99px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.2px;
white-space: nowrap;
/* Category chip */
.chip {
background: var(--navy3);
border-radius: 5px;
padding: 2px 8px;
font-size: 11px;
color: var(--text3);
}
/* ════════════════════════════════════════════════════════
10. FORMS & INPUTS
════════════════════════════════════════════════════════ */
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 16px;
}
.form-label {
font-size: 11px;
font-weight: 700;
color: var(--text2);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.input-wrap { position: relative; }
.form-input, .form-select {
width: 100%;
background: var(--navy3);
border: 1px solid var(--border);
border-radius: var(--r8);
padding: 10px 13px;
font-size: 13.5px;
color: var(--text1);
outline: none;
transition: border 0.2s, box-shadow 0.2s;
}
.form-input:focus, .form-select:focus {
border-color: var(--indigo);
box-shadow: 0 0 0 3px var(--indigo-dim);
}
.form-input::placeholder { color: var(--text3); }
.[Link] { border-color: var(--rose); box-shadow: 0 0 0 3px var(--rose-dim); }
.form-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
color: var(--text3);
pointer-events: none;
.f-error {
font-size: 11px;
color: var(--rose);
display: none;
}
.[Link] { display: block; }
/* Upload area */
.upload-area {
border: 2px dashed var(--border);
border-radius: 11px;
padding: 22px 16px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
margin-bottom: 16px;
}
.upload-area:hover { border-color: var(--indigo); background: var(--indigo-dim); }
.upload-emoji { font-size: 30px; margin-bottom: 6px; }
.upload-txt { font-size: 12.5px; color: var(--text3); }
/* Status toggle */
.status-toggle { display: flex; gap: 8px; }
.status-btn {
flex: 1;
padding: 9px;
border-radius: var(--r8);
border: 1px solid var(--border);
background: transparent;
color: var(--text3);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.[Link]-green {
border-color: var(--emerald);
/* ════════════════════════════════════════════════════════
11. MODAL / DRAWER
════════════════════════════════════════════════════════ */
#modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 100;
display: none;
backdrop-filter: blur(2px);
}
#[Link] { display: block; }
.drawer-hd {
padding: 18px 22px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.drawer-title { font-size: 16px; font-weight: 800; color: var(--text1); }
.drawer-sub { font-size: 12px; color: var(--text3); margin-top: 2px; }
.drawer-close {
width: 30px;
height: 30px;
background: var(--navy3);
border: none;
border-radius: var(--r8);
color: var(--text3);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.drawer-close:hover { background: var(--navy4); color: var(--text1); }
.drawer-ft {
padding: 14px 22px;
border-top: 1px solid var(--border);
display: flex;
gap: 10px;
flex-shrink: 0;
}
/* ════════════════════════════════════════════════════════
12. BUTTONS
════════════════════════════════════════════════════════ */
.btn-primary {
background: linear-gradient(135deg, var(--indigo), var(--indigo-l));
border: none;
border-radius: var(--r12);
color: #fff;
font-size: 13.5px;
font-weight: 700;
padding: 10px 20px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 7px;
box-shadow: 0 3px 12px rgba(91,99,254,0.3);
transition: all 0.2s;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 6px 22px rgba(91,99,254,0.45);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.btn-secondary {
background: var(--navy3);
border: 1px solid var(--border);
border-radius: var(--r12);
color: var(--text2);
font-size: 13px;
font-weight: 600;
padding: 10px 18px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 7px;
transition: all 0.15s;
}
.btn-secondary:hover { background: var(--navy4); color: var(--text1); }
.btn-ghost {
background: transparent;
border: 1px solid var(--border);
border-radius: var(--r12);
color: var(--text3);
font-size: 13px;
font-weight: 600;
padding: 10px 18px;
cursor: pointer;
transition: all 0.15s;
}
.btn-ghost:hover { border-color: var(--indigo); color: var(--indigo-l); }
.btn-danger {
background: var(--rose-dim);
/* ════════════════════════════════════════════════════════
13. TOAST
════════════════════════════════════════════════════════ */
.toast-container {
position: fixed;
bottom: 22px;
right: 22px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 500;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
background: var(--navy2);
border: 1px solid var(--border);
border-radius: var(--r12);
padding: 12px 16px;
min-width: 280px;
box-shadow: var(--shadow);
pointer-events: auto;
animation: toastIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
transition: opacity 0.3s, transform 0.3s;
}
@keyframes toastIn {
from { opacity: 0; transform: translateX(40px); }
to { opacity: 1; transform: translateX(0); }
}
.[Link] { opacity: 0; transform: translateX(40px); }
.toast-icon { font-size: 16px; flex-shrink: 0; }
.toast-msg { font-size: 13px; color: var(--text1); font-weight: 500; }
/* ════════════════════════════════════════════════════════
14. EMPTY STATES
════════════════════════════════════════════════════════ */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 52px 20px;
text-align: center;
}
.empty-icon { font-size: 52px; margin-bottom: 14px; }
.empty-title { font-size: 16px; font-weight: 800; color: var(--text1); margin-bottom: 6px; }
.empty-sub { font-size: 13px; color: var(--text3); max-width: 280px; line-height: 1.6; margin-
bottom: 20px; }
/* Filter tabs */
/* Progress bar */
.prog-bar { height: 5px; background: var(--navy4); border-radius: 99px; overflow: hidden; }
.prog-fill { height: 100%; border-radius: 99px; transition: width 1.2s ease; }
.prog-bar-lg { height: 7px; background: var(--navy4); border-radius: 99px; overflow: hidden; }
/* ════════════════════════════════════════════════════════
15. PAGE-SPECIFIC SECTIONS
════════════════════════════════════════════════════════ */
/* ── Invoice preview ── */
.invoice-preview {
background: #fff;
color: #1e293b;
border-radius: var(--r16);
padding: 26px;
font-size: 13px;
line-height: 1.6;
}
.inv-logo { font-size: 17px; font-weight: 900; color: #1a1a2e; margin-bottom: 3px; }
.inv-address { font-size: 11.5px; color: #64748b; margin-bottom: 18px; }
.inv-hd-row { display: flex; justify-content: space-between; margin-bottom: 14px; }
.inv-bill-to .lbl { font-size: 10px; color: #64748b; font-weight: 700; text-transform: uppercase;
letter-spacing: 1px; }
.inv-divider { border: none; border-top: 1px solid #e2e8f0; margin: 10px 0; }
.inv-summary-row {
display: flex;
justify-content: space-between;
font-size: 12.5px;
color: #64748b;
margin-bottom: 5px;
}
.inv-total-row {
display: flex;
justify-content: space-between;
font-size: 17px;
font-weight: 900;
color: #1a1a2e;
border-top: 2px solid var(--indigo);
padding-top: 10px;
margin-top: 6px;
}
.inv-pay-chip {
margin-top: 14px;
padding: 7px 12px;
background: #f1f5f9;
border-radius: 8px;
font-size: 12px;
font-weight: 700;
color: var(--indigo);
text-align: center;
}
/* ── Analytics ── */
.analytics-kpi {
background: var(--navy2);
border: 1px solid var(--border);
border-radius: var(--r12);
padding: 18px;
border-left: 3px solid;
}
/* ── Settings sections ── */
.settings-panel {
background: var(--navy2);
border: 1px solid var(--border);
border-radius: var(--r16);
padding: 22px;
margin-bottom: 16px;
}
.settings-panel-title {
font-size: 14px;
font-weight: 800;
margin-bottom: 18px;
.notif-pref-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 11px 0;
border-bottom: 1px solid rgba(48,54,61,0.4);
}
.notif-pref-row:last-child { border-bottom: none; }
.notif-pref-label { font-size: 13.5px; color: var(--text1); }
.op-hours-row {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 14px;
background: var(--navy3);
border-radius: var(--r8);
margin-bottom: 9px;
flex-wrap: wrap;
gap: 8px;
}
.op-day-label { font-size: 13px; font-weight: 600; min-width: 110px; }
/* ── Ledger ── */
.ledger-credit { color: var(--emerald); font-weight: 700; }
.ledger-debit { color: var(--rose); font-weight: 700; }
/* Expense donut */
.expense-summary-box {
background: linear-gradient(135deg, rgba(91,99,254,0.07), rgba(123,130,255,0.07));
border: 1px solid rgba(91,99,254,0.15);
border-radius: var(--r16);
padding: 22px;
text-align: center;
margin-bottom: 16px;
}
/* ════════════════════════════════════════════════════════
16. UTILITIES
════════════════════════════════════════════════════════ */
.mb4 { margin-bottom: 4px; }
.mb8 { margin-bottom: 8px; }
.mb12 { margin-bottom: 12px; }
.mb16 { margin-bottom: 16px; }
.mb20 { margin-bottom: 20px; }
.mb24 { margin-bottom: 24px; }
.mt4 { margin-top: 4px; }
.mt8 { margin-top: 8px; }
.mt12 { margin-top: 12px; }
.mt16 { margin-top: 16px; }
/* ════════════════════════════════════════════════════════
17. RESPONSIVE
════════════════════════════════════════════════════════ */
/* ── Mobile ≤ 640px ── */
@media (max-width: 640px) {
.kpi-grid { grid-template-columns: 1fr 1fr; }
.g3 { grid-template-columns: 1fr; }
.g4 { grid-template-columns: 1fr 1fr; }
.two-col { grid-template-columns: 1fr; }
#content { padding: 14px 14px; }
.card { padding: 15px; }
.kpi-card { padding: 14px; }
.kpi-value { font-size: 23px; }
#drawer { width: 100%; border-left: none; }
.toast-container { bottom: 12px; right: 12px; left: 12px; }
.toast { min-width: unset; width: 100%; }
thead th:nth-child(n+4):not(:last-child) { display: none; }
tbody td:nth-child(n+4):not(:last-child) { display: none; }
.btn-quick-add span { display: none; }
.btn-quick-add { padding: 7px 10px; }
.nb-search-wrap { max-width: 140px; }
.inv-stat-grid { grid-template-columns: 1fr 1fr; }
}
8. css / [Link]
📄 File: css/[Link]
Lines of code: 945
/* ============================================================
RetailPro — Customer Dashboard CSS
File: RetailPro/css/[Link]
Matches existing dark theme: Sora font, navy palette,
emerald accent, indigo secondary.
============================================================ */
@import url('[Link]
family=Sora:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
/* ── Reset ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* ── Base ───────────────────────────────────────────────── */
body {
font-family: var(--font);
background: var(--navy);
color: var(--text1);
display: flex;
min-height: 100vh;
font-size: 14px;
line-height: 1.5;
}
a { text-decoration: none; color: inherit; }
button { cursor: pointer; font-family: var(--font); }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--navy); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* ════════════════════════════════════════════
SIDEBAR
════════════════════════════════════════════ */
.sidebar {
width: var(--sw); background: var(--navy2); border-right: 1px solid var(--border);
display: flex; flex-direction: column; position: fixed; top: 0; left: 0; bottom: 0; z-index: 200;
transition: transform .3s;
}
.sidebar-brand {
padding: 20px 16px 16px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 10px;
}
.brand-logo {
width: 34px; height: 34px; background: linear-gradient(135deg,var(--emerald),var(--sky));
.section-label {
font-size: 10px; font-weight: 600; color: var(--text3);
text-transform: uppercase; letter-spacing: .1em; padding: 18px 16px 7px;
}
.nav-item {
display: flex; align-items: center; gap: 10px; padding: 10px 14px;
margin: 1px 8px; border-radius: var(--r8); color: var(--text2);
font-size: 13px; font-weight: 500; cursor: pointer; transition: all .15s; position: relative;
}
.nav-item:hover { background: var(--navy3); color: var(--text1); }
.[Link] { background: var(--emerald-dim); color: var(--emerald); }
.[Link]::before {
content: ''; position: absolute; left: -8px; top: 50%; transform: translateY(-50%);
width: 3px; height: 20px; background: var(--emerald); border-radius: 0 2px 2px 0;
}
.nav-icon {
width: 32px; height: 32px; border-radius: var(--r8);
display: flex; align-items: center; justify-content: center; font-size: 15px;
background: rgba(255,255,255,.04); flex-shrink: 0; transition: all .15s;
}
.[Link] .nav-icon { background: var(--emerald-dim); }
.nav-badge {
margin-left: auto; background: var(--rose); color: white;
font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 99px;
}
.nav-count {
margin-left: auto; background: var(--emerald); color: var(--navy);
font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 99px;
}
.sidebar-footer { margin-top: auto; padding: 12px 8px; border-top: 1px solid var(--border); }
.user-card {
display: flex; align-items: center; gap: 10px; padding: 10px;
border-radius: var(--r8); cursor: pointer; transition: background .15s;
}
.user-card:hover { background: var(--navy3); }
.user-avatar {
width: 34px; height: 34px; border-radius: 50%;
background: linear-gradient(135deg,var(--emerald),var(--sky));
display: flex; align-items: center; justify-content: center;
font-size: 13px; font-weight: 700; color: var(--navy); flex-shrink: 0;
}
.user-info .uname { font-size: 13px; font-weight: 600; color: var(--text1); }
.user-info .uemail { font-size: 11px; color: var(--text2); }
.logout-btn {
display: flex; align-items: center; gap: 8px; padding: 8px 10px; margin-top: 4px;
border-radius: var(--r8); color: var(--rose); font-size: 12px; font-weight: 500;
cursor: pointer; transition: background .15s;
}
.logout-btn:hover { background: var(--rose-dim); }
/* ════════════════════════════════════════════
.search-topbar {
display: flex; align-items: center; gap: 8px;
background: var(--navy3); border: 1px solid var(--border);
border-radius: var(--r12); padding: 8px 14px; width: 240px; transition: border .2s;
}
.search-topbar:focus-within { border-color: var(--emerald); }
.search-topbar input { background: none; border: none; outline: none; color: var(--text1); font-
family: var(--font); font-size: 13px; flex: 1; }
.search-topbar input::placeholder { color: var(--text2); }
.icon-btn {
width: 36px; height: 36px; background: var(--navy3); border: 1px solid var(--border);
border-radius: var(--r8); display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all .15s; font-size: 15px; position: relative;
}
.icon-btn:hover { border-color: var(--emerald); background: var(--emerald-dim); }
.notif-dot {
position: absolute; top: 6px; right: 6px; width: 7px; height: 7px;
background: var(--rose); border-radius: 50%; border: 2px solid var(--navy2);
}
.cart-topbtn {
display: flex; align-items: center; gap: 7px; padding: 7px 14px;
background: var(--emerald-dim); border: 1px solid rgba(16,217,160,.3);
border-radius: var(--r8); cursor: pointer; transition: all .15s;
font-size: 12px; font-weight: 600; color: var(--emerald);
}
.cart-topbtn:hover { background: var(--emerald); color: var(--navy); }
.cart-count {
background: var(--rose); color: white; border-radius: 99px;
padding: 1px 6px; font-size: 10px; font-weight: 700;
}
/* ════════════════════════════════════════════
PAGE SYSTEM
════════════════════════════════════════════ */
.page { display: none; }
.[Link] { display: block; animation: fadeIn .3s ease; }
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
.page-content { padding: 24px 28px; }
/* ════════════════════════════════════════════
SHARED COMPONENTS
════════════════════════════════════════════ */
/* Section headers */
.sec-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;
}
.sec-title { font-size: 15px; font-weight: 700; }
.sec-sub { font-size: 12px; color: var(--text2); margin-top: 2px; }
.view-all-btn {
font-size: 12px; color: var(--emerald); background: none; border: none;
cursor: pointer; font-weight: 500; display: flex; align-items: center; gap: 4px;
}
.view-all-btn:hover { text-decoration: underline; }
/* Chips / filters */
.filter-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.chip {
padding: 6px 14px; border-radius: 99px; font-size: 12px; font-weight: 500;
/* Cards */
.card {
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r16); overflow: hidden;
}
.card-p { padding: 18px 20px; }
/* Badges */
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 99px; font-size: 11px; font-weight: 600;
}
.badge-emerald { background: var(--emerald-dim); color: var(--emerald); }
.badge-rose { background: var(--rose-dim); color: var(--rose); }
.badge-amber { background: var(--amber-dim); color: var(--amber); }
.badge-sky { background: var(--sky-dim); color: var(--sky); }
.badge-violet { background: var(--violet-dim); color: var(--violet); }
/* Buttons */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
padding: 8px 16px; border-radius: var(--r8); font-size: 13px; font-weight: 600;
border: none; cursor: pointer; transition: all .15s; font-family: var(--font);
}
.btn-primary { background: var(--emerald); color: var(--navy); }
.btn-primary:hover { background: var(--emerald-l); box-shadow: 0 4px 16px rgba(16,217,160,.35);
transform: translateY(-1px); }
.btn-secondary { background: var(--navy3); color: var(--text2); border: 1px solid var(--border); }
.btn-secondary:hover { color: var(--text1); border-color: var(--text3); }
.btn-ghost { background: transparent; color: var(--text2); border: 1px solid var(--border); }
.btn-ghost:hover { color: var(--text1); }
.btn-sm { padding: 5px 12px; font-size: 11px; }
.btn-icon { width: 32px; height: 32px; padding: 0; border-radius: var(--r8); }
/* Stars */
.stars { color: var(--amber); font-size: 12px; }
.star-count { font-size: 11px; color: var(--text2); margin-left: 4px; }
/* ════════════════════════════════════════════
HOME PAGE — HERO BANNER
════════════════════════════════════════════ */
.hero-banner {
background: linear-gradient(120deg,#0d1f3c 0%,#0f2040 40%,#0d1b38 100%);
border: 1px solid var(--border); border-radius: var(--r20);
padding: 28px 32px; margin-bottom: 22px; position: relative; overflow: hidden;
}
.hero-banner::before {
content: ''; position: absolute; right: -60px; top: -60px;
width: 280px; height: 280px;
background: radial-gradient(circle,rgba(16,217,160,.12) 0%,transparent 65%); border-radius: 50%;
}
.hero-banner::after {
content: ''; position: absolute; left: 30%; bottom: -80px;
width: 200px; height: 200px;
background: radial-gradient(circle,rgba(91,99,254,.1) 0%,transparent 65%); border-radius: 50%;
}
.hero-content { position: relative; z-index: 1; }
.hero-greeting { font-size: 11px; font-weight: 600; color: var(--emerald); text-transform: uppercase;
letter-spacing: .1em; margin-bottom: 6px; }
.hero-title { font-size: 24px; font-weight: 700; line-height: 1.25; margin-bottom: 8px; }
.hero-title span { color: var(--emerald); }
.hero-sub { font-size: 13px; color: var(--text2); margin-bottom: 18px; }
.hero-search {
display: flex; align-items: center; gap: 10px;
/* ════════════════════════════════════════════
CATEGORY PILLS
════════════════════════════════════════════ */
.cat-scroll { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 4px; margin-bottom: 20px; }
.cat-scroll::-webkit-scrollbar { height: 3px; }
.cat-pill {
display: flex; flex-direction: column; align-items: center; gap: 5px;
padding: 12px 16px; border-radius: var(--r12);
background: var(--navy2); border: 1px solid var(--border);
cursor: pointer; transition: all .2s; flex-shrink: 0; min-width: 75px;
}
.cat-pill:hover, .[Link] {
border-color: rgba(16,217,160,.4); background: var(--emerald-dim); color: var(--emerald);
}
.cat-pill-icon { font-size: 22px; }
.cat-pill-label { font-size: 11px; font-weight: 500; color: var(--text2); white-space: nowrap; }
.cat-pill:hover .cat-pill-label, .[Link] .cat-pill-label { color: var(--emerald); }
/* ════════════════════════════════════════════
MAP SECTION
════════════════════════════════════════════ */
.map-section { margin-bottom: 24px; }
.map-layout { display: grid; grid-template-columns: 1fr 340px; gap: 16px; }
.map-container {
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r16); overflow: hidden; position: relative; height: 480px;
}
#map { width: 100%; height: 100%; }
.map-controls {
position: absolute; top: 12px; right: 12px;
display: flex; flex-direction: column; gap: 6px; z-index: 10;
}
.map-btn {
width: 34px; height: 34px; background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r8); display: flex; align-items: center; justify-content: center;
font-size: 14px; cursor: pointer; transition: all .15s; color: var(--text1);
}
.map-btn:hover { border-color: var(--emerald); background: var(--emerald-dim); }
.map-overlay-search {
position: absolute; top: 12px; left: 12px; right: 60px; z-index: 10;
display: flex; gap: 8px;
}
.map-search-box {
flex: 1; display: flex; align-items: center; gap: 8px;
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r8); padding: 8px 12px;
}
.map-search-box input { background: none; border: none; outline: none; color: var(--text1); font-
family: var(--font); font-size: 13px; flex: 1; }
.map-search-box input::placeholder { color: var(--text2); }
.map-filter-bar {
position: absolute; bottom: 12px; left: 12px; right: 12px; z-index: 10;
display: flex; gap: 8px; overflow-x: auto;
}
.map-filter-bar::-webkit-scrollbar { display: none; }
.map-chip {
padding: 5px 12px; border-radius: 99px; font-size: 11px; font-weight: 600;
background: var(--navy2); border: 1px solid var(--border); color: var(--text2);
cursor: pointer; white-space: nowrap; transition: all .15s;
}
.[Link] { background: var(--emerald); color: var(--navy); border-color: var(--emerald); }
.map-chip:hover:not(.active) { border-color: var(--text3); color: var(--text1); }
.user-location-dot {
position: absolute; width: 14px; height: 14px; border-radius: 50%;
background: var(--sky); border: 2px solid white;
box-shadow: 0 0 0 6px rgba(56,189,248,.2);
animation: ping 2s ease-in-out infinite;
}
@keyframes ping {
0%,100% { box-shadow: 0 0 0 6px rgba(56,189,248,.2); }
50% { box-shadow: 0 0 0 12px rgba(56,189,248,.05); }
}
.shop-list-card {
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r12); padding: 14px; cursor: pointer;
transition: all .2s; position: relative;
}
.shop-list-card:hover, .[Link] {
border-color: var(--emerald); transform: translateX(2px);
}
.[Link] { background: var(--emerald-dim); }
.slc-header { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px; }
.slc-icon { font-size: 26px; flex-shrink: 0; }
.slc-name { font-size: 13px; font-weight: 700; margin-bottom: 2px; }
.slc-cat { font-size: 11px; color: var(--text2); }
.slc-meta { display: flex; gap: 8px; align-items: center; font-size: 11px; color: var(--text2);
margin-bottom: 8px; flex-wrap: wrap; }
.slc-dist { display: flex; align-items: center; gap: 3px; }
.slc-open { font-weight: 600; }
.[Link] { color: var(--emerald); }
.[Link] { color: var(--rose); }
.slc-actions { display: flex; gap: 6px; }
.slc-btn {
flex: 1; padding: 5px 8px; border-radius: var(--r8); font-size: 11px; font-weight: 600;
border: 1px solid var(--border); background: var(--navy3); color: var(--text2);
cursor: pointer; transition: all .15s; font-family: var(--font);
}
.[Link] { background: var(--emerald); color: var(--navy); border-color: var(--emerald); }
.slc-btn:hover:not(.primary) { color: var(--text1); }
.slc-fav-btn {
width: 30px; height: 30px; border-radius: var(--r8);
background: var(--navy3); border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
cursor: pointer; font-size: 14px; transition: all .15s; flex-shrink: 0;
}
.slc-fav-btn:hover, .[Link] { background: var(--rose-dim); color: var(--rose); border-
color: rgba(255,77,109,.3); }
.[Link] { color: var(--rose); }
/* ════════════════════════════════════════════
SHOP GRID CARDS
════════════════════════════════════════════ */
.shops-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin-bottom: 24px; }
.shop-card {
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r16); overflow: hidden; transition: all .2s; cursor: pointer;
}
.shop-card:hover { border-color: var(--emerald); transform: translateY(-2px); box-shadow: 0 8px 30px
rgba(0,0,0,.35); }
.shop-card-cover {
height: 100px; position: relative; overflow: hidden;
display: flex; align-items: center; justify-content: center; font-size: 42px;
/* ════════════════════════════════════════════
PRODUCT CARDS GRID
════════════════════════════════════════════ */
.products-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 14px; margin-bottom:
24px; }
.prod-card {
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r16); overflow: hidden; transition: all .2s; position: relative;
}
.prod-card:hover { border-color: var(--indigo); transform: translateY(-2px); box-shadow: 0 8px 24px
rgba(0,0,0,.3); }
.prod-thumb {
height: 110px; display: flex; align-items: center; justify-content: center;
font-size: 46px; position: relative; overflow: hidden;
}
.prod-stock-tag {
position: absolute; top: 8px; left: 8px;
padding: 3px 8px; border-radius: 99px; font-size: 10px; font-weight: 700;
}
.prod-wish-btn {
position: absolute; top: 8px; right: 8px; width: 26px; height: 26px;
border-radius: 50%; background: rgba(0,0,0,.5); border: none; cursor: pointer;
display: flex; align-items: center; justify-content: center; font-size: 12px;
transition: all .2s;
}
.prod-wish-btn:hover, .[Link] { background: var(--rose-dim); }
.prod-offer-tag {
position: absolute; bottom: 8px; left: 8px;
background: var(--rose); color: white;
padding: 2px 7px; border-radius: var(--r4); font-size: 10px; font-weight: 700;
}
/* ════════════════════════════════════════════
SHOP DETAIL MODAL
════════════════════════════════════════════ */
.modal-overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,.7); backdrop-filter: blur(6px);
z-index: 500; align-items: center; justify-content: center; padding: 20px;
}
.[Link] { display: flex; }
.shop-modal {
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r20); width: 100%; max-width: 780px; max-height: 88vh;
overflow-y: auto; animation: modalIn .3s cubic-bezier(.34,1.2,.64,1);
}
@keyframes modalIn { from{opacity:0;transform:scale(.94) translateY(20px)}
to{opacity:1;transform:scale(1) translateY(0)} }
.modal-header {
padding: 20px 24px; border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between; align-items: center;
position: sticky; top: 0; background: var(--navy2); z-index: 10;
}
.modal-close {
width: 32px; height: 32px; border-radius: var(--r8);
background: var(--navy3); border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
cursor: pointer; font-size: 16px; color: var(--text2); transition: all .15s;
}
.modal-close:hover { color: var(--text1); background: var(--navy4); }
.modal-body { padding: 24px; }
.shop-modal-hero {
display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;
}
.shop-modal-info { }
.shop-modal-icon-lg { font-size: 56px; margin-bottom: 12px; }
.shop-modal-name { font-size: 20px; font-weight: 800; margin-bottom: 6px; }
.shop-modal-meta { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--
text2); }
.shop-modal-meta-row { display: flex; align-items: center; gap: 7px; }
.shop-modal-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.shop-tag { padding: 4px 10px; border-radius: 99px; font-size: 10px; font-weight: 600; background:
var(--navy3); border: 1px solid var(--border); color: var(--text2); }
/* ════════════════════════════════════════════
CART PAGE
════════════════════════════════════════════ */
.cart-layout { display: grid; grid-template-columns: 1fr 300px; gap: 20px; }
.cart-section { background: var(--navy2); border: 1px solid var(--border); border-radius: var(--r16);
overflow: hidden; }
.cart-header { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; justify-
content: space-between; align-items: center; }
.cart-header-title { font-size: 14px; font-weight: 700; }
.cart-item {
display: flex; align-items: center; gap: 13px;
padding: 14px 18px; border-bottom: 1px solid rgba(48,54,61,.5);
transition: background .15s;
}
.cart-item:hover { background: rgba(255,255,255,.01); }
.cart-item:last-child { border-bottom: none; }
.cart-img {
width: 52px; height: 52px; border-radius: var(--r8);
background: var(--navy3); border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center; font-size: 26px; flex-shrink: 0;
}
.cart-item-name { font-size: 13px; font-weight: 600; margin-bottom: 2px; }
.cart-item-shop { font-size: 11px; color: var(--text2); }
.cart-item-price { font-size: 14px; font-weight: 700; font-family: var(--mono); color: var(--emerald);
margin-top: 4px; }
.qty-ctrl { display: flex; align-items: center; gap: 7px; }
.qty-btn {
width: 28px; height: 28px; border-radius: var(--r8);
background: var(--navy3); border: 1px solid var(--border);
cursor: pointer; font-size: 14px; font-weight: 700; color: var(--text1);
display: flex; align-items: center; justify-content: center; transition: all .15s;
}
.qty-btn:hover { border-color: var(--emerald); color: var(--emerald); }
.qty-val { font-family: var(--mono); font-size: 14px; font-weight: 600; min-width: 20px; text-align:
center; }
.rm-btn {
width: 28px; height: 28px; border-radius: var(--r8);
background: var(--rose-dim); border: 1px solid rgba(255,77,109,.2);
cursor: pointer; font-size: 13px; color: var(--rose);
display: flex; align-items: center; justify-content: center; transition: all .15s;
}
.rm-btn:hover { background: var(--rose); color: white; }
/* Compare bar */
.compare-bar {
position: fixed; bottom: 0; left: var(--sw); right: 0;
background: var(--navy2); border-top: 1px solid var(--border);
padding: 12px 24px; display: none; align-items: center; gap: 16px;
z-index: 90;
}
.[Link] { display: flex; }
.compare-items { display: flex; gap: 10px; flex: 1; }
.compare-item-slot {
width: 100px; height: 52px; border: 1px dashed var(--border);
border-radius: var(--r8); display: flex; align-items: center; justify-content: center;
font-size: 11px; color: var(--text3); position: relative;
}
.[Link] { border-style: solid; border-color: var(--emerald); background: var(--
emerald-dim); }
.compare-item-slot .rm { position: absolute; top: -6px; right: -6px; width: 16px; height: 16px;
border-radius: 50%; background: var(--rose); color: white; font-size: 10px; display: flex; align-
items: center; justify-content: center; cursor: pointer; }
/* ════════════════════════════════════════════
ORDERS PAGE
════════════════════════════════════════════ */
.order-stats-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 12px; margin-bottom:
20px; }
.order-stat-card {
background: var(--navy2); border: 1px solid var(--border); border-radius: var(--r12);
padding: 14px 16px; display: flex; align-items: center; gap: 12px;
}
.osi { width: 36px; height: 36px; border-radius: var(--r8); display: flex; align-items: center;
justify-content: center; font-size: 17px; }
.osv { font-size: 20px; font-weight: 700; font-family: var(--mono); }
.osl { font-size: 11px; color: var(--text2); }
.order-card {
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r16); margin-bottom: 12px; overflow: hidden;
}
.order-card-header {
padding: 14px 18px; display: flex; align-items: center; gap: 12px;
border-bottom: 1px solid rgba(48,54,61,.5);
}
.order-shop-icon {
width: 38px; height: 38px; border-radius: var(--r8);
background: var(--navy3); border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0;
}
.order-id { font-family: var(--mono); font-size: 12px; color: var(--indigo-l); }
.order-meta { font-size: 11px; color: var(--text2); }
.order-amount { font-family: var(--mono); font-size: 16px; font-weight: 700; color: var(--emerald);
margin-left: auto; }
.order-items-row { padding: 10px 18px; display: flex; gap: 8px; flex-wrap: wrap; }
.order-item-chip {
display: flex; align-items: center; gap: 5px; padding: 4px 10px;
background: var(--navy3); border: 1px solid var(--border);
border-radius: var(--r8); font-size: 11px; color: var(--text2);
}
.order-timeline { padding: 12px 18px; border-top: 1px solid rgba(48,54,61,.5); display: flex; align-
items: center; }
.timeline-step { display: flex; flex-direction: column; align-items: center; flex: 1; position:
relative; }
.timeline-step:not(:last-child)::after {
content: ''; position: absolute; top: 11px; left: 50%;
.order-actions { padding: 10px 18px; display: flex; gap: 8px; border-top: 1px solid rgba(48,54,61,.5);
}
/* ════════════════════════════════════════════
WISHLIST PAGE
════════════════════════════════════════════ */
.wishlist-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 14px; }
/* ════════════════════════════════════════════
NOTIFICATIONS PANEL
════════════════════════════════════════════ */
.notif-drawer {
position: fixed; top: 0; right: -380px; width: 380px; bottom: 0;
background: var(--navy2); border-left: 1px solid var(--border);
z-index: 300; transition: right .3s ease; display: flex; flex-direction: column;
}
.[Link] { right: 0; }
.notif-drawer-header {
padding: 20px; border-bottom: 1px solid var(--border);
display: flex; justify-content: space-between; align-items: center;
}
.notif-drawer-body { flex: 1; overflow-y: auto; }
.notif-item {
padding: 14px 20px; border-bottom: 1px solid rgba(48,54,61,.5);
display: flex; gap: 12px; align-items: flex-start; transition: background .15s; cursor: pointer;
}
.notif-item:hover { background: rgba(255,255,255,.02); }
.[Link] { background: rgba(16,217,160,.04); }
.notif-icon { font-size: 20px; flex-shrink: 0; margin-top: 2px; }
.notif-title { font-size: 13px; font-weight: 600; margin-bottom: 3px; }
.notif-body { font-size: 12px; color: var(--text2); line-height: 1.5; }
.notif-time { font-size: 10px; color: var(--text3); margin-top: 4px; }
.notif-unread-dot { width: 7px; height: 7px; background: var(--emerald); border-radius: 50%; margin-
top: 6px; flex-shrink: 0; }
/* ════════════════════════════════════════════
PROFILE PAGE
════════════════════════════════════════════ */
.profile-layout { display: grid; grid-template-columns: 280px 1fr; gap: 20px; }
.profile-sidebar-card {
background: var(--navy2); border: 1px solid var(--border);
border-radius: var(--r16); padding: 24px; text-align: center;
position: sticky; top: 76px;
}
.profile-avatar-lg {
width: 72px; height: 72px; border-radius: 50%;
background: linear-gradient(135deg,var(--emerald),var(--sky));
display: flex; align-items: center; justify-content: center;
font-size: 28px; font-weight: 700; color: var(--navy);
margin: 0 auto 14px;
}
.profile-name { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
.profile-email { font-size: 12px; color: var(--text2); margin-bottom: 16px; }
/* Address card */
.address-card {
background: var(--navy3); border: 1px solid var(--border);
border-radius: var(--r12); padding: 14px; position: relative;
}
.[Link] { border-color: rgba(16,217,160,.4); background: var(--emerald-dim); }
.address-default-badge { font-size: 10px; font-weight: 700; color: var(--emerald); margin-bottom: 6px;
}
/* ════════════════════════════════════════════
DATA TABLE
════════════════════════════════════════════ */
.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.data-table th {
padding: 10px 16px; font-size: 11px; font-weight: 600; color: var(--text2);
text-transform: uppercase; letter-spacing: .06em;
background: var(--navy3); border-bottom: 1px solid var(--border); text-align: left;
}
.data-table td { padding: 14px 16px; border-bottom: 1px solid rgba(48,54,61,.5); }
.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: rgba(255,255,255,.015); }
.td-muted { color: var(--text2); font-size: 12px; }
/* ════════════════════════════════════════════
RECENTLY VIEWED / HORIZONTAL SCROLL
════════════════════════════════════════════ */
.h-scroll { display: flex; gap: 14px; overflow-x: auto; padding-bottom: 6px; }
.h-scroll::-webkit-scrollbar { height: 4px; }
.h-scroll-item { flex-shrink: 0; }
/* ════════════════════════════════════════════
TOAST
════════════════════════════════════════════ */
.toast-container {
position: fixed; bottom: 24px; right: 24px;
display: flex; flex-direction: column; gap: 8px; z-index: 999; pointer-events: none;
}
.toast {
display: flex; align-items: center; gap: 10px;
/* ════════════════════════════════════════════
MOBILE BOTTOM NAV
════════════════════════════════════════════ */
.mobile-bottom-nav {
display: none; position: fixed; bottom: 0; left: 0; right: 0;
background: var(--navy2); border-top: 1px solid var(--border);
z-index: 150; padding: 8px 0 10px;
}
.mbn-items { display: flex; justify-content: space-around; }
.mbn-item {
display: flex; flex-direction: column; align-items: center; gap: 3px;
padding: 5px 16px; cursor: pointer; transition: all .15s; position: relative;
}
.mbn-icon { font-size: 20px; }
.mbn-label { font-size: 10px; color: var(--text2); font-weight: 500; }
.[Link] .mbn-label { color: var(--emerald); }
.mbn-badge {
position: absolute; top: 2px; right: 8px; background: var(--rose);
color: white; font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 99px;
}
/* ════════════════════════════════════════════
RESPONSIVE BREAKPOINTS
════════════════════════════════════════════ */
/* Tablets */
@media (max-width: 900px) {
:root { --sw: 0px; }
.sidebar { transform: translateX(-240px); --sw: 240px; }
.[Link]-open { transform: translateX(0); }
.main { margin-left: 0; }
.hamburger { display: flex; }
.search-topbar { width: 180px; }
.hero-stats { display: none; }
.products-grid { grid-template-columns: repeat(2,1fr); }
.cart-layout { grid-template-columns: 1fr; }
.cart-summary { position: static; }
.profile-layout { grid-template-columns: 1fr; }
.profile-sidebar-card { position: static; }
.order-stats-grid { grid-template-columns: repeat(2,1fr); }
.form-grid2 { grid-template-columns: 1fr; }
.shop-modal-hero { grid-template-columns: 1fr; }
.modal-products-grid { grid-template-columns: repeat(2,1fr); }
.notif-drawer { width: 100%; right: -100%; }
}
/* Mobile */
@media (max-width: 600px) {
.page-content { padding: 16px; }
.shops-grid { grid-template-columns: 1fr; }
.products-grid { grid-template-columns: repeat(2,1fr); }
9. [Link]
📄 File: [Link]
Lines of code: 444
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="RetailPro — Login and Register for Shopkeepers and Customers">
<title>RetailPro — Sign In / Register</title>
<!-- ════════════════════════════════════════════════════════
LEFT PANEL — Branding & Feature Highlights
════════════════════════════════════════════════════════ -->
<div class="left-panel">
<div class="left-content">
<!-- ════════════════════════════════════════════════════════
RIGHT PANEL — Auth Forms
════════════════════════════════════════════════════════ -->
<div class="right-panel">
<div class="auth-card" id="auth-card">
<!-- ══════════════════════════════════
LOGIN FORM
══════════════════════════════════ -->
<div class="form-panel active" id="panel-login" role="region" aria-label="Login">
<!-- ══════════════════════════════════
REGISTER FORM
══════════════════════════════════ -->
<div class="form-panel" id="panel-register" role="region" aria-label="Register">
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="RetailPro — Shopkeeper Dashboard"/>
<title>RetailPro — Shopkeeper Dashboard</title>
<!-- ════════════════════════════════════════════════════════
APP SHELL
════════════════════════════════════════════════════════ -->
<div id="app">
<!-- ════════════════════════════════════════════════════
SIDEBAR
════════════════════════════════════════════════════ -->
<aside id="sidebar" role="navigation" aria-label="Main navigation">
<div class="sb-section-label">Main</div>
<div class="sb-divider"></div>
<div class="sb-section-label">Finance</div>
<div class="sb-divider"></div>
<div class="sb-section-label">Insights</div>
<div class="sb-divider"></div>
<div class="sb-section-label">Shop</div>
<div class="sb-divider"></div>
</nav>
<!-- ════════════════════════════════════════════════════
MAIN AREA
════════════════════════════════════════════════════ -->
<div id="main">
<div class="notif-item">
<div class="notif-ic" style="background:var(--sky-dim)">💡</div>
<div class="notif-body">
<div class="notif-text">Tip: Basmati Rice sells 3× more on weekends!</div>
<div class="notif-time">This morning</div>
</div>
</div>
</div>
</div><!-- /notif wrap -->
<!-- ════════════════════════════════════════════════════
CONTENT
════════════════════════════════════════════════════ -->
<div id="content" role="main">
<!-- ══════════════════════════════════════════════════
DASHBOARD
══════════════════════════════════════════════════ -->
<section class="page-section active" id="page-dashboard" aria-label="Dashboard">
<div class="page-hd">
<div class="page-title">Good Morning, Rajan! 👋</div>
<div class="page-sub">Here's your store performance — <strong style="color:var(--
indigo-l)" id="dash-shop-name">Rajan General Stores</strong></div>
</div>
<div class="card">
<div class="section-row">
<div>
<div class="card-title">Revenue Overview</div>
<div class="card-sub" id="chart-subtitle">Loading…</div>
</div>
<div class="time-tabs">
<button class="time-tab active"
onclick="switchChart('weekly',this)">Weekly</button>
<button class="time-tab"
onclick="switchChart('monthly',this)">Monthly</button>
</div>
</div>
<div class="chart-wrap"><canvas id="revenueChart"></canvas></div>
</div>
<div class="card">
<div class="card-title mb8">🏆 Best Sellers</div>
<div class="card-sub mb16">Top performers this month</div>
<div id="best-sellers-list"></div>
</div>
</div>
<!-- ══════════════════════════════════════════════════
PRODUCTS
══════════════════════════════════════════════════ -->
<section class="page-section" id="page-products" aria-label="Products">
<div class="page-hd">
<div class="section-row">
<div>
<div class="page-title">Products</div>
<div class="page-sub" id="prod-count-sub">Loading…</div>
</div>
<button class="btn-primary" onclick="openProductDrawer(null)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Add Product
</button>
</div>
</div>
<div class="card">
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th></th><th>Product</th><th>Category</th>
<th>Price</th><th>Stock</th><th>Margin</th>
<th>Status</th><th>Actions</th>
</tr>
</thead>
<tbody id="prod-tbody"></tbody>
</table>
</div>
</div>
<!-- ══════════════════════════════════════════════════
INVENTORY
══════════════════════════════════════════════════ -->
<section class="page-section" id="page-inventory" aria-label="Inventory">
<div class="page-hd">
<div class="page-title">Inventory Tracking</div>
<div class="page-sub">Monitor stock levels and manage product availability</div>
</div>
<!-- ══════════════════════════════════════════════════
ORDERS / SALES
══════════════════════════════════════════════════ -->
<section class="page-section" id="page-orders" aria-label="Sales and Orders">
<div class="page-hd">
<div class="page-title">Sales & Orders</div>
<div class="page-sub">Track all transactions and revenue performance</div>
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>Invoice</th><th>Customer</th><th>Date</th>
<th>Items</th><th>Amount</th><th>Status</th><th>Actions</th>
</tr>
</thead>
<tbody id="orders-tbody"></tbody>
</table>
</div>
</div>
<!-- ══════════════════════════════════════════════════
INVOICES
══════════════════════════════════════════════════ -->
<section class="page-section" id="page-invoices" aria-label="Invoices">
<div class="page-hd">
<div class="section-row">
<div>
<div class="page-title">Invoices & Billing</div>
<div class="page-sub">Generate, manage and print professional invoices</div>
</div>
<div class="flex gap8">
<button class="btn-primary btn-sm" id="inv-tab-create"
onclick="switchInvTab('create')">+ Create Invoice</button>
<button class="btn-secondary btn-sm" id="inv-tab-history"
onclick="switchInvTab('history')">📋 History</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title mb4">New Invoice</div>
<div class="card-sub mb16" id="inv-number">Invoice #1043 · 28 March 2026</div>
<div class="form-group">
<label class="form-label" for="inv-customer">Customer Name / Phone</label>
<input class="form-input" id="inv-customer" placeholder="Walk-in or search
name…"
oninput="updateInvPreview()"/>
</div>
<div class="form-group">
<label class="form-label">Payment Mode</label>
<div class="pay-modes" id="pay-modes">
<button class="pay-btn active" onclick="setInvPayMode(this,'Cash')">💵
Cash</button>
<button class="pay-btn" onclick="setInvPayMode(this,'UPI')">📱
UPI</button>
<button class="pay-btn" onclick="setInvPayMode(this,'Card')">💳
Card</button>
<button class="pay-btn" onclick="setInvPayMode(this,'Credit')">🏦
Credit</button>
</div>
</div>
<hr class="inv-divider"/>
<div id="inv-prev-totals"></div>
<div class="inv-pay-chip" id="inv-prev-paymode">Payment: Cash</div>
<div style="margin-top:14px;font-size:11px;color:#94a3b8;text-
align:center">Thank you for shopping with us! 🙏</div>
</div>
</div>
</div>
</div><!-- /inv-create-panel -->
<tr><th>Invoice</th><th>Customer</th><th>Date</th><th>Amount</th><th>Status</
th><th>Actions</th></tr>
</thead>
<tbody id="inv-history-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════
ANALYTICS
══════════════════════════════════════════════════ -->
<section class="page-section" id="page-analytics" aria-label="Analytics">
<div class="page-hd">
<div class="page-title">Analytics & Reports</div>
<div class="page-sub">Deep insights into your business growth and trends</div>
</div>
<!-- ══════════════════════════════════════════════════
TALLY / ACCOUNTS
══════════════════════════════════════════════════ -->
<section class="page-section" id="page-tally" aria-label="Tally and Accounts">
<div class="page-hd">
<div class="page-title">Tally & Accounts</div>
<div class="page-sub">Financial ledger, income tracking and expense management</div>
</div>
<tr><th>Date</th><th>Description</th><th>Credit</th><th>Debit</th><th>Balance</th></tr>
</thead>
<tbody id="ledger-tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════
SETTINGS
<div class="page-hd">
<div class="page-title">Settings</div>
<div class="page-sub">Manage your shop profile, hours and preferences</div>
</div>
<div class="settings-panel">
<div class="settings-panel-title">Shop Information</div>
<div class="profile-photo-wrap">
<div class="profile-photo" id="s-user-initial">R</div>
<div>
<div class="profile-name" id="s-user-name">Rajan Kumar</div>
<div class="profile-role">Shopkeeper · RetailPro Pro</div>
<button class="profile-change-btn" onclick="showToast('📷 Upload feature
coming soon!','info')">Change Photo</button>
</div>
</div>
<div class="two-col">
<div class="form-group">
<label class="form-label" for="s-shop-name">Shop Name</label>
<input class="form-input" id="s-shop-name" placeholder="Your shop name"/>
</div>
<div class="form-group">
<label class="form-label" for="s-tagline">Tagline</label>
<input class="form-input" id="s-tagline" placeholder="Your tagline"/>
</div>
</div>
<div class="two-col">
<div class="form-group">
<label class="form-label" for="s-gst">GST Number</label>
<input class="form-input" id="s-gst" placeholder="22AAAAA0000A1Z5"/>
</div>
<div class="form-group">
<label class="form-label" for="s-phone">Phone</label>
<input class="form-input" id="s-phone" type="tel" placeholder="+91 98765
43210"/>
</div>
</div>
<div class="two-col">
<div class="form-group">
<label class="form-label" for="s-email">Email</label>
<input class="form-input" id="s-email" type="email"
placeholder="shop@[Link]"/>
</div>
<div class="form-group">
<label class="form-label" for="s-upi">UPI ID</label>
<input class="form-input" id="s-upi" placeholder="shop@upi"/>
</div>
</div>
</div>
<div class="settings-panel">
<div class="settings-panel-title">Account & Security</div>
<div class="form-group">
<label class="form-label" for="s-curr-pass">Current Password</label>
<input class="form-input" id="s-curr-pass" type="password"
placeholder="••••••••"/>
</div>
<div class="two-col">
<div class="form-group">
<label class="form-label" for="s-new-pass">New Password</label>
<div class="settings-panel">
<div class="settings-panel-title">Operating Hours</div>
<div id="op-hours-config"></div>
</div>
<div class="settings-panel">
<div class="settings-panel-title">Notification Preferences</div>
<div id="notif-prefs-list"></div>
</div>
</div>
</div><!-- /g2 -->
<!-- ══════════════════════════════════════════════════
LOCATION
══════════════════════════════════════════════════ -->
<section class="page-section" id="page-location" aria-label="Shop Location">
<div class="page-hd">
<div class="page-title">Shop Location</div>
<div class="page-sub">Set your location to help customers discover your shop on the
map</div>
</div>
<div class="g2">
<div class="card">
<div class="card-title mb4">Pin Your Location</div>
<div class="card-sub mb16">Drag the marker or search to set your exact shop
location</div>
<div class="form-group">
<label class="form-label" for="loc-address">Address</label>
<input class="form-input" id="loc-address" placeholder="Your shop address"/>
</div>
<div class="two-col mb16">
<div class="form-group" style="margin-bottom:0">
<label class="form-label" for="loc-lat">Latitude</label>
<input class="form-input" id="loc-lat" placeholder="13.0827"/>
</div>
<div class="form-group" style="margin-bottom:0">
<label class="form-label" for="loc-lng">Longitude</label>
<input class="form-input" id="loc-lng" placeholder="80.2707"/>
</div>
</div>
</div>
</section><!-- /location -->
<div class="drawer-hd">
<div>
<div class="drawer-title" id="drawer-title">Add New Product</div>
<div class="drawer-sub" id="drawer-sub">Fill in the details for the new product.</div>
</div>
<button class="drawer-close" onclick="closeDrawer()" aria-label="Close drawer">✕</button>
</div>
<div class="drawer-body">
<div class="drawer-ft">
<button class="btn-ghost flex1" onclick="closeDrawer()">Cancel</button>
<button class="btn-primary" style="flex:2;justify-content:center" onclick="saveProduct()">Save
Product</button>
</div>
<!-- ════════════════════════════════════════════════════════
CONFIRM MODAL (Delete)
════════════════════════════════════════════════════════ -->
<div id="confirm-modal" role="alertdialog" aria-labelledby="confirm-title" aria-modal="true">
<div class="confirm-box">
<div class="confirm-icon"></div>
<div class="confirm-title" id="confirm-title">Delete Product</div>
<p class="confirm-msg" id="confirm-msg">Are you sure? This cannot be undone.</p>
<div class="confirm-btns">
<button class="btn-ghost flex1" onclick="closeConfirm()">Cancel</button>
<button class="btn-danger flex1" onclick="doConfirm()">Yes, Delete</button>
</div>
</div>
</div>
<!-- ════════════════════════════════════════════════════════
MOBILE BOTTOM NAV
════════════════════════════════════════════════════════ -->
<nav id="mob-nav" aria-label="Mobile navigation">
<div class="mob-nav-inner">
<button class="mob-nav-item active" data-page="dashboard" onclick="navigate('dashboard')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-
linecap="round">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
</svg>
Home
</button>
<button class="mob-nav-item" data-page="products" onclick="navigate('products')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-
linecap="round">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/>
<circle cx="7" cy="7" r="1" fill="currentColor"/>
</svg>
Products
<!-- ════════════════════════════════════════════════════════
TOAST CONTAINER
════════════════════════════════════════════════════════ -->
<div class="toast-container" id="toasts" aria-live="polite" aria-atomic="true"></div>
<!-- ════════════════════════════════════════════════════════
SCRIPTS
════════════════════════════════════════════════════════ -->
<script src="../js/[Link]"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RetailPro — Customer Dashboard</title>
<!-- ═══════════════════════════════════════════════════
GOOGLE MAPS API KEY — ADD YOUR KEY HERE
Steps:
1. Go to [Link]
2. Create a project → Enable "Maps JavaScript API"
and "Places API"
3. Generate an API key
4. Replace YOUR_GOOGLE_MAPS_API_KEY below with it
5. Save and reload — the real map replaces the demo grid
═══════════════════════════════════════════════════ -->
<!-- <script src="[Link]
key=YOUR_GOOGLE_MAPS_API_KEY&libraries=places&callback=initGoogleMap" async defer></script> -->
</div>
</div>
<div class="sec-header">
<div><div class="sec-title">Shops Near You 📍</div><div class="sec-sub">Chengalpattu,
Tamil Nadu</div></div>
</div>
<!-- ═══════════════════════════════════════════════
MAP SECTION
— When Google Maps API key is added (see <head>),
the real interactive map loads in #map div.
— Without the key, the beautiful demo map shows.
═══════════════════════════════════════════════ -->
<div class="map-layout" style="margin-bottom:24px">
<div class="map-container">
<!-- Real Google Map renders here when API key is added -->
<div id="map" style="width:100%;height:100%;display:none"></div>
<div class="map-overlay-search">
<div class="map-search-box">
<span>🔍</span>
<input type="text" id="map-search" placeholder="Search on map…"
oninput="handleMapSearch([Link])">
</div>
</div>
<div class="map-controls">
<div class="map-btn" onclick="showToast('Zoom in','info')" title="Zoom in">+</div>
<div class="map-btn" onclick="showToast('Zoom out','info')" title="Zoom
out">−</div>
<div class="map-btn" onclick="showToast('Centering on your location…','info')"
title="My location">◎</div>
</div>
<div class="map-filter-bar">
<div class="map-chip active"
onclick="setMapFilter(this,'category','All')">All</div>
<div class="map-chip" onclick="setMapFilter(this,'category','Grocery')">🌾
Grocery</div>
<div class="map-chip" onclick="setMapFilter(this,'category','Electronics')">⚡
Electronics</div>
<div class="map-chip" onclick="setMapFilter(this,'category','Furniture')">🪑
Furniture</div>
<div class="map-chip" onclick="setMapFilter(this,'category','Appliances')">🌀
Appliances</div>
<div class="map-chip" onclick="toggleOpenFilter(this)">● Open Now</div>
</div>
</div>
<div class="shop-list-panel" id="shop-list-panel"></div>
</div>
<div class="sec-header">
<div><div class="sec-title">All Nearby Shops</div><div class="sec-sub" id="nearby-
count">Loading…</div></div>
</div>
<div class="shops-grid" id="nearby-shops-grid"></div>
</div>
</div>
</main>
<!-- ═══════════════════════════════════════════════════
SCRIPTS
[Link] handles all: auth guard, shops, map,
cart, wishlist, orders, profile, notifications.
═══════════════════════════════════════════════════ -->
<script src="../js/[Link]"></script>
<script>
/* ── Extra inline helpers ── */
/* ── Post-load init ── */
[Link]('load', () => {
const u = [Link]([Link]('rp_user') || '{}');
</body>
</html>
12. js / [Link]
📄 File: js/[Link]
Lines of code: 566
/* ============================================================
RetailPro — Auth Page JavaScript
File: js/[Link]
Covers:
- Tab switching (Login ↔ Register)
- Login role (Shopkeeper / Customer)
- Register role (Shopkeeper / Customer)
- Password visibility toggle
- Password strength meter
- Confirm-password live check
- Agree checkbox
- Form validation (login + register)
- Submit handlers (login + register)
- Toast notifications
- Session storage (localStorage)
- Auto-redirect if already logged in
- Role-based redirect after login/register
============================================================ */
'use strict';
/* ── State ───────────────────────────────────────────────── */
let loginRole = 'sk'; // 'sk' | 'cu'
let regRole = 'sk';
let agreedToTerms = true;
/* ════════════════════════════════════════════════════════════
SESSION (localStorage — no backend needed)
Key: 'rp_user'
Shape: { name, email, phone, role, store, address, loggedIn }
════════════════════════════════════════════════════════════ */
const Session = {
KEY: 'rp_user',
save(data) { [Link]([Link], [Link](data)); },
get() { try { return [Link]([Link]([Link])); } catch { return null; } },
clear() { [Link]([Link]); },
isLoggedIn() { const u = [Link](); return !!(u && [Link]); },
};
/* ════════════════════════════════════════════════════════════
TAB SWITCHER (Login ↔ Register)
════════════════════════════════════════════════════════════ */
function showTab(tab) {
const tLogin = $('tab-login');
[Link]('active-login', 'active-register');
[Link]('active-login', 'active-register');
/* ════════════════════════════════════════════════════════════
LOGIN ROLE SWITCHER
════════════════════════════════════════════════════════════ */
function setLoginRole(role) {
loginRole = role;
[Link]('active-sk', 'active-cu');
[Link]('active-sk', 'active-cu');
/* ════════════════════════════════════════════════════════════
REGISTER ROLE SWITCHER
════════════════════════════════════════════════════════════ */
function setRegRole(role) {
regRole = role;
[Link]('active-sk', 'active-cu');
[Link]('active-sk', 'active-cu');
if (!agreedToTerms) {
[Link] = 'agree-check';
[Link] = '';
}
}
/* ════════════════════════════════════════════════════════════
PASSWORD — toggle visibility
════════════════════════════════════════════════════════════ */
function togglePass(inputId, iconEl) {
const input = $(inputId);
if ([Link] === 'password') {
[Link] = 'text';
[Link] = '🙈';
} else {
/* ════════════════════════════════════════════════════════════
PASSWORD — strength meter
════════════════════════════════════════════════════════════ */
function checkStrength(val) {
const segs = ['seg1', 'seg2', 'seg3', 'seg4'];
const lbl = $('strength-lbl');
let score = 0;
if ([Link] >= 8) score++;
if (/[A-Z]/.test(val)) score++;
if (/[0-9]/.test(val)) score++;
if (/[^A-Za-z0-9]/.test(val)) score++;
const levels = [
{ cls: 'weak', txt: 'Weak 😬', color: 'var(--rose)' },
{ cls: 'ok', txt: 'Fair 🙂', color: 'var(--amber)' },
{ cls: 'good', txt: 'Good 👍', color: 'var(--sky)' },
{ cls: 'strong', txt: 'Strong 💪', color: 'var(--emerald)' },
];
/* ════════════════════════════════════════════════════════════
CONFIRM PASSWORD — live check
════════════════════════════════════════════════════════════ */
function initConfirmCheck() {
const confirmEl = $('reg-confirm');
if (!confirmEl) return;
[Link]('input', function () {
const pass = $('reg-pass').value;
const confirmOk = $('reg-confirm-ok');
const confirmErr = $('reg-confirm-err');
if (![Link]) {
[Link]('show');
[Link]('show');
return;
}
/* ════════════════════════════════════════════════════════════
AGREE CHECKBOX
/* ════════════════════════════════════════════════════════════
VALIDATION HELPERS
════════════════════════════════════════════════════════════ */
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function validatePhone(phone) {
return /^[\d\s\+\-]{7,15}$/.test([Link]());
}
if (show) {
[Link]('show');
if (input) [Link]('error');
} else {
[Link]('show');
if (input) [Link]('error');
}
}
/* ════════════════════════════════════════════════════════════
HANDLE LOGIN SUBMIT
Saves session → redirects to the correct dashboard
════════════════════════════════════════════════════════════ */
function handleLogin() {
const email = $('login-email').[Link]();
const pass = $('login-pass').value;
let valid = true;
if (!validateEmail(email)) {
showFieldError('login-email-err', true);
valid = false;
} else {
showFieldError('login-email-err', false);
}
if ([Link] < 6) {
showFieldError('login-pass-err', true);
valid = false;
} else {
showFieldError('login-pass-err', false);
}
if (!valid) return;
[Link] = false;
[Link] = loginRole === 'sk'
? 'Sign In as Shopkeeper →'
: 'Sign In as Customer →';
showToast(
loginRole === 'sk'
? '✅ Welcome back, Shopkeeper! Redirecting…'
: '✅ Welcome back! Redirecting to your portal…',
'success'
);
/* Role-based redirect */
setTimeout(() => {
[Link] = loginRole === 'sk'
? 'pages/[Link]'
: 'pages/[Link]';
}, 1200);
})
.catch(err => {
[Link] = false;
[Link] = loginRole === 'sk'
? 'Sign In as Shopkeeper →'
: 'Sign In as Customer →';
showToast(`❌ ${[Link]}`, 'error');
});
}
/* ════════════════════════════════════════════════════════════
HANDLE REGISTER SUBMIT
Saves session → redirects to the correct dashboard
════════════════════════════════════════════════════════════ */
function handleRegister() {
const name = $('reg-name').[Link]();
const phone = $('reg-phone').[Link]();
const email = $('reg-email').[Link]();
const pass = $('reg-pass').value;
const confirm = $('reg-confirm').value;
const store = $('reg-store').[Link]();
const address = $('reg-address').[Link]();
let valid = true;
if ([Link] < 2) {
showFieldError('reg-name-err', true); valid = false;
} else { showFieldError('reg-name-err', false); }
if (!validatePhone(phone)) {
showFieldError('reg-phone-err', true); valid = false;
} else { showFieldError('reg-phone-err', false); }
if (!validateEmail(email)) {
showFieldError('reg-email-err', true); valid = false;
} else { showFieldError('reg-email-err', false); }
if ([Link] < 8) {
showFieldError('reg-pass-err', true); valid = false;
} else { showFieldError('reg-pass-err', false); }
if (!agreedToTerms) {
showToast('Please agree to the Terms of Service to continue.', 'error');
valid = false;
}
if (!valid) return;
[Link] = false;
[Link] = regRole === 'sk'
? 'Create Shopkeeper Account →'
: 'Create Customer Account →';
/* Role-based redirect */
setTimeout(() => {
[Link] = regRole === 'sk'
? 'pages/[Link]'
: 'pages/[Link]';
}, 1400);
})
.catch(err => {
[Link] = false;
[Link] = regRole === 'sk'
? 'Create Shopkeeper Account →'
: 'Create Customer Account →';
showToast(`❌ ${[Link]}`, 'error');
});
}
/* ════════════════════════════════════════════════════════════
TOAST NOTIFICATIONS
════════════════════════════════════════════════════════════ */
function showToast(msg, type = 'info') {
const container = $('toasts');
if (!container) return;
const colors = {
success: 'var(--emerald)',
error: 'var(--rose)',
info: 'var(--sky)',
warn: 'var(--amber)',
};
const icons = {
success: '✅',
error: '❌',
info: '💡',
warn: '⚠️',
};
[Link](toast);
setTimeout(() => {
[Link]('out');
setTimeout(() => [Link](), 350);
}, 4000);
}
/* ════════════════════════════════════════════════════════════
INITIALISE (runs once DOM is ready)
════════════════════════════════════════════════════════════ */
[Link]('DOMContentLoaded', () => {
setLoginRole('sk');
setRegRole('sk');
initConfirmCheck();
13. js / [Link]
📄 File: js/[Link]
Lines of code: 1314
/* ============================================================
RetailPro — Shopkeeper Dashboard JavaScript
File: js/[Link]
Covers:
- Session guard (redirect if not logged in as sk)
- Data layer (localStorage-backed store)
- Sidebar (collapse, mobile open/close)
- Navigation (page switching)
- Global search (live filter)
- Notifications (panel open/close, mark all read)
- Dashboard (KPI counters, charts, best-sellers, recent txns)
- Products (CRUD, search, filter, drawer form, validation)
- Inventory (stock health, restock modal)
- Orders/Sales (table, filter by status)
- Analytics (charts, category breakdown)
- Tally/Accounts (ledger, add expense)
- Invoices (create, line items, live preview, history)
- Settings (profile update, operating hours, notifications)
- Location (map placeholder interactions)
- Toast (typed notifications)
- Confirm modal (delete confirmation)
- Logout
- Charts ([Link] wrappers)
- Responsive helpers
============================================================ */
'use strict';
/* ════════════════════════════════════════════════════════
HELPERS
════════════════════════════════════════════════════════ */
const $ = id => [Link](id);
const $$ = sel => [Link](sel);
const fmt = n => '₹' + [Link](n).toLocaleString('en-IN');
const uid = () => [Link]().toString(36).slice(2, 10);
const now = () => new Date().toLocaleDateString('en-IN', { day:'numeric', month:'short',
year:'numeric' });
/* ════════════════════════════════════════════════════════
SESSION (mirrors [Link] Session object)
════════════════════════════════════════════════════════ */
const Session = {
KEY: 'rp_user',
get() { try { return [Link]([Link]([Link])); } catch { return null; } },
clear() { [Link]([Link]); },
isLoggedIn() { const u = [Link](); return !!(u && [Link]); },
};
/* ════════════════════════════════════════════════════════
DATA STORE (localStorage-backed)
defaults() {
const user = [Link]();
return {
shop: {
name: user?.store || 'Rajan General Stores',
tagline: 'Your Neighbourhood Store',
gst: '22AAAAA0000A1Z5',
phone: '+91 98765 43210',
email: user?.email || 'shop@[Link]',
upi: 'rajan@upi',
address: 'No.12, Gandhi Nagar, Chennai – 600020',
lat: '13.0827',
lng: '80.2707',
hours: { 'Mon–Sat': { open:'08:00', close:'21:00', active:true }, Sunday:
{ open:'09:00', close:'18:00', active:false } },
notifs: { lowStock:true, newOrder:true, weeklyReport:true, tips:false },
},
products: [
{ id:'p1', name:'Basmati Rice 5kg', cat:'Grains', price:320, cost:240, stock:83,
threshold:20, status:'active', sku:'GR-001', emoji:'🌾' },
{ id:'p2', name:'Sunflower Oil 1L', cat:'Oils', price:158, cost:120, stock:9,
threshold:15, status:'low', sku:'OL-002', emoji:'🫒' },
{ id:'p3', name:'Toor Dal 1kg', cat:'Pulses', price:148, cost:110, stock:0,
threshold:10, status:'out', sku:'PL-003', emoji:'🫘' },
{ id:'p4', name:'Aashirvaad Atta 5kg', cat:'Grains', price:275, cost:210, stock:45,
threshold:15, status:'active', sku:'GR-004', emoji:'🌾' },
{ id:'p5', name:'Salt 1kg', cat:'Spices', price:28, cost:18, stock:4,
threshold:20, status:'low', sku:'SP-005', emoji:'🧂' },
{ id:'p6', name:'Sugar 1kg', cat:'Essentials',price:45, cost:32, stock:6,
threshold:15, status:'low', sku:'ES-006', emoji:'🍚' },
{ id:'p7', name:'Maggi Noodles 70g', cat:'Packaged', price:14, cost:10, stock:210,
threshold:50, status:'active', sku:'PK-007', emoji:'🍜' },
{ id:'p8', name:"Parle-G Biscuits", cat:'Packaged', price:10, cost:7, stock:340,
threshold:100,status:'active', sku:'PK-008', emoji:'🍪' },
],
orders: [
{ id:'#1042', customer:'Mohan Kumar', date:'27 Mar 2026', items:3, amount:1200,
status:'paid', payMode:'Cash' },
{ id:'#1041', customer:'Walk-in', date:'27 Mar 2026', items:1, amount:840,
status:'paid', payMode:'UPI' },
{ id:'#1040', customer:'Priya Sharma', date:'27 Mar 2026', items:5, amount:2100,
status:'paid', payMode:'Cash' },
{ id:'#1039', customer:'Raj Patel', date:'26 Mar 2026', items:2, amount:476,
status:'paid', payMode:'Card' },
{ id:'#1038', customer:'Walk-in', date:'26 Mar 2026', items:4, amount:1380,
status:'paid', payMode:'Cash' },
{ id:'#1037', customer:'Suresh V.', date:'26 Mar 2026', items:2, amount:616,
status:'pending', payMode:'Credit'},
{ id:'#1036', customer:'Anita Devi', date:'25 Mar 2026', items:1, amount:158,
status:'paid', payMode:'UPI' },
{ id:'#1035', customer:'Walk-in', date:'25 Mar 2026', items:6, amount:2780,
status:'paid', payMode:'Cash' },
],
ledger: [
{ id:uid(), date:'27 Mar', desc:'Invoice #1042 — Mohan Kumar', credit:1200,
debit:null },
{ id:uid(), date:'27 Mar', desc:'Invoice #1041 — Walk-in', credit:840,
debit:null },
{ id:uid(), date:'27 Mar', desc:'Electricity Bill', credit:null,
debit:800 },
{ id:uid(), date:'26 Mar', desc:'Invoice #1040 — Priya Sharma', credit:2100,
debit:null },
{ id:uid(), date:'26 Mar', desc:'Staff Salary — Advance', credit:null,
debit:5000 },
{ id:uid(), date:'25 Mar', desc:'Invoice #1038 — Walk-in', credit:1380,
debit:null },
load() {
try {
const raw = [Link]([Link]);
return raw ? [Link](raw) : [Link]();
} catch { return [Link](); }
},
save(data) {
[Link]([Link], [Link](data));
},
};
/* Working copy */
let state = [Link]();
/* ════════════════════════════════════════════════════════
WEEKLY / MONTHLY CHART DATA
════════════════════════════════════════════════════════ */
const weeklyData = [
{ label:'Mon', val:12400, orders:31 },
{ label:'Tue', val:18200, orders:46 },
{ label:'Wed', val:15800, orders:40 },
{ label:'Thu', val:22100, orders:56 },
{ label:'Fri', val:19600, orders:49 },
{ label:'Sat', val:28900, orders:73 },
{ label:'Sun', val:14280, orders:38 },
];
const monthlyData = [
{ label:'Jan', val:242000 }, { label:'Feb', val:218000 }, { label:'Mar', val:284000 },
{ label:'Apr', val:196000 }, { label:'May', val:310000 }, { label:'Jun', val:289000 },
{ label:'Jul', val:334000 }, { label:'Aug', val:298000 }, { label:'Sep', val:267000 },
{ label:'Oct', val:312000 }, { label:'Nov', val:356000 }, { label:'Dec', val:421000 },
];
/* ════════════════════════════════════════════════════════
TOAST (reuses [Link] pattern)
════════════════════════════════════════════════════════ */
function showToast(msg, type = 'info') {
const container = $('toasts');
const colors = { success:'var(--emerald)', error:'var(--rose)', info:'var(--sky)', warn:'var(--
amber)' };
⚠️
const icons = { success:'', error:'', info:'💡', warn:'⚠️' };
const el = [Link]('div');
[Link] = 'toast';
[Link] = `3px solid ${colors[type] || [Link]}`;
[Link] = `<span class="toast-icon">${icons[type] || [Link]}</span><span class="toast-
msg">${msg}</span>`;
[Link](el);
/* ════════════════════════════════════════════════════════
CONFIRM MODAL
════════════════════════════════════════════════════════ */
let _confirmCb = null;
/* ════════════════════════════════════════════════════════
SIDEBAR
════════════════════════════════════════════════════════ */
let sidebarCollapsed = false;
function toggleSidebar() {
sidebarCollapsed = !sidebarCollapsed;
const sb = $('sidebar');
[Link]('collapsed', sidebarCollapsed);
}
function openMobSidebar() {
$('sidebar').[Link]('mob-open');
$('sb-overlay').[Link]('open');
}
function closeMobSidebar() {
$('sidebar').[Link]('mob-open');
$('sb-overlay').[Link]('open');
}
/* ════════════════════════════════════════════════════════
PAGE NAVIGATION
════════════════════════════════════════════════════════ */
let currentPage = 'dashboard';
function navigate(page) {
if (currentPage === page) return;
currentPage = page;
function renderPage(page) {
const map = {
dashboard : renderDashboard,
products : renderProducts,
inventory : renderInventory,
orders : renderOrders,
analytics : renderAnalytics,
tally : renderTally,
invoices : renderInvoices,
settings : renderSettings,
location : renderLocation,
};
if (map[page]) map[page]();
}
/* ════════════════════════════════════════════════════════
GLOBAL SEARCH
════════════════════════════════════════════════════════ */
function handleGlobalSearch(q) {
if (![Link]()) return;
const lower = [Link]();
/* ════════════════════════════════════════════════════════
NOTIFICATIONS PANEL
════════════════════════════════════════════════════════ */
function toggleNotifPanel() {
/* ════════════════════════════════════════════════════════
DASHBOARD
════════════════════════════════════════════════════════ */
function renderDashboard() {
/* KPIs */
const totalStock = [Link]((s, p) => s + [Link], 0);
const lowCount = [Link](p => [Link] === 'low' || [Link] === 'out').length;
animateNum($('kpi-revenue'), 14280, '₹');
animateNum($('kpi-stock'), totalStock);
animateNum($('kpi-orders'), 38);
animateNum($('kpi-lowstock'), lowCount);
/* Best sellers */
const bsData = [
{ name:"Parle-G Biscuits", sold:520, revenue:5200, pct:100 },
{ name:'Basmati Rice 5kg', sold:340, revenue:28400, pct:65 },
{ name:'Maggi Noodles 70g', sold:410, revenue:5740, pct:79 },
{ name:'Sunflower Oil 1L', sold:240, revenue:19200, pct:46 },
{ name:'Aashirvaad Atta 5kg', sold:180, revenue:24750, pct:35 },
];
const colors = ['var(--indigo)', 'var(--emerald)', 'var(--sky)', 'var(--amber)', 'var(--indigo-
l)'];
const bsEl = $('best-sellers-list');
if (bsEl) {
[Link] = [Link]((b, i) => `
<div class="bs-row">
<div class="bs-label">
<span><span class="bs-rank">#${i+1}</span><span
class="bs-name">${[Link]}</span></span>
<span class="bs-sold">${[Link]} sold</span>
</div>
<div class="bs-bar"><div class="bs-fill" style="width:${[Link]}%;background:$
{colors[i]}"></div></div>
<div class="bs-revenue">${fmt([Link])}</div>
</div>`).join('');
}
/* Recent transactions */
const tbody = $('dash-txn-tbody');
if (tbody) {
[Link] = [Link](0, 5).map(o => `
<tr>
<td class="td-mono">${[Link]}</td>
<td class="td-bold">${[Link]}</td>
<td class="td-muted">${[Link]}</td>
<td class="td-muted">${[Link]} item${[Link] > 1 ? 's' : ''}</td>
<td class="td-bold">${fmt([Link])}</td>
<td>${statusBadge([Link])}</td>
<td>
<div class="tbl-actions">
/* Chart subtitle */
const sub = $('chart-subtitle');
if (sub) [Link] = chartMode === 'weekly'
? '₹1,41,300 this week · ' + [Link]((s,d)=>s+[Link],0) + ' orders'
: '₹33,27,000 this year';
buildRevenueChart();
}
/* Animated counter */
function animateNum(el, target, prefix = '', suffix = '') {
if (!el) return;
let start = 0, dur = 900, step = 14;
const inc = target / (dur / step);
const t = setInterval(() => {
start += inc;
if (start >= target) {
[Link] = prefix + [Link](target).toLocaleString('en-IN') + suffix;
clearInterval(t);
} else {
[Link] = prefix + [Link](start).toLocaleString('en-IN') + suffix;
}
}, step);
}
/* ════════════════════════════════════════════════════════
[Link] WRAPPERS
════════════════════════════════════════════════════════ */
const CHART_DEFAULTS = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#161B22',
borderColor: '#30363D',
borderWidth: 1,
titleColor: '#8B949E',
bodyColor: '#F0F6FC',
padding: 10,
},
},
scales: {
x: {
grid: { color: 'rgba(48,54,61,0.5)', drawBorder: false },
ticks: { color: '#484F58', font: { size: 11, family: "'Sora', sans-serif" } },
},
y: {
grid: { color: 'rgba(48,54,61,0.5)', drawBorder: false },
ticks: {
color: '#484F58',
font: { size: 11, family: "'Sora', sans-serif" },
callback: v => '₹' + (v >= 1000 ? [Link](v/1000) + 'k' : v),
},
},
},
};
function buildRevenueChart() {
const canvas = $('revenueChart');
if (!canvas) return;
if (revenueChart) { [Link](); revenueChart = null; }
function buildAnalyticsChart() {
const canvas = $('analyticsChart');
if (!canvas) return;
if (analyticsChart) { [Link](); analyticsChart = null; }
/* ════════════════════════════════════════════════════════
STATUS BADGE HELPER
════════════════════════════════════════════════════════ */
function statusBadge(s) {
const map = {
active: ['badge-green', 'Active'],
low: ['badge-amber', 'Low Stock'],
out: ['badge-red', 'Out of Stock'],
paid: ['badge-green', 'Paid'],
pending: ['badge-amber', 'Pending'],
/* ════════════════════════════════════════════════════════
PRODUCTS
════════════════════════════════════════════════════════ */
let prodFilter = 'all';
let editingProdId = null;
function renderProducts() {
updateProdFilterCounts();
renderProductTable(filteredProds());
}
function filteredProds() {
if (prodFilter === 'all') return [Link];
return [Link](p => [Link] === prodFilter);
}
function updateProdFilterCounts() {
const all = [Link];
const active = [Link](p => [Link] === 'active').length;
const low = [Link](p => [Link] === 'low').length;
const out = [Link](p => [Link] === 'out').length;
const cnt = id => { const el = $(id); if (el) [Link] = [Link] === 'all' ? all :
[Link] === 'active' ? active : [Link] === 'low' ? low : out; };
['cnt-all','cnt-active','cnt-low','cnt-out'].forEach(cnt);
$('prod-count-sub').textContent = `${all} total products in your store`;
}
function renderProductTable(list) {
const tbody = $('prod-tbody');
if (!tbody) return;
if (![Link]) {
[Link] = `<tr><td colspan="9">
<div class="empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">No products found</div>
<div class="empty-sub">Try a different filter or add a new product.</div>
<button class="btn-primary btn-sm" onclick="openProductDrawer(null)">+ Add
Product</button>
</div>
</td></tr>`;
return;
}
/* ── Product Drawer ── */
function openProductDrawer(id) {
editingProdId = id;
const p = id ? [Link](x => [Link] === id) : null;
$('modal-overlay').[Link]('open');
$('drawer').[Link]('open');
}
function closeDrawer() {
$('modal-overlay').[Link]('open');
$('drawer').[Link]('open');
editingProdId = null;
}
function calcMargin() {
const price = parseFloat($('d-price')?.value) || 0;
const cost = parseFloat($('d-cost')?.value) || 0;
const el = $('margin-display');
if (!el) return;
if (price && cost) {
const m = [Link](((price - cost) / price) * 100);
const cls = m >= 25 ? 'good' : m >= 15 ? 'ok' : 'low';
function clearFormErrors() {
$$('.[Link]').forEach(e => [Link]('show'));
$$('.[Link]').forEach(e => [Link]('error'));
}
function showFieldErr(errId) {
const el = $(errId);
if (!el) return;
[Link]('show');
[Link]('.form-group')?.querySelector('.form-input')?.[Link]('error');
}
function saveProduct() {
const name = $('d-name').[Link]();
const price = parseFloat($('d-price').value) || 0;
const cost = parseFloat($('d-cost').value) || 0;
const stock = parseInt($('d-stock').value) || 0;
const thr = parseInt($('d-threshold').value)|| 10;
const cat = $('d-cat').value;
const sku = $('d-sku').[Link]() || 'PRD-' + [Link]().toString().slice(-6);
clearFormErrors();
let valid = true;
if (!name) { showFieldErr('d-name-err'); valid = false; }
if (!price || price < 1) { showFieldErr('d-price-err'); valid = false; }
if (!valid) return;
if (editingProdId) {
const idx = [Link](p => [Link] === editingProdId);
if (idx > -1) [Link][idx] = { ...[Link][idx], name, price, cost, stock:
stock_val, threshold: thr, cat, sku, status, emoji };
showToast('✅ Product updated successfully!', 'success');
} else {
[Link]({ id: 'p' + uid(), name, price, cost, stock: stock_val, threshold: thr, cat,
sku, status, emoji });
showToast('✅ Product added successfully!', 'success');
}
persist();
closeDrawer();
renderProducts();
updateInventoryBadge();
}
function confirmDeleteProduct(id) {
const p = [Link](x => [Link] === id);
if (!p) return;
openConfirm('Delete Product', `Are you sure you want to delete "${[Link]}"? This action cannot be
undone.`, () => {
[Link] = [Link](x => [Link] !== id);
persist();
renderProducts();
showToast(' Product deleted.', 'warn');
});
}
/* ════════════════════════════════════════════════════════
INVENTORY
════════════════════════════════════════════════════════ */
function renderInventory() {
const total = [Link];
const well = [Link](p => [Link] === 'active').length;
const low = [Link](p => [Link] === 'low').length;
const out = [Link](p => [Link] === 'out').length;
/* Low-stock alerts */
const alerts = [Link](p => [Link] === 'low' || [Link] === 'out');
const alertEl = $('low-stock-list');
if (alertEl) {
[Link] = [Link]
? [Link](p => `
<div class="inv-health-row">
<div class="inv-emoji">${[Link]}</div>
<div class="inv-info">
<div class="inv-name">${[Link]}</div>
<div class="inv-meta">SKU: ${[Link]} · Threshold: ${[Link]} units</div>
<div class="prog-bar-lg" style="margin-top:7px;max-width:180px">
<div class="prog-fill"
style="width:${[Link](100,[Link]([Link]/[Link]*100))}%;background:$
{[Link]===0?'var(--rose)':'var(--amber)'}"></div>
</div>
</div>
<div class="inv-stock">
<div class="inv-qty" style="color:${[Link]===0?'var(--rose)':'var(--amber)'}">$
{[Link]}</div>
<div class="inv-qty-lbl">units left</div>
</div>
${statusBadge([Link])}
<button class="btn-primary btn-sm" onclick="restockProduct('${[Link]}')">+
Restock</button>
</div>`).join('')
: `<div class="empty-state" style="padding:28px">
<div class="empty-icon">✅</div>
<div class="empty-title">All products well stocked</div>
</div>`;
}
function restockProduct(id) {
const p = [Link](x => [Link] === id);
if (!p) return;
const qty = parseInt(prompt(`Restock "${[Link]}"\nEnter quantity to add:`, 50));
if (isNaN(qty) || qty <= 0) return;
[Link] += qty;
[Link] = computeStatus(p);
persist();
renderInventory();
updateInventoryBadge();
showToast(`✅ Restocked ${[Link]} +${qty} units`, 'success');
}
/* ════════════════════════════════════════════════════════
ORDERS / SALES
════════════════════════════════════════════════════════ */
let orderFilter = 'all';
function renderOrders() {
const tbody = $('orders-tbody');
if (!tbody) return;
if (![Link]) {
[Link] = `<tr><td colspan="7"><div class="empty-state"><div
class="empty-icon">🧾</div><div class="empty-title">No orders found</div></div></td></tr>`;
return;
}
/* ════════════════════════════════════════════════════════
ANALYTICS
════════════════════════════════════════════════════════ */
function renderAnalytics() {
buildAnalyticsChart();
const cats = [
['Grains', 38, 'var(--indigo)'],
['Packaged', 25, 'var(--sky)'],
['Oils', 18, 'var(--emerald)'],
['Pulses', 12, 'var(--amber)'],
['Others', 7, 'var(--text3)'],
];
const el = $('cat-breakdown');
if (el) {
[Link] = [Link](([l, pct, c]) => `
<div class="cat-row">
<div class="cat-label-row">
<span style="font-size:13px;color:var(--text1)">${l}</span>
<span style="font-size:13px;font-weight:700;color:${c}">${pct}%</span>
</div>
<div class="cat-bar">
<div class="cat-fill" style="width:${pct}%;background:${c}"></div>
</div>
</div>`).join('');
}
}
/* ════════════════════════════════════════════════════════
TALLY / ACCOUNTS
════════════════════════════════════════════════════════ */
function renderTally() {
const total = [Link]((s, l) => s + ([Link] || 0) - ([Link] || 0), 0);
/* Ledger table */
const tbody = $('ledger-tbody');
if (tbody) {
let running = 0;
[Link] = [Link](l => {
running += ([Link] || 0) - ([Link] || 0);
return `
<tr>
<td class="td-muted">${[Link]}</td>
<td style="font-size:13px">${[Link]}</td>
<td class="ledger-credit">${[Link] ? fmt([Link]) : '—'}</td>
<td class="ledger-debit">${[Link] ? fmt([Link]) : '—'}</td>
<td style="font-weight:700;color:${running>=0?'var(--emerald)':'var(--rose)'}; font-
family:var(--mono)">${fmt([Link](running))}</td>
</tr>`;
}).join('');
}
/* Expense breakdown */
const expenseData = [
function addExpense() {
const desc = $('exp-desc').[Link]();
const amount = parseFloat($('exp-amount').value);
const cat = $('exp-cat').value;
[Link]({
id: uid(),
date: now(),
desc: `${cat} — ${desc}`,
credit: null,
debit: amount,
});
persist();
renderTally();
$('exp-desc').value = '';
$('exp-amount').value = '';
showToast(`✅ Expense recorded: ${fmt(amount)}`, 'success');
}
/* ════════════════════════════════════════════════════════
INVOICES
════════════════════════════════════════════════════════ */
let invItems = [{ productId: [Link][0]?.id || 'p1', qty: 1 }];
let invPayMode = 'Cash';
let invTab = 'create';
function renderInvoices() {
renderInvItems();
updateInvPreview();
renderInvHistory();
}
function switchInvTab(tab) {
invTab = tab;
$('inv-create-panel').[Link] = tab === 'create' ? 'block' : 'none';
$('inv-history-panel').[Link] = tab === 'history' ? 'block' : 'none';
$('inv-tab-create').className = tab === 'create' ? 'btn-primary btn-sm' : 'btn-secondary btn-
sm';
$('inv-tab-history').className = tab === 'history' ? 'btn-primary btn-sm' : 'btn-secondary btn-
sm';
}
function renderInvItems() {
const tbody = $('inv-items-tbody');
if (!tbody) return;
function updateInvPreview() {
const subtotal = [Link]((s, it) => {
const p = [Link](x => [Link] === [Link]) || [Link][0];
return s + (p?.price || 0) * [Link];
}, 0);
const gstPct = parseFloat($('inv-gst')?.value || 5);
const discPct = parseFloat($('inv-discount')?.value || 0);
const gst = [Link](subtotal * gstPct / 100);
const disc = [Link](subtotal * discPct / 100);
const total = subtotal + gst - disc;
const cust = $('inv-customer')?.value || 'Walk-in Customer';
const pt = $('inv-prev-tbody');
if (pt) {
[Link] = [Link](it => {
const p = [Link](x => [Link] === [Link]) || [Link][0];
return `<tr>
<td style="padding:4px 0;font-size:12px">${p?.emoji} ${p?.name}</td>
<td style="text-align:center;color:#64748b;font-size:12px">×${[Link]}</td>
<td style="text-align:right;font-size:12.5px;font-weight:700;color:#1e293b">$
{fmt((p?.price||0)*[Link])}</td>
</tr>`;
}).join('');
}
function generateInvoice() {
const cust = $('inv-customer')?.value?.trim() || 'Walk-in';
if (![Link]) { showToast('⚠️ Add at least one product.', 'warn'); return; }
function renderInvHistory() {
const tbody = $('inv-history-tbody');
if (!tbody) return;
[Link] = [Link](o => `
<tr>
<td class="td-mono">${[Link]}</td>
<td class="td-bold">${[Link]}</td>
<td class="td-muted">${[Link]}</td>
<td class="td-bold">${fmt([Link])}</td>
<td>${statusBadge([Link])}</td>
<td>
<div class="tbl-actions">
<button class="act-btn view" onclick="showToast('Viewing ${[Link]}','info')"
title="View">👁</button>
<button class="act-btn print" onclick="showToast('Printing ${[Link]}…','info')"
title="Print"></button>
</div>
</td>
</tr>`).join('');
}
/* ════════════════════════════════════════════════════════
SETTINGS
════════════════════════════════════════════════════════ */
function renderSettings() {
const s = [Link];
/* Populate inputs */
/* User info */
const user = [Link]();
const nameEl = $('s-user-name');
if (nameEl) [Link] = user?.name || [Link];
const initEl = $('s-user-initial');
if (initEl) [Link] = (user?.name || [Link] || 'R')[0].toUpperCase();
/* Operating hours */
const hoursEl = $('op-hours-config');
if (hoursEl) {
[Link] = [Link]([Link]).map(([day, v]) => `
<div class="op-hours-row">
<span class="op-day-label">${day}</span>
<input type="time" class="form-input" value="${[Link]}" style="width:110px;padding:7px
9px"
onchange="updateHours('${day}','open',[Link])"/>
<span style="font-size:12px;color:var(--text3)">to</span>
<input type="time" class="form-input" value="${[Link]}" style="width:110px;padding:7px
9px"
onchange="updateHours('${day}','close',[Link])"/>
<button class="toggle-switch ${[Link] ? 'on' : ''}"
id="toggle-${[Link](/[–\s]/g,'_')}"
onclick="toggleHours('${day}')"></button>
</div>`).join('');
}
/* Notification prefs */
const prefs = [
['lowStock', 'Low Stock Alerts', [Link]],
['newOrder', 'New Order Notifications', [Link]],
['weeklyReport', 'Weekly Revenue Report', [Link]],
['tips', 'Smart Sales Tips', [Link]],
];
const nEl = $('notif-prefs-list');
if (nEl) {
[Link] = [Link](([k, label, on]) => `
<div class="notif-pref-row">
<span class="notif-pref-label">${label}</span>
<button class="toggle-switch ${on ? 'on' : ''}" id="pref-${k}"
onclick="toggleNotifPref('${k}')"></button>
</div>`).join('');
}
}
function saveSettings() {
[Link] = $('s-shop-name')?.value?.trim() || [Link];
[Link] = $('s-tagline')?.value?.trim() || [Link];
[Link] = $('s-gst')?.value?.trim() || [Link];
[Link] = $('s-phone')?.value?.trim() || [Link];
[Link] = $('s-email')?.value?.trim() || [Link];
[Link] = $('s-upi')?.value?.trim() || [Link];
persist();
showToast('✅ Settings saved successfully!', 'success');
}
/* ════════════════════════════════════════════════════════
LOCATION
════════════════════════════════════════════════════════ */
function renderLocation() {
const s = [Link];
const set = (id, val) => { const el = $(id); if (el) [Link] = val || ''; };
set('loc-address', [Link]);
set('loc-lat', [Link]);
set('loc-lng', [Link]);
}
function saveLocation() {
[Link] = $('loc-address')?.value?.trim() || [Link];
[Link] = $('loc-lat')?.value?.trim() || [Link];
[Link] = $('loc-lng')?.value?.trim() || [Link];
persist();
showToast('📍 Shop location saved!', 'success');
}
/* ════════════════════════════════════════════════════════
LOGOUT
════════════════════════════════════════════════════════ */
function logout() {
openConfirm('Sign Out', 'Are you sure you want to sign out?', () => {
[Link]();
showToast('👋 Signed out. Redirecting…', 'info');
setTimeout(() => { [Link] = '../[Link]'; }, 1200);
});
}
/* ════════════════════════════════════════════════════════
RESPONSIVE HELPER
════════════════════════════════════════════════════════ */
function handleResize() {
const mobMenuBtn = $('mob-menu-btn');
if (!mobMenuBtn) return;
if ([Link] <= 900) {
[Link] = 'flex';
/* If sidebar was desktop-collapsed, reset for mobile */
if (!sidebarCollapsed) {
$('sidebar').[Link]('collapsed');
}
} else {
[Link] = 'none';
$('sidebar').[Link]('mob-open');
$('sb-overlay').[Link]('open');
}
}
[Link]('resize', handleResize);
/* ════════════════════════════════════════════════════════
INITIALISE
════════════════════════════════════════════════════════ */
[Link]('DOMContentLoaded', () => {
14. js / [Link]
📄 File: js/[Link]
Lines of code: 1217
/* ============================================================
RetailPro — Customer Dashboard JavaScript
File: RetailPro/js/[Link]
All features: auth guard, map, shops, cart, wishlist,
compare, notifications, orders, profile, localStorage state.
============================================================ */
'use strict';
/* ═══════════════════════════════════════════════════════════
1. SESSION & AUTH GUARD
═══════════════════════════════════════════════════════════ */
const Session = {
KEY: 'rp_user',
get() { try { return [Link]([Link]([Link])); } catch { return null; } },
clear(){ [Link]([Link]); }
};
function initAuth() {
const u = [Link]();
if (!u || ![Link] || [Link] !== 'cu') {
[Link] = '../[Link]';
return false;
}
currentUser = u;
// Populate UI with user data
const init = ([Link] || [Link] || 'C')[0].toUpperCase();
qs('#cu-avatar').textContent = init;
qs('#cu-name').textContent = [Link] || 'Customer';
qs('#cu-email').textContent = [Link] || '';
qs('#profile-name').textContent = [Link] || 'Customer';
qs('#profile-email').textContent = [Link] || '';
qs('#edit-name').value = [Link] || '';
qs('#edit-email').value = [Link] || '';
qs('#edit-phone').value = [Link] || '';
qs('#hero-greeting').textContent = `👋 Hello, ${([Link] || 'Customer').split(' ')[0]}!`;
qs('#topbar-sub').textContent = '📍 Chengalpattu, TN · 48 shops nearby';
return true;
}
function doLogout() {
[Link]();
showToast('Logged out. See you soon!', 'info');
setTimeout(() => { [Link] = '../[Link]'; }, 900);
}
/* ═══════════════════════════════════════════════════════════
2. STATE — in-memory + localStorage
═══════════════════════════════════════════════════════════ */
const $ = id => [Link](id);
const qs = sel => [Link](sel);
const qsa = sel => [Link](sel);
const State = {
KEY: 'rp_customer_state',
load() {
try { return [Link]([Link]([Link])) || [Link](); }
catch { return [Link](); }
},
save(data) {
/* ═══════════════════════════════════════════════════════════
3. SHOP DATA
═══════════════════════════════════════════════════════════ */
const SHOPS = [
{
id:'shop-1', name:'Raj General Store', cat:'Grocery & Essentials', emoji:'🏪',
rating:4.8, reviews:120, dist:0.4, distLabel:'0.4 km',
open:true, address:'12 Main Street, Chengalpattu',
phone:'+91 98765 43210', since:'2019',
tags:['Grocery','Dairy','Spices','Beverages'],
products: ['Rice', 'Oil', 'Salt', 'Butter', 'Tea', 'Biscuits'],
priceRange:'₹20 – ₹500',
coverBg:'linear-gradient(135deg,#1a2a1a,#0d1f0d)',
desc:'Your neighbourhood go-to store for all daily essentials. Freshness guaranteed.',
hours:'8:00 AM – 9:00 PM', delivery:true,
},
{
id:'shop-2', name:'TechZone Electronics', cat:'Electronics & Gadgets', emoji:'⚡',
rating:4.6, reviews:86, dist:1.2, distLabel:'1.2 km',
open:true, address:'45 North Street, Chengalpattu',
phone:'+91 87654 32109', since:'2021',
tags:['Electronics','Mobiles','Laptops','Accessories'],
products: ['iPhone 15', 'Samsung Galaxy', 'Laptop', 'Earbuds', 'Chargers'],
priceRange:'₹299 – ₹1,20,000',
coverBg:'linear-gradient(135deg,#1a1a2a,#0d0d20)',
desc:'Premium electronics with genuine warranty and expert advice.',
hours:'10:00 AM – 8:00 PM', delivery:true,
},
{
id:'shop-3', name:'Bismillah Wood Furniture', cat:'Furniture & Home', emoji:'🪑',
rating:4.9, reviews:201, dist:2.1, distLabel:'2.1 km',
open:false, address:'10 Bazulla Road, Chengalpattu',
phone:'+91 76543 21098', since:'2015',
tags:['Furniture','Sofa','Wardrobe','Dining'],
products: ['Sofa Set', 'Wardrobe', 'Dining Table', 'Bed Frame', 'Study Table'],
priceRange:'₹4,999 – ₹85,000',
coverBg:'linear-gradient(135deg,#2a1a0d,#1f120a)',
desc:'Premium handcrafted furniture. Teak, rosewood, and engineered wood options.',
hours:'9:00 AM – 6:00 PM', delivery:true,
},
{
id:'shop-4', name:'CoolBreeze Appliances', cat:'Home Appliances', emoji:'🌀',
rating:4.5, reviews:72, dist:2.9, distLabel:'2.9 km',
open:true, address:'8 Temple Road, Chengalpattu',
/* ═══════════════════════════════════════════════════════════
4. PRODUCT DATA
═══════════════════════════════════════════════════════════ */
const PRODUCTS = [
{ id:'prod-1', name:'Basmati Rice 5kg', shop:'Raj General Store', emoji:'🌾', price:480,
oldPrice:null, rating:4.8, bg:'linear-gradient(135deg,#1a2a1a,#0d1f0d)', stock:'in', off:null },
{ id:'prod-2', name:'iPhone 15 128GB', shop:'TechZone Electronics', emoji:'📱', price:72999,
oldPrice:79999, rating:4.9, bg:'linear-gradient(135deg,#1a1a2a,#0d0d20)', stock:'in', off:9 },
{ id:'prod-3', name:'Premium Sofa 3+1+1', shop:'Bismillah Furniture', emoji:'🛋', price:24999,
oldPrice:32000, rating:4.7, bg:'linear-gradient(135deg,#2a1a0d,#1f120a)', stock:'in', off:22 },
{ id:'prod-4', name:'Split AC 1.5 Ton', shop:'CoolBreeze Appliances', emoji:'❄️', price:36990,
oldPrice:42000, rating:4.6, bg:'linear-gradient(135deg,#0d1f2a,#0a1520)', stock:'in', off:12 },
{ id:'prod-5', name:'Organic Tomatoes 1kg', shop:'Green Basket', emoji:'🍅', price:60,
oldPrice:null, rating:4.5, bg:'linear-gradient(135deg,#1a2a1a,#102010)', stock:'in', off:null },
{ id:'prod-6', name:'Sunflower Oil 5L', shop:'Raj General Store', emoji:'🫙', price:750,
oldPrice:820, rating:4.3, bg:'linear-gradient(135deg,#2a2a0d,#1a1a0a)', stock:'low', off:9 },
{ id:'prod-7', name:'Samsung Galaxy S24', shop:'TechZone Electronics', emoji:'📲', price:64999,
oldPrice:69999, rating:4.8, bg:'linear-gradient(135deg,#1a1a2a,#0d0d20)', stock:'in', off:7 },
{ id:'prod-8', name:'Amul Butter 500g', shop:'Raj General Store', emoji:'🧈', price:280,
oldPrice:null, rating:4.6, bg:'linear-gradient(135deg,#1a2a1a,#0d1f0d)', stock:'in', off:null },
{ id:'prod-9', name:'Washing Machine 7kg',shop:'CoolBreeze Appliances', emoji:'🧺', price:22990,
oldPrice:28000, rating:4.5, bg:'linear-gradient(135deg,#0d1f2a,#0a1520)', stock:'in', off:18 },
{ id:'prod-10',name:'Vitamin C 1000mg', shop:'MedPlus Pharmacy', emoji:'💊', price:299,
oldPrice:350, rating:4.7, bg:'linear-gradient(135deg,#1a0d2a,#130a1f)', stock:'in', off:15 },
{ id:'prod-11',name:'Study Table Oak', shop:'Bismillah Furniture', emoji:'🪑', price:8999,
oldPrice:11000, rating:4.4, bg:'linear-gradient(135deg,#2a1a0d,#1f120a)', stock:'out', off:18 },
{ id:'prod-12',name:'Tata Tea Premium', shop:'Raj General Store', emoji:'🍵', price:210,
oldPrice:null, rating:4.5, bg:'linear-gradient(135deg,#1a1a2a,#0d0d20)', stock:'in', off:null },
];
/* ═══════════════════════════════════════════════════════════
5. MAP DEMO (ready to connect Google Maps API)
═══════════════════════════════════════════════════════════ */
const SHOP_POSITIONS = {
'shop-1': { top:'55%', left:'47%' },
'shop-2': { top:'35%', left:'60%' },
'shop-3': { top:'68%', left:'30%' },
const MAP_COLORS = {
'shop-1':'#10D9A0','shop-2':'#5B63FE','shop-3':'#F5A623',
'shop-4':'#38BDF8','shop-5':'#A78BFA','shop-6':'#FF4D6D',
};
function buildDemoMap() {
const bg = qs('.map-demo-bg');
if (!bg) return;
// User location
const userDot = [Link]('div');
[Link] = 'user-location-dot';
[Link] = 'top:55%;left:47%;position:absolute;transform:translate(-50%,-50%)';
[Link] = 'Your location';
[Link](userDot);
// Shop markers
[Link](shop => {
const pos = SHOP_POSITIONS[[Link]] || { top:'50%', left:'50%' };
const marker = [Link]('div');
[Link] = 'map-marker';
[Link] = `top:${[Link]};left:${[Link]}`;
[Link] = [Link];
[Link] = `
<div class="marker-label">${[Link]}</div>
<div class="marker-pin" style="background:${MAP_COLORS[[Link]]}">
<span>${[Link]}</span>
</div>
<div class="marker-dot"></div>
`;
[Link]('click', () => selectShopOnMap([Link]));
[Link](marker);
});
}
function selectShopOnMap(shopId) {
// Highlight list card
qsa('.shop-list-card').forEach(c => [Link]('active'));
const card = qs(`[data-shop-id="${shopId}"]`);
if (card) {
[Link]('active');
[Link]({ behavior:'smooth', block:'nearest' });
}
// Highlight marker
qsa('.map-marker').forEach(m => {
/* ──────────────────────────────────────────
Google Maps API Integration Stub
To activate: replace YOUR_API_KEY below
and call initGoogleMap() instead of buildDemoMap()
────────────────────────────────────────── */
const GOOGLE_MAPS_API_KEY = 'YOUR_API_KEY_HERE'; // <── replace this
function initGoogleMap() {
// Called when Google Maps script loads (callback=initGoogleMap in URL)
const map = new [Link]([Link]('map'), {
center: { lat: 12.6921, lng: 79.9768 }, // Chengalpattu
zoom: 14,
styles: [
{ elementType:'geometry', stylers:[{ color:'#0d1117' }] },
{ elementType:'[Link]', stylers:[{ color:'#8B949E' }] },
{ elementType:'[Link]', stylers:[{ color:'#0D1117' }] },
{ featureType:'road', elementType:'geometry', stylers:[{ color:'#21262D' }] },
{ featureType:'road', elementType:'[Link]', stylers:[{ color:'#30363D' }] },
{ featureType:'water', elementType:'geometry', stylers:[{ color:'#0d1b2a' }] },
],
});
// User marker
new [Link]({
position: { lat:12.6921, lng:79.9768 }, map,
icon: { path: [Link], scale:8, fillColor:'#38BDF8', fillOpacity:1,
strokeColor:'white', strokeWeight:2 },
title: 'Your location'
});
// Shop markers
const shopCoords = {
'shop-1':{ lat:12.6940, lng:79.9780 }, 'shop-2':{ lat:12.6960, lng:79.9810 },
'shop-3':{ lat:12.6900, lng:79.9750 }, 'shop-4':{ lat:12.6930, lng:79.9740 },
'shop-5':{ lat:12.6970, lng:79.9795 }, 'shop-6':{ lat:12.6895, lng:79.9825 },
};
[Link](shop => {
const coords = shopCoords[[Link]];
if (!coords) return;
const marker = new [Link]({
position: coords, map,
label: { text: [Link], fontSize:'18px' },
title: [Link],
});
[Link]('click', () => {
selectShopOnMap([Link]);
openShopModal([Link]);
});
});
}
/* ═══════════════════════════════════════════════════════════
6. RENDER SHOPS
═══════════════════════════════════════════════════════════ */
function renderShopListPanel() {
const container = $('shop-list-panel');
if (!container) return;
[Link] = [Link](s => `
<div class="shop-list-card" data-shop-id="${[Link]}" onclick="selectShopOnMap('${[Link]}')">
<div class="slc-header">
<span class="slc-icon">${[Link]}</span>
<div>
<div class="slc-name">${[Link]}</div>
<div class="slc-cat">${[Link]}</div>
</div>
</div>
<div class="slc-meta">
<span class="slc-dist">📍 ${[Link]}</span>
<span>⭐ ${[Link]}</span>
<span class="slc-open ${[Link]?'open':'closed'}">${[Link]?'● Open':'● Closed'}</span>
</div>
<div class="slc-actions">
<button class="slc-btn primary" onclick="[Link]();openShopModal('$
{[Link]}')">View Shop</button>
<button class="slc-btn" onclick="[Link]();sendEnquiry('$
{[Link]}')">Enquire</button>
<button class="slc-fav-btn ${[Link]([Link])?'active':''}"
onclick="[Link]();toggleFavShop('${[Link]}',this)">
${[Link]([Link])?'❤️':'🤍'}
</button>
</div>
</div>
`).join('');
}
/* ═══════════════════════════════════════════════════════════
7. RENDER PRODUCTS
═══════════════════════════════════════════════════════════ */
function renderProducts(products, containerId) {
const container = $(containerId);
if (!container) return;
[Link] = [Link](p => {
const inWish = [Link]([Link]);
const inCompare = [Link]([Link]);
const stockTag = [Link] === 'in' ? '<span class="prod-stock-tag badge-emerald">● In
Stock</span>'
: [Link] === 'low' ? '<span class="prod-stock-tag"
style="background:var(--amber-dim);color:var(--amber)">⚠ Low Stock</span>'
: '<span class="prod-stock-tag badge-rose">✕ Out of Stock</span>';
const offerTag = [Link] ? `<span class="prod-offer-tag">${[Link]}% OFF</span>` : '';
const oldPriceHtml = [Link] ? `<span class="prod-old-price">₹$
{[Link]('en-IN')}</span>` : '';
return `
<div class="prod-card" data-prod-id="${[Link]}">
<div class="prod-thumb" style="background:${[Link]}">
${[Link]}
${stockTag}
<button class="prod-wish-btn ${inWish?'active':''}"
onclick="toggleWishlist('${[Link]}',this)" title="Wishlist">
${inWish?'❤️':'🤍'}
</button>
${offerTag}
</div>
<div class="prod-body">
<div class="prod-shop">${[Link]}</div>
<div class="prod-name">${[Link]}</div>
<div class="prod-price-row">
<span class="prod-price">₹${[Link]('en-IN')}</span>
${oldPriceHtml}
</div>
<div class="prod-footer">
<span class="prod-rating">⭐ ${[Link]}</span>
<div style="display:flex;gap:5px;align-items:center">
<button class="btn btn-sm btn-ghost" style="font-size:10px;padding:4px 8px"
onclick="toggleCompare('${[Link]}')" title="Compare">⇌</button>
<button class="prod-add-btn ${[Link]==='out'?'':''}"
${[Link]==='out'?'disabled':''} onclick="addToCart('${[Link]}',this)">
${[Link]==='out'?'✕':'+'}
</button>
</div>
</div>
</div>
</div>`;
}).join('');
}
/* ═══════════════════════════════════════════════════════════
8. CART SYSTEM
═══════════════════════════════════════════════════════════ */
function addToCart(prodId, btnEl) {
const prod = [Link](p => [Link] === prodId);
if (!prod || [Link] === 'out') return;
function renderCart() {
const body = $('cart-body');
if (!body) return;
if (![Link]) {
[Link] = `<div style="text-align:center;padding:48px 20px">
<div style="font-size:52px;margin-bottom:12px">🛒</div>
<div style="font-size:16px;font-weight:700;margin-bottom:6px">Your cart is empty</div>
<div style="font-size:13px;color:var(--text2);margin-bottom:20px">Add products from nearby
shops</div>
<button class="btn btn-primary" onclick="switchPage('home')">Discover Shops</button>
</div>`;
return;
}
updateCartSummary();
}
function removeCartItem(id) {
const el = $(`ci-${id}`);
if (el) { [Link]='0'; [Link]='translateX(20px)';
[Link]='all .3s'; setTimeout(()=>[Link](),300); }
[Link] = [Link](c => [Link] !== id);
saveState();
updateCartBadges();
updateCartSummary();
showToast('Item removed from cart','error');
if (![Link]) setTimeout(renderCart, 400);
}
function clearCart() {
[Link] = [];
saveState();
updateCartBadges();
renderCart();
function updateCartSummary() {
const sub = [Link]((s,c) => s + [Link] * [Link], 0);
const delivery = sub > 0 ? 30 : 0;
const el_sub = $('cart-subtotal'), el_tot = $('cart-total');
if (el_sub) el_sub.textContent = '₹' + [Link]('en-IN');
if (el_tot) el_tot.textContent = '₹' + (sub + delivery).toLocaleString('en-IN');
}
function updateCartBadges() {
const count = [Link]((s,c) => s + [Link], 0);
qsa('.cart-badge').forEach(el => { [Link] = count; [Link] = count ? 'inline' :
'none'; });
}
function applyPromo() {
const code = $('promo-input')?.[Link]().toUpperCase();
if (code === 'SAVE10') showToast('Promo applied! ₹109 off 🎉','success');
else if (code === 'FREE') showToast('Free delivery applied!','success');
else showToast('Invalid promo code','error');
}
function checkout() {
showToast('🎉 Order placed successfully!','success');
[Link] = [];
saveState();
updateCartBadges();
setTimeout(()=> switchPage('orders'), 1200);
}
/* ═══════════════════════════════════════════════════════════
9. WISHLIST
═══════════════════════════════════════════════════════════ */
function toggleWishlist(prodId, btnEl) {
const idx = [Link](prodId);
if (idx > -1) {
[Link](idx,1);
if (btnEl) { [Link]('active'); [Link] = '🤍'; }
showToast('Removed from wishlist','info');
} else {
[Link](prodId);
if (btnEl) { [Link]('active'); [Link] = '❤️'; }
showToast('Added to wishlist ❤️','success');
}
saveState();
renderWishlist();
}
function renderWishlist() {
const container = $('wishlist-container');
if (!container) return;
const wishProds = [Link](p => [Link]([Link]));
if (![Link]) {
[Link] = `<div style="text-align:center;padding:40px;grid-column:1/-1">
<div style="font-size:48px;margin-bottom:12px">💔</div>
<div style="font-size:16px;font-weight:700;margin-bottom:6px">Wishlist is empty</div>
<div style="font-size:13px;color:var(--text2)">Save products you like for later</div>
</div>`;
return;
}
renderProducts(wishProds, 'wishlist-container');
}
/* ═══════════════════════════════════════════════════════════
10. FAVOURITE SHOPS
═══════════════════════════════════════════════════════════ */
function toggleFavShop(shopId, btnEl) {
const idx = [Link](shopId);
if (idx > -1) {
function renderFavShops() {
const container = $('fav-shops-container');
if (!container) return;
const favs = [Link](s => [Link]([Link]));
if (![Link]) {
[Link] = `<div style="text-align:center;padding:40px;grid-column:1/-1">
<div style="font-size:48px;margin-bottom:12px">🏪</div>
<div style="font-size:15px;font-weight:700;margin-bottom:6px">No favourite shops yet</div>
<div style="font-size:13px;color:var(--text2)">Tap ❤️ on any shop to save it here</div>
</div>`;
return;
}
renderShopsGrid(favs, 'fav-shops-container');
}
function toggleFollowShop(shopId) {
const idx = [Link](shopId);
const btn = $('follow-shop-btn');
if (idx > -1) {
[Link](idx,1);
if (btn) { [Link] = '+ Follow'; [Link]('btn-primary');
[Link]('btn-secondary'); }
showToast('Unfollowed shop','info');
} else {
[Link](shopId);
if (btn) { [Link] = '✓ Following'; [Link]('btn-primary');
[Link]('btn-secondary'); }
showToast('Now following this shop!','success');
}
saveState();
}
/* ═══════════════════════════════════════════════════════════
11. COMPARE
═══════════════════════════════════════════════════════════ */
function toggleCompare(prodId) {
const idx = [Link](prodId);
if (idx > -1) {
[Link](idx,1);
showToast('Removed from compare','info');
} else {
if ([Link] >= 3) { showToast('Max 3 products can be compared','warn');
return; }
[Link](prodId);
const prod = [Link](p => [Link] === prodId);
showToast(`${prod?.name} added to compare`,'info');
}
saveState();
renderCompareBar();
}
function renderCompareBar() {
const bar = $('compare-bar');
if (!bar) return;
if (![Link]) { [Link]('visible'); return; }
[Link]('visible');
const slots = $('compare-slots');
if (!slots) return;
[Link] = [0,1,2].map(i => {
function openCompareModal() {
if ([Link] < 2) { showToast('Add at least 2 products to compare','warn');
return; }
const prods = [Link](p => [Link]([Link]));
// Build compare table
const modal = $('compare-modal');
const body = $('compare-modal-body');
if (!body) return;
[Link]('open');
}
/* ═══════════════════════════════════════════════════════════
12. RECENTLY VIEWED
═══════════════════════════════════════════════════════════ */
function addRecentlyViewed(prodId) {
[Link] = [Link](id => id !== prodId);
[Link](prodId);
if ([Link] > 8) [Link]();
saveState();
renderRecentlyViewed();
}
function renderRecentlyViewed() {
const container = $('recently-viewed');
if (!container) return;
const prods = [Link](id => [Link](p => [Link] === id)).filter(Boolean);
[Link] = [Link](p => `
<div class="h-scroll-item">
<div class="prod-card" style="width:150px" data-prod-id="${[Link]}"
onclick="addRecentlyViewed('${[Link]}')">
<div class="prod-thumb" style="background:${[Link]};height:80px;font-size:32px">${[Link]}
<span class="prod-stock-tag ${[Link]==='in'?'badge-emerald':'badge-rose'}"
style="font-size:9px;padding:2px 6px">${[Link]==='in'?'In Stock':'Out'}</span>
</div>
<div class="prod-body" style="padding:8px">
<div class="prod-name" style="font-size:11px">${[Link]}</div>
<div class="prod-price" style="font-size:13px;margin-top:4px">₹$
{[Link]('en-IN')}</div>
</div>
</div>
/* ═══════════════════════════════════════════════════════════
13. SHOP DETAIL MODAL
═══════════════════════════════════════════════════════════ */
function openShopModal(shopId) {
const shop = [Link](s => [Link] === shopId);
if (!shop) return;
$('shop-modal-content').innerHTML = `
<div class="shop-modal-hero">
<div class="shop-modal-info">
<div class="shop-modal-icon-lg">${[Link]}</div>
<div class="shop-modal-name">${[Link]}</div>
<div class="shop-modal-meta">
<div class="shop-modal-meta-row">📍 ${[Link]}</div>
<div class="shop-modal-meta-row">📞 ${[Link]}</div>
<div class="shop-modal-meta-row">⭐ ${[Link]} · ${[Link]} reviews</div>
<div class="shop-modal-meta-row">🕐 ${[Link]}</div>
<div class="shop-modal-meta-row">📦 ${[Link]}</div>
<div class="shop-modal-meta-row" style="color:${[Link]?'var(--emerald)':'var(--
rose)'}">
${[Link]?'● Open Now':'● Closed'}
</div>
</div>
<p style="font-size:12px;color:var(--text2);margin:12px 0;line-height:1.6">$
{[Link]}</p>
<div class="shop-modal-tags">${[Link](t=>`<span
class="shop-tag">${t}</span>`).join('')}</div>
</div>
<div class="shop-modal-actions">
<div class="shop-modal-stat-grid">
<div class="shop-modal-stat"><div class="sms-label">Since</div><div class="sms-val">$
{[Link]}</div></div>
<div class="shop-modal-stat"><div class="sms-label">Products</div><div class="sms-
val">${[Link]*12}+</div></div>
<div class="shop-modal-stat"><div class="sms-label">Delivery</div><div class="sms-
val">${[Link]?'✓ Yes':'✗ No'}</div></div>
<div class="shop-modal-stat"><div class="sms-label">Distance</div><div class="sms-
val">${[Link]}</div></div>
</div>
<button id="follow-shop-btn" class="btn ${isFollow?'btn-primary':'btn-secondary'} btn-
full" onclick="toggleFollowShop('${shopId}')">
${isFollow?'✓ Following':'+ Follow Shop'}
</button>
<button class="btn btn-ghost btn-full" onclick="toggleFavShop('${shopId}', this)">
${isFav?'❤️ Saved':'🤍 Save Shop'}
</button>
<button class="btn btn-ghost btn-full" onclick="sendEnquiry('${shopId}')">
💬 Quick Enquiry
</button>
<button class="btn btn-ghost btn-full" onclick="[Link]('[Link]
📞 Call Shop
</button>
</div>
</div>
$('shop-modal-overlay').[Link]('open');
[Link] = 'hidden';
}
function closeShopModal() {
$('shop-modal-overlay').[Link]('open');
[Link] = '';
}
function sendEnquiry(shopId) {
openShopModal(shopId);
setTimeout(() => { const enq = $('enq-msg'); if (enq) [Link](); }, 400);
}
function submitEnquiry(shopId) {
const shop = [Link](s => [Link] === shopId);
const name = $('enq-name')?.[Link]();
const msg = $('enq-msg')?.[Link]();
if (!name || !msg) { showToast('Please fill your name and message','warn'); return; }
showToast(`Enquiry sent to ${shop?.name}! They'll contact you soon.`,'success');
closeShopModal();
}
/* ═══════════════════════════════════════════════════════════
14. NOTIFICATIONS
═══════════════════════════════════════════════════════════ */
const NOTIFICATIONS = [
{ id:1, icon:'🛍', title:'Order Delivered!', body:'Your order #ORD-028 from Raj General Store has
been delivered.', time:'2 min ago', unread:true },
{ id:2, icon:'⚡', title:'Flash Sale — TechZone!', body:'iPhone 15 now at ₹72,999. Limited stock,
grab it fast!', time:'1 hr ago', unread:true },
{ id:3, icon:'📦', title:'Back in Stock!', body:'Samsung Galaxy S24 is back at TechZone
Electronics.', time:'3 hr ago', unread:true },
₹ title:'Price Drop Alert', body:'Sunflower Oil 5L dropped by ₹70 at Raj General
{ id:4, icon:'',
Store.', time:'Today', unread:true },
{ id:5, icon:'🏪', title:'New Shop Nearby!', body:'Fresh Greens Organic opened 2.3 km from you.
Check it out!', time:'Yesterday', unread:false },
{ id:6, icon:'⭐', title:'Rate Your Purchase', body:'How was Amul Butter 500g? Share your review.',
time:'2 days ago', unread:false },
{ id:7, icon:'🎉', title:'Exclusive Offer', body:'Use code SAVE10 for 10% off your next order at any
shop.', time:'3 days ago', unread:false },
];
function openNotifDrawer() {
$('notif-drawer').[Link]('open');
$('notif-overlay').[Link]('open');
[Link] = 'hidden';
[Link] = 0;
qsa('.notif-badge').forEach(b => [Link] = 'none');
saveState();
}
function closeNotifDrawer() {
$('notif-drawer').[Link]('open');
$('notif-overlay').[Link]('open');
function renderNotifications() {
const body = $('notif-body');
if (!body) return;
[Link] = [Link](n => `
<div class="notif-item ${[Link]?'unread':''}" onclick="markNotifRead(${[Link]})">
<span class="notif-icon">${[Link]}</span>
<div style="flex:1">
<div class="notif-title">${[Link]}</div>
<div class="notif-body">${[Link]}</div>
<div class="notif-time">${[Link]}</div>
</div>
${[Link]?'<div class="notif-unread-dot"></div>':''}
</div>
`).join('');
}
function markNotifRead(id) {
const notif = [Link](n => [Link] === id);
if (notif) { [Link] = false; renderNotifications(); }
}
function markAllRead() {
[Link](n => [Link] = false);
renderNotifications();
showToast('All notifications marked as read','success');
}
/* ═══════════════════════════════════════════════════════════
15. ORDERS
═══════════════════════════════════════════════════════════ */
const ORDERS = [
{ id:'ORD-028', shop:'Raj General Store', shopEmoji:'🏪', items:['🌾 Rice ×1','🫙 Oil ×2','🧂 Salt
×1'], amount:524, status:'delivered', date:'26 Mar 2026 · 10:32 AM', timeline:
['done','done','done','done','done'] },
{ id:'ORD-027', shop:'Fresh Mart', shopEmoji:'🛒', items:['🧈 Butter ×1','🥛 Milk ×2'],
amount:320, status:'processing',date:'25 Mar 2026 · 9:15 AM', timeline:
['done','done','current','',''] },
{ id:'ORD-026', shop:'Green Basket', shopEmoji:'🌿', items:['🍪 Parle-G ×3','🍵 Tea ×1'],
amount:180, status:'pending', date:'24 Mar 2026 · 6:45 PM', timeline:['done','current','','',''] },
{ id:'ORD-025', shop:'TechZone Electronics',shopEmoji:'⚡', items:['📱 Phone Case ×1'],
amount:599, status:'delivered', date:'22 Mar 2026 · 3:00 PM', timeline:
['done','done','done','done','done'] },
];
function renderOrders() {
const container = $('orders-list');
if (!container) return;
[Link] = [Link](o => `
<div class="order-card">
<div class="order-card-header">
<div class="order-shop-icon">${[Link]}</div>
<div>
<div style="font-size:13px;font-weight:600">${[Link]}</div>
<div class="order-id">#${[Link]}</div>
<div class="order-meta">${[Link]}</div>
</div>
<div style="margin-left:auto;display:flex;flex-direction:column;align-items:flex-
end;gap:6px">
<span class="badge ${STATUS_BADGE[[Link]]}">${STATUS_TEXT[[Link]]}</span>
<div class="order-amount">₹${[Link]('en-IN')}</div>
</div>
</div>
/* ═══════════════════════════════════════════════════════════
16. SEARCH — live search across shops & products
═══════════════════════════════════════════════════════════ */
function initSearch() {
const topSearch = $('topbar-search');
const heroSearch = $('hero-search-input');
function heroSearch() {
const q = $('hero-search-input')?.[Link]().toLowerCase();
if (!q) return;
$('topbar-search').value = q;
$('topbar-search').dispatchEvent(new Event('input'));
}
/* ═══════════════════════════════════════════════════════════
17. FILTERS
═══════════════════════════════════════════════════════════ */
let activeFilters = { category:'All', distance:'All', rating:'All', open:false };
function toggleOpenFilter(btn) {
[Link] = ![Link];
[Link]('active', [Link]);
applyShopFilter();
}
/* ═══════════════════════════════════════════════════════════
18. PAGE NAVIGATION
═══════════════════════════════════════════════════════════ */
const PAGE_META = {
home: { title:'Discover', sub:'📍 Chengalpattu, TN · 48 shops nearby' },
shops: { title:'Nearby Shops', sub:'Browse and filter shops near you' },
products: { title:'Products', sub:'Trending and recommended items' },
cart: { title:'My Cart', sub:'Review your cart before checkout' },
wishlist: { title:'My Wishlist', sub:'Products you saved for later' },
orders: { title:'My Orders', sub:'Track and manage your orders' },
profile: { title:'My Profile', sub:'Manage your account and preferences' },
};
function switchPage(pageId) {
qsa('.page').forEach(p => [Link]('active'));
qsa('.nav-item').forEach(n => [Link]('active'));
qsa('.mbn-item').forEach(n => [Link]('active'));
// Page-specific render
if (pageId === 'cart') renderCart();
if (pageId === 'wishlist') renderWishlist();
// Scroll to top
[Link]({ top:0, behavior:'smooth' });
}
/* ═══════════════════════════════════════════════════════════
19. SIDEBAR MOBILE TOGGLE
═══════════════════════════════════════════════════════════ */
function toggleSidebar() {
qs('.sidebar').[Link]('mobile-open');
qs('.sidebar-overlay').[Link]('open');
}
function closeSidebar() {
qs('.sidebar').[Link]('mobile-open');
qs('.sidebar-overlay').[Link]('open');
}
/* ═══════════════════════════════════════════════════════════
20. PRICE ALERT
═══════════════════════════════════════════════════════════ */
function setPriceAlert(prodId) {
const prod = [Link](p => [Link] === prodId);
if (!prod) return;
if ([Link](prodId)) {
showToast(`Price alert already set for ${[Link]}`,'info');
} else {
[Link](prodId);
saveState();
showToast(`Price alert set for ${[Link]} 🔔`,'success');
}
}
/* ═══════════════════════════════════════════════════════════
21. TOAST
═══════════════════════════════════════════════════════════ */
function showToast(msg, type='info') {
const container = $('toasts');
if (!container) return;
const colors = { success:'var(--emerald)', error:'var(--rose)', info:'var(--sky)', warn:'var(--
amber)' };
⚠️
const icons = { success:'', error:'', info:'💡', warn:'⚠️' };
const t = [Link]('div');
[Link] = 'toast';
[Link] = `3px solid ${colors[type]||'var(--indigo)'}`;
[Link] = `<span style="font-size:16px">${icons[type]||'ℹ️'}</span><span class="toast-msg">$
{msg}</span>`;
[Link](t);
setTimeout(() => { [Link]('out'); setTimeout(() => [Link](), 350); }, 4000);
}
/* ═══════════════════════════════════════════════════════════
22. UTILITY
═══════════════════════════════════════════════════════════ */
function debounce(fn, ms) {
let timer;
return function(...args) { clearTimeout(timer); timer = setTimeout(() => [Link](this, args), ms);
};
}
function saveProfile() {
const name = $('edit-name')?.[Link]();
const email = $('edit-email')?.[Link]();
if (!name) { showToast('Name cannot be empty','warn'); return; }
[Link] = name;
[Link] = email || [Link];
[Link] ? null : [Link]('rp_user', [Link]({...currentUser}));
[Link]('rp_user', [Link]({...currentUser, loggedIn:true}));
qs('#cu-name').textContent = name;
/* ═══════════════════════════════════════════════════════════
23. INIT
═══════════════════════════════════════════════════════════ */
[Link]('DOMContentLoaded', () => {
if (!initAuth()) return;
// Build map
loadGoogleMapsAPI();
// Search
initSearch();
// Category pills
qsa('.cat-pill').forEach(pill => {
[Link]('click', function() {
qsa('.cat-pill').forEach(p => [Link]('active'));
[Link]('active');
const cat = [Link] || 'All';
[Link] = cat;
applyShopFilter();
});
});
// Hero chips
qsa('.hero-chip').forEach(chip => {
[Link]('click', function() {
qsa('.hero-chip').forEach(c => [Link]('active'));
[Link]('active');
});
});
// Sidebar overlay
qs('.sidebar-overlay')?.addEventListener('click', closeSidebar);