GripFit E-commerce: React + Vite Migration PRD

Product GripFit (codebase: opencodegrip), a fitness/apparel store for Nepal
Document type Product Requirements Document, PHP monolith to React (Vite) SPA + PHP/MySQLi API + Firebase Auth
Version 1.1 (adds lightweight / fast-load and lazy image loading requirements, see 12.1)
Date 29 Sep 2026
Basis Analysis of the uploaded opencodegrip project: database.sql, .htaccess routes, config/, includes/, storefront pages, admin/*, api/*, CSS tokens, service worker

How this was produced. I read the full database schema, all URL routes, the config/constants, shared helpers, the checkout / cart / order / auth / NCM flows, the admin module list, the theme CSS variables and the logo files. I did not read every line of the ~27k lines of PHP. Items marked [Confirm] are places where the code was ambiguous (for example commented-out blocks) and need a short discovery check before build.


1. Summary

1.1 What exists today

A server-rendered PHP 8 / MySQLi / session-based store with:

1.2 What we are building

Layer Today Target
UI PHP templates + vanilla JS React 18 + Vite + TypeScript SPA
Auth PHP sessions, password_hash, custom Google OAuth, OTP reset Firebase Authentication only (identity + tokens)
Data MySQLi in page files MySQL via MySQLi, behind a PHP JSON REST API
Everything else (orders, catalog, roles, carts, settings) MySQL Stays in MySQL, schema identical or additive
Theme Black/white, data-theme dark mode Shopify-style minimalist black/white, toggleable light/dark/system

1.3 A critical architectural point

MySQLi is a PHP extension. A React/Vite app runs in the browser and cannot connect to MySQL. So "React Vite with MySQLi" necessarily means:

Firebase is used only to answer "who is this person?". Every authorization decision (customer vs admin, who owns which order) is made by the PHP API against MySQL. This is the design assumed throughout the document.

1.4 Assumptions

  1. "Sophify" = Shopify. I interpret the theme as a Shopify-like look: generous whitespace, thin borders, restrained type, monochrome with almost no decoration.
  2. Hosting is Apache/PHP shared hosting or a VPS (the current .htaccess, Cloudflare X-Forwarded-Proto handling, and Sundarharaicha, Morang shop address suggest this). The frontend deploys as static files; the API deploys as PHP.
  3. Language and locale: English UI, Rs. currency, Asia/Kathmandu timezone (UTC+05:45).
  4. Scope is parity. No new commerce features are added except those listed in section 14 (optional).
  5. Logos: the four logo files already exist in the zip under assets/img/ (not assets/image/). Section 9.3 maps them. If you supply new versions, only filenames need to match.

2. Goals, non-goals, success criteria

2.1 Goals

2.2 Non-goals

2.3 Success criteria

Metric Target
Feature parity checklist (section 5) 100% of P0 and P1 items pass UAT
Data migration 0 orders lost; users can sign in after cut-over (see 7.6)
Lighthouse (mobile, product & shop pages) Performance ≥ 85, Accessibility ≥ 95, SEO ≥ 95
Initial JS (storefront, gzip) ≤ 120 KB on home; admin code and Firebase SDK not in the initial bundle
Home page weight (first load, mobile) ≤ 500 KB transferred before user interaction (images included)
Core Web Vitals (mobile, 4G) LCP ≤ 2.5 s, CLS ≤ 0.05, INP ≤ 200 ms
Theme No flash of wrong theme on first paint; toggle persists across sessions
Checkout conversion vs. old site Not worse than baseline over the first 4 weeks

3. Users and roles

Role Source of truth Capabilities
Guest none Browse catalog, search, view product, track an order (order number + phone), read static pages, contact form. Cannot add to cart (current behavior, cart is DB-backed and requires login).
Customer (users.role = 'customer') MySQL Cart, wishlist, coupons, checkout, order history and cancellation, reviews (verified buyers only), notifications, profile, account deletion.
Admin (admin) MySQL Everything in the admin panel, gated by the extra admin access code.
Superadmin (superadmin) MySQL Admin plus user/role management. [Confirm] exact superadmin-only actions in admin/users.php and settings.php.

Role is stored in MySQL (as today). Firebase custom claims may mirror it as a convenience, but the API never trusts the claim alone; it re-reads users.role and users.status.


4. Target architecture

4.1 Diagram (logical)

Browser (React SPA, Vite build, PWA)
  ├─ Firebase JS SDK  ── sign in / sign up / Google / reset ──▶ Firebase Auth
  │        └─ getIdToken()
  └─ fetch('/api/...', Authorization: Bearer <Firebase ID token>)
            │
            ▼
PHP API (mysqli, prepared statements, JSON)
  ├─ verifies ID token (Google public keys, aud/iss/exp)
  ├─ maps firebase_uid ▶ users row (role, status)
  ├─ MySQL (same schema)
  ├─ PHPMailer (order/contact/payment-reminder/delete-code emails)
  ├─ minishlink/web-push (VAPID push)
  ├─ NCM webhook receiver + NCM API client
  └─ uploads/ (products, brands, categories, slides, avatars; payment_receipts private)

4.2 Frontend stack (recommended)

Concern Choice Notes
Framework React 18, Vite, TypeScript TS strongly recommended given the size of the domain model
Routing React Router v6 (data routers), lazy routes /admin/* is a separately lazy-loaded chunk
Server state TanStack Query Caching, pagination, optimistic cart/wishlist
Client state Zustand (or Context) for theme, cart drawer, toasts, applied coupon
Forms/validation React Hook Form + Zod Zod schemas shared with API contract docs
Styling Tailwind CSS + CSS variables (design tokens from section 9) Tokens are the existing variable names/values
Icons lucide-react, imported per icon (tree-shaken), brand icons (WhatsApp/Facebook) as inline SVG Font Awesome is removed entirely (the current site ships ~5 FA CSS files plus font files)
Charts (admin) react-chartjs-2 (Chart.js 4, as today) Colors read from theme tokens
PWA vite-plugin-pwa (Workbox) Replaces hand-written service-worker.js, keeps offline page and push handler
Head/SEO react-helmet-async + crawler shim (section 12.2)
Testing Vitest + Testing Library; Playwright e2e
Bundle guard rollup-plugin-visualizer + size-limit in CI Build fails if a budget in 12.1 is exceeded
Dependencies policy No moment/lodash/jQuery/UI mega-kits; prefer native APIs (Intl, fetch, IntersectionObserver) Every new dependency needs a size justification

4.3 Backend stack

4.4 Repo layout

gripfit/
├─ web/                      # React + Vite
│  ├─ src/
│  │  ├─ app/                # router, providers (Query, Theme, Auth)
│  │  ├─ features/           # catalog, cart, checkout, orders, account, wishlist, reviews, notifications
│  │  ├─ admin/              # dashboard, products, orders, users, coupons, ...
│  │  ├─ components/ui/      # Button, Input, Modal, Drawer, Toast, Skeleton, Pagination ...
│  │  ├─ lib/                # api client, firebase.ts, format.ts (Rs. formatting), nepalAddress.ts
│  │  ├─ styles/tokens.css   # light/dark variables
│  │  └─ assets/logos/       # grip.png, grip-dark.png, grip-icon.png, grip-icon-dark.png
│  └─ public/                # manifest, icons, offline.html, robots.txt
├─ api/                      # PHP
│  ├─ public/index.php       # front controller
│  ├─ src/{Http,Auth,Repositories,Services,Support}/
│  ├─ config/                # env loader, constants
│  ├─ migrations/            # 0001_baseline.sql, 0002_firebase.sql, ...
│  ├─ uploads/               # + .htaccess protections (as today)
│  └─ composer.json
└─ docs/                     # this PRD, API contract (OpenAPI), runbooks

5. Feature inventory and requirements

Priority: P0 = launch blocker, P1 = required for parity, ships in same release if possible, P2 = post-launch/optional.

5.1 Storefront

ID Feature Legacy source New route Requirements Pri
S-01 Home index.php / Hero slider from hero_slides (active, ordered by sort_order) with fallback static hero; "Shop by Category"; "Best Sellers"; "Featured Products" (is_featured); "Why Choose Us"; announcement bar from site_settings. Server-side cached lists (replaces cache/*.cache files). P0
S-02 Shop / listing shop.php /shop Filters: department, category, brand, gender (men/women/unisex), featured, in-stock, on-sale; search text; sort (default newest; also price asc/desc etc. [Confirm] full list); server pagination; filter state lives in the URL query string; skeleton loaders. P0
S-03 Live search search_ajax.php header search Debounced (≈250 ms) query, max 10 results, name/slug/image/min price/old price/brand, only active products with stock. P0
S-04 Product detail product.php /shop/:slug Image gallery with lightbox and pagination; size/color selectors driven by product_stock variants (price/old price/qty/SKU per variant); stock status per variant; add-to-cart; wishlist toggle; share (WhatsApp, Facebook, Messenger, native share, copy link); related products (same category, then same brand, up to 8); reviews list and rating summary; recently-viewed [Confirm whether exists]. P0
S-05 Reviews product.php, review_action.php product page Only verified purchasers may review; one review per (user, product, order item); rating 1-5, title, comment; new reviews pending; admin sets verified/rejected; users can edit/delete their own; products.average_rating and total_reviews kept in sync. P1
S-06 Cart cart.php, cart_action.php, assets/js/cart.js /cart (+ mini-cart drawer) DB-backed, login required. Add (auto-selects cheapest in-stock variant when no size/color chosen), update quantity (capped to available stock), remove, fetch. Unique per (user, stock, product). Cart count badge in header. P0
S-07 Coupons coupon_action.php cart/checkout Apply/remove code. Validate: active, not expired, used_count < max_uses, per_user_limit, min_order_amount, and scope (all/category/brand/product) with discount computed only on eligible items. Types percentage/fixed. Re-validated on order placement. P0
S-08 Checkout checkout.php /checkout Name, email, phone, address using Nepal address picker (nepal-address.json: province → district → municipality → ward); payment choice: WhatsApp/COD-style ("whatsapp") or Prepaid; prepaid requires gateway (esewa or bank_transfer), QR display (gripfit-qr.jpg, bank-qr.jpg) and a payment screenshot upload; totals: subtotal - discount + shipping. Shipping = SHIPPING_COST unless the free-shipping threshold is enabled and met (see 8.2). P0
S-09 Order placement checkout.php POST /api/orders Single DB transaction: validate items & stock, re-price from DB (never trust client prices), create order (order_number = yymmdd + 8 hex chars, uniqueness-checked), snapshot order_items, decrement product_stock, record coupon_usage + used_count, save receipt, clear cart, audit-log, notify admins (push + notifications), send confirmation email. P0
S-10 Order confirmation order_confirmation.php /order-confirmation/:orderNumber Owner-only. Summary, payment instructions (e.g. WhatsApp deep link with order text), status badges. P0
S-11 Order tracking (guest-friendly) track-order-status.php /track-order-status Lookup by order_number and customer_phone. Shows order status, payment status, NCM status/location/last sync. Rate-limited. P1
S-12 Wishlist wishlist.php, wishlist_action.php /wishlist Toggle/remove; get_products view with price/stock; heart state on all product cards. P1
S-13 Account: Profile account.php /account (tab) View/edit full name, phone, address, avatar upload; email shown read-only (owned by Firebase); appearance (theme) control; danger zone. P0
S-14 Account: Orders account.php, order_action.php /account?tab=orders Status filter pills, pagination, order details, payment-reminder cues, cancel only when pending; cancelling restores stock if not paid; paid orders become refunded payment status. P0
S-15 Account deletion account_action.php account danger zone Request a 6-digit code by email (password_resets.type='account_delete'), confirm within expiry, then soft-delete (status='deleted', deleted_at), delete Firebase user. Order history is retained (orders.user_id uses ON DELETE SET NULL). P1
S-16 Notifications api/notifications.php, api/mark_read.php header bell Unread badge, dropdown list, mark one/all read. Backed by notifications (+ user_notifications for read state; bulk notifications are lazily materialized as today). P1
S-17 Web Push opt-in ordernotification/* prompt in account/after order Subscribe/unsubscribe; store in push_subscriptions; service worker shows notification and opens the target URL. P1
S-18 Static pages about/contact/faq/shipping/returns/privacy/terms same slugs Content parity. Contact form emails the shop (sendContactEmail), rate-limited. Shop contact details from site_settings. P1
S-19 System pages 404/500/coming-soon/maintenance/offline *, /status/* Maintenance and coming-soon modes toggled from settings; admins can bypass maintenance. [Confirm bypass logic]. P1
S-20 PWA manifest.json, service-worker.js - Installable, offline fallback page, asset caching, push. Icons from assets/icons/. P1

5.2 Authentication (Firebase)

ID Feature Requirement Pri
A-01 Register Email + password via Firebase; collect full name (and phone) in our form; on first token the API creates the users row (firebase_uid, role customer). Send Firebase verification email. Password strength rule kept (validatePasswordStrength) client-side, Firebase policy enforces minimum. P0
A-02 Login Email/password via Firebase; then POST /api/auth/session to sync and fetch profile/role. Generic error copy. P0
A-03 Google sign-in Firebase Google provider (popup; redirect fallback on mobile). Replaces custom OAuth code, google_id, state handling. The "suggested Google email" mismatch edge case in auth.php is handled by Firebase account-linking rules. P0
A-04 Forgot / reset password Firebase sendPasswordResetEmail; the branded reset page can use Firebase's action handler or a custom /reset-password page using confirmPasswordReset. The OTP password_reset flow is retired. P0
A-05 Logout / session Firebase signOut; token refresh handled by SDK; API is stateless. P0
A-06 Route guards RequireAuth (customer routes), RequireAdmin (admin routes; also requires access-code verification). P0
A-07 Admin access code (2nd factor) Preserve admin/verify.php behavior: after login, an admin must submit the access code (from settings/env) → API returns a short-lived, signed admin-verify token (e.g. 8-12 h, bound to uid) sent as X-Admin-Token. Rate-limited. P0
A-08 Account status enforcement API rejects tokens whose users.status = 'deleted'. Deleting an admin invalidates access immediately (as admin/guard.php does today). P0
A-09 Email/identity change Email changes must go through Firebase (verifyBeforeUpdateEmail) and then sync to users.email. sync_email.php behavior maps to the /api/auth/session sync. P1
A-10 Last seen Update users.last_seen at most every 5 minutes per user (throttle as today). P2

5.3 Admin panel

All under /admin, lazy-loaded, protected by A-06/A-07, every mutation writes to audit_logs.

ID Module Legacy Requirements Pri
AD-01 Dashboard dashboard.php KPI cards; Order pipeline; Quick actions; Orders & total sales (last 7 days); Order-status doughnut; Top-selling products; Profit analytics (uses stockadmin_variant purchase rates); Customer analytics; Sales forecast (next 7 days); Sales heatmap (by weekday and hour); Recent activity; Coupon analytics; Abandoned-cart analytics. P1
AD-02 Products products.php, product_form.php, bulk_products.php, bulk_ids.php, product_lookup.php List with search/filter/pagination; create/edit with multi-image upload (primary + sort_order), variants grid (size × color: qty, price, old price, SKU), featured/status toggles, department/category/brand/gender; bulk actions; product lookup. Slug auto-generation (slugify). P0
AD-03 Catalog taxonomy categories, brands, departments, sizes, colors (+ *_form) CRUD with image upload for categories and brands; unique slugs/names; delete handler with dependency checks (delete_handler.php). Size sort order configurable (size_sort_order). P0
AD-04 Orders orders.php, order_detail.php, order_form.php, view-receipt.php List + search + filters; detail view; create offline (in-store) orders (order_type='offline'); lifecycle actions: verify/reject payment, mark paid, confirm, ship, deliver, cancel, reactivate, return [Confirm which are live; several appear commented out in order_detail.php]; admin note; view payment receipt (private file, served only to admins); send payment-reminder email; send push notification to the customer; transfer order to another user; NCM order id + status tracking. Stock restoration on cancel/return (restoreOrderStock). P0
AD-05 Users users.php, user_detail.php Search/list; detail with orders, last login/seen; role changes (superadmin); soft delete/restore. P1
AD-06 Coupons coupons.php, coupon_get.php CRUD, all fields in coupons, usage counts, scope selector (category/brand/product picker). P0
AD-07 Hero slides hero_slides.php, hero_slide_get.php CRUD, image upload, order, active toggle. P1
AD-08 Reviews moderation reviews.php List by status; verify/reject/delete; recalculates product rating aggregates. P1
AD-09 Stock purchasing admin_stock*.php Log supplier purchases: product name/slug, supplier, bill number, variants (size, color, qty, rate; subtotal generated column). Used for profit analytics. P1
AD-10 Audit logs audit_logs.php, search_audit_logs.php Filter by user/action/entity/date; read-only. P1
AD-11 Settings settings.php Manage site_settings keys (list in 6.3): site name, announcement bar (+enabled), maintenance mode, shipping cost, free-shipping threshold (+enabled), shop contact info, socials, SMTP, emails on/off, admin access code, size sort order. P0
AD-12 Push/notifications send_push_handler.php Send bulk or personalized notifications (writes notifications / user_notifications, dispatches web push). P1

5.4 Integrations

ID Integration Requirement Pri
I-01 NCM (Nepal Can Move) Keep POST /api/ncm/webhook (idempotent, matches orders.ncm_order_id, updates ncm_status, ncm_last_sync only when changed, supports order_id and order_ids[], test payload). Keep fetchNcmOrder for admin "fetch status". Token from env. Add shared-secret or IP verification on the webhook [Confirm NCM supports it]. P1
I-02 Email (PHPMailer/Gmail SMTP) Templates preserved: order confirmation, delete-account code, contact, payment reminder. Password-reset email is now Firebase's. Global emails_enabled flag respected. Move sending to a queue-lite pattern (send after response flush) so checkout is not blocked by SMTP latency. P0
I-03 Web Push (VAPID) Keep minishlink/web-push; subscription stored in push_subscriptions; expire dead endpoints (HTTP 404/410). P1
I-04 WhatsApp Deep links for ordering/sharing using shop_whatsapp setting. P0

6. Data model

6.1 Principle

Keep database.sql as the baseline (opencodegrip1), convert it to versioned migrations, apply only additive changes, and keep names, types and enums. All 26 tables are retained:

users, audit_logs, categories, brands, departments, products, product_images, sizes, colors, product_stock, cart, wishlist, orders, order_items, coupons, coupon_usage, site_settings, password_resets, rate_limits, stockadmin, stockadmin_variant, push_subscriptions, hero_slides, reviews, notifications, user_notifications

6.2 Required schema changes (migration 0002)

Table Change Reason
users ADD firebase_uid VARCHAR(128) NULL UNIQUE, email_verified TINYINT(1) NOT NULL DEFAULT 0, auth_provider VARCHAR(30) NULL Map Firebase identity to the existing user row
users ADD avatar VARCHAR(500) NULL in the baseline CREATE TABLE In database.sql the ALTER TABLE users ADD avatar runs before CREATE TABLE users (would fail on a fresh install); the column exists in production, so fold it into the baseline.
users password and google_id: keep nullable and unused for one release, then drop in 0003 Enables rollback; Firebase now owns credentials
orders ADD ncm_order_id VARCHAR(100), ncm_status VARCHAR(100), ncm_location_event VARCHAR(50), ncm_current_location VARCHAR(255), ncm_last_sync DATETIME (+ index on ncm_order_id) The code uses these columns but they only exist as an ALTER inside an HTML comment in includes/ncm.php, so a fresh install is missing them.
orders Widen enums as already drafted in comments: payment_method += 'instore', payment_gateway += 'cash' Needed for offline orders [Confirm production state]
orders ADD INDEX (user_id, created_at), (status), (customer_phone, order_number) Account list, admin filters, tracking lookups
reviews ADD CHECK (rating BETWEEN 1 AND 5) The constraint is currently a stray comment
products Rating aggregates maintained by the API in the same transaction as review moderation (or enable the commented-out trigger, but choose one approach only) Avoid drift
password_resets Retain for type='account_delete' only; password_reset type deprecated Firebase handles reset
rate_limits Unchanged; extended usage (contact form, tracking lookups, coupon apply, admin code) Brute-force protection

Everything else stays byte-for-byte identical. Character set stays utf8mb4_unicode_ci, engine InnoDB.

6.3 site_settings keys (unchanged)

site_name, announcement_bar, announcement_bar_enabled, maintenance_mode, shipping_cost, free_shipping_threshold, free_shipping_threshold_enabled, shop_email, shop_phone, shop_address, shop_whatsapp, social_facebook, social_twitter, social_instagram, social_youtube, smtp_user, smtp_pass, emails_enabled, admin_access_code, size_sort_order

Requirements:

6.4 Key relationships to preserve

6.5 Seed data

database-seed.sql (settings, sizes, colors) becomes seeds/0001_defaults.sql. Add a dev-only sample catalog seed for demos and Playwright tests.


7. API specification (v1)

Base path /api/v1. Auth column: P public, U Firebase user, A admin (Firebase + role + admin token), S superadmin.

7.1 Public / catalog

Method & path Auth Purpose
GET /home P hero slides, categories, best sellers, featured (cached 5 min)
GET /taxonomy P departments, categories, brands, sizes, colors
GET /products P filters: department, category, brand, gender, featured, instock, onsale, search, sort, page
GET /products/{slug} P product + images + variants + rating summary + related
GET /products/{slug}/reviews P paginated reviews (public statuses only)
GET /search?q= P live search (max 10)
GET /settings/public P non-secret settings
POST /contact P rate-limited contact email
POST /orders/track P {order_number, phone}; rate-limited; returns limited fields
POST /ncm/webhook P (secret) NCM status updates

7.2 Auth & account

Method & path Auth Purpose
POST /auth/session U Verify ID token, upsert user by firebase_uid/email, return {user, role, admin_verified:false}
POST /admin/verify A-lite Submit access code → returns admin token
GET /me · PATCH /me · POST /me/avatar U profile
POST /me/delete/request · POST /me/delete/confirm U email code flow, soft delete + Firebase user removal

7.3 Shopping

Method & path Auth Purpose
GET /cart U items with live price/stock, subtotal
POST /cart · PATCH /cart/{id} · DELETE /cart/{id} U add/update/remove
POST /coupons/validate U body {code} → discount preview (stateless; client stores applied code)
GET/POST/DELETE /wishlist (+ /wishlist/toggle) U wishlist ops and product hydration
POST /orders U multipart: checkout payload + optional payment_proof
GET /orders · GET /orders/{orderNumber} U own orders (owner check)
POST /orders/{id}/cancel U pending-only, owner-only
POST /reviews · PATCH /reviews/{id} · DELETE /reviews/{id} U verified-purchaser rules
GET /notifications · POST /notifications/read U list / mark read
POST /push/subscribe · DELETE /push/subscribe U web push

7.4 Admin (all A, all audited)

/admin/dashboard, /admin/products (+ bulk, lookup, image upload/reorder), /admin/{categories|brands|departments|sizes|colors}, /admin/orders (+ /{id}/status, /{id}/payment, /{id}/note, /{id}/transfer, /{id}/ncm, /{id}/reminder, /{id}/push, /{id}/receipt), /admin/users, /admin/coupons, /admin/hero-slides, /admin/reviews, /admin/stock-purchases, /admin/audit-logs, /admin/settings, /admin/notifications/send.

7.5 Cross-cutting API rules

  1. Never trust client money. Line prices, discount and shipping are recomputed server-side from the DB at order time.
  2. Validate token every request: signature, aud = Firebase project id, iss, exp; cache Google public keys per Cache-Control.
  3. Stateless coupon: the old $_SESSION['applied_coupon'] becomes a client-held code that is revalidated at POST /orders.
  4. Pagination uses page + per_page and returns meta: {total, pages}.
  5. Idempotency: POST /orders accepts an Idempotency-Key header to prevent double orders on flaky mobile networks.
  6. Uploads: max 2 MB; allowed real MIME jpeg/png/webp/gif (checked with finfo, not extension); re-encoded to WebP; random or order-based filenames; payment_receipts/ never publicly served.
  7. Errors: consistent HTTP codes (401 unauthenticated, 403 forbidden, 404, 409 stock conflict, 422 validation, 429 rate limit).
  8. Timezone: DB session set to +05:45 for NOW() semantics as today, or store UTC and convert in the API (decide once; see open question Q6).

7.6 Migrating existing users to Firebase

  1. Export users (email, full_name, password hash, google_id).
  2. Use Firebase Admin importUsers:
  3. Write returned UIDs into users.firebase_uid. Users whose email cannot be matched are linked on first POST /auth/session by verified email.
  4. Keep a rollback window with the old PHP site behind a maintenance banner.

8. Business rules (must not change)

8.1 Orders

8.2 Shipping

shipping = 0 when free_shipping_threshold_enabled and (cart_total ≥ threshold or total_after_discount ≥ threshold); otherwise shipping_cost. The current default values are shipping_cost = 1000 and free_shipping_threshold = 500 in code (both DB-overridable). [Confirm production values; a 500 threshold with 1000 shipping looks like a placeholder.]

8.3 Coupons

Discount base is the eligible subtotal only (all / category / brand / product scope). Percentage discounts must be computed as integer rupees (the current code rounds to 2 decimals into an INT column). Choose ROUND() to nearest rupee and apply consistently client and server. A coupon cannot exceed the eligible amount.

8.4 Reviews

Verified purchaser only (has a non-cancelled purchase of that product). Starts pending; only verified reviews count toward rating aggregates.

8.5 Privacy & retention

Soft-deleted users keep order history for accounting; personal profile fields (phone/address/avatar) are cleared on deletion [Confirm policy]. Payment receipts are private.


9. Design system (Shopify-style black & white)

9.1 Principles

  1. Monochrome first: black, white and neutrals carry the whole UI. Color appears only for semantic state (success, warning, danger, info) and in the brand logo.
  2. Content over chrome: thin 1 px borders, minimal shadows, generous whitespace, product imagery is the hero.
  3. Quiet motion: 150-200 ms opacity/transform transitions; respects prefers-reduced-motion.
  4. Consistency: the admin panel uses the same tokens and components as the storefront, just denser.

9.2 Tokens (reuse existing values)

Defined in styles/tokens.css, exposed to Tailwind via CSS variables.

Token Light Dark
--primary / --accent #0A0A0A #F5F5F5
--primary-light #525252 #A3A3A3
--accent-hover #262626 #D4D4D4
--accent-soft #F5F5F5 #1A1A1A
--light (page bg) #FAFAFA #0A0A0A
--white (surface) #FFFFFF #111111
--dark (text) #0A0A0A #F5F5F5
--gray #A3A3A3 #666666
--gray-light (borders) #E5E5E5 #262626
--success / soft #16A34A / #DCFCE7 soft #14532D
--danger / soft #DC2626 / #FEE2E2 soft #7F1D1D
--warning / soft #D97706 / #FEF3C7 soft #78350F
--info / soft #2563EB / #DBEAFE soft #1E3A5F
Radius 8 px (--radius), 12 px (--radius-lg) same
Font Inter, system fallback stack same
Layout container max-width 1200 px, 20 px gutter same
Nav 64 px desktop / 56 px below 992 px same

Primary buttons are solid black (light theme) / solid near-white (dark theme) with inverted text. Secondary buttons are 1 px bordered. Focus ring: 2 px --accent with 2 px offset.

9.3 Theme toggle requirements

ID Requirement
T-01 Three modes: Light, Dark, System. Header shows a compact sun/moon toggle; the account/profile page and admin header offer the full 3-way control.
T-02 Theme is applied via <html data-theme="dark"> (light = attribute absent), as the current site does, so tokens map 1:1.
T-03 Persist in localStorage key theme with values `light
T-04 An inline script in index.html sets data-theme before React mounts to prevent a flash of the wrong theme.
T-05 "System" follows prefers-color-scheme live via a change listener.
T-06 <meta name="theme-color"> updates on toggle (#FFFFFF light / #0A0A0A dark).
T-07 Third-party bits follow the theme: Chart.js colors (admin), toasts, skeletons, Firebase-hosted action pages if customised, emails stay light.
T-08 Optional (P2): save theme to the user profile so it follows them across devices.
T-09 All text meets WCAG AA contrast in both themes (verify grays: #A3A3A3 on white fails for body text; use --primary-light for text, --gray for decorative only).

9.4 Logo handling

Existing files in the zip (assets/img/), verified visually: a bold black-and-white "GT" mark and "GRIPFIT" wordmark with a red claw/FIT accent.

Context Light theme Dark theme Source file (size)
Desktop / tablet (≥ 768 px), full wordmark grip.png (dark wordmark) grip-dark.png (white wordmark) 512×104 RGBA
Mobile (< 768 px), compact mark grip-icon.png (dark mark) grip-icon-dark.png (white mark) 181×141 RGBA

Requirements:

9.5 Component inventory

Header (announcement bar, logo, search with live results, nav, wishlist, cart with count badge, notification bell with unread dot, account menu, theme toggle, mobile menu), Footer, Product card (image, brand, name, price/old price, on-sale and out-of-stock badges, wishlist heart, quick add), Product gallery + lightbox, Variant selectors (size chips, color chips), Quantity stepper, Mini-cart drawer, Filter sidebar/sheet (mobile bottom-sheet), Pagination, Breadcrumbs, Rating stars, Review card, Status badges (order/payment), Address picker (cascading selects), Toast, Modal, Confirm dialog, Skeleton loaders (cards, table rows, forms, order cards, summary, profile, product detail: parity with renderSkeleton*), Empty states, Admin data table (sortable, filterable, bulk select), Stat card, Chart card, Image uploader (drag-drop, reorder, preview).

9.6 Responsive & accessibility


10. Routes

10.1 Storefront

Path Page
/ Home
/shop Listing (filters in query string)
/shop/:slug Product
/cart · /checkout Cart · Checkout (auth)
/order-confirmation/:orderNumber Confirmation (auth, owner)
/track-order-status Tracking
/wishlist · /account Wishlist · Account (auth)
/login · /register · /forgot-password · /reset-password Auth pages
/about /contact /faq /shipping /returns /privacy /terms Static
/status/maintenance · /status/coming-soon · * System / 404

10.2 Legacy URL compatibility

The old site already 301-redirects *.php to clean URLs and product URLs are /shop/{slug}. Keep every clean URL identical so SEO, WhatsApp links and printed material keep working. Keep 301s for /product.php?slug= and other .php URLs.

10.3 Admin

/admin (dashboard), /admin/verify, /admin/products, /admin/products/new, /admin/products/:id, /admin/{categories|brands|departments|sizes|colors}, /admin/orders, /admin/orders/new, /admin/orders/:id, /admin/users, /admin/users/:id, /admin/coupons, /admin/hero-slides, /admin/reviews, /admin/stock, /admin/audit-logs, /admin/settings.


11. Security requirements

  1. Rotate secrets found in the upload. The zip contains a real .env and the .git history. The .env holds DB credentials, SMTP password, admin access code, NCM token, Google client secret and VAPID private key, and one SMTP app password is pasted in plain text inside a comment. Treat all of them as exposed: rotate every one before launch, delete them from git history, and never ship .env, .git, .kilo, graphify-out, cache/, test_db.php or css-optimization-test.html to production.
  2. Firebase config split: the web firebaseConfig (apiKey, projectId...) is public by design. The service-account JSON is private and lives only on the PHP server, outside the web root. Restrict the Firebase API key by HTTP referrer and enable authorized domains only.
  3. Prepared statements everywhere; no string-built SQL.
  4. AuthZ on every route, including owner checks on orders, cart, wishlist, reviews, and addresses.
  5. Rate limits (existing table): login attempts are now Firebase's; keep server limits for register-sync, contact, tracking, coupon validation, admin-code (5 tries / 15 min), reset/delete codes.
  6. Admin hardening: access-code second factor, short-lived admin token, audit trail on all writes, optional IP allow-list.
  7. Uploads: as in 7.5 #6; upload directories get .htaccess (no PHP execution) as today; receipts served only through an authorised endpoint.
  8. Headers: HTTPS redirect (existing), HSTS, X-Content-Type-Options, Referrer-Policy, a Content-Security-Policy that allows Firebase and Google Fonts only as needed.
  9. XSS: React escapes by default; sanitize any admin-authored rich text (product description, FAQ) with DOMPurify before dangerouslySetInnerHTML.
  10. PII: phone/address minimisation in logs; audit-log details must not store secrets.

12. Non-functional requirements

12.1 Performance: lightweight build, fast loading, lazy images

The storefront must feel instant on a mid-range Android phone over 4G. Everything below is a requirement, not a suggestion, and the budgets are enforced in CI.

12.1.1 Performance budgets (enforced by size-limit and Lighthouse CI)

Item Budget
Initial JS, storefront home (gzip) ≤ 120 KB
Any single lazy route chunk (gzip) ≤ 60 KB (product page ≤ 80 KB)
Initial CSS (gzip) ≤ 20 KB (critical CSS inlined)
Fonts ≤ 60 KB total on first load
Product card thumbnail ≤ 25 KB each (WebP, ~400 px wide)
Product page main image ≤ 90 KB (WebP, ~800 px wide)
LCP / CLS / INP (mobile, 4G) ≤ 2.5 s / ≤ 0.05 / ≤ 200 ms
Time to first API data (home) ≤ 400 ms server time (cached)
Lighthouse mobile Performance ≥ 90 on home, shop, product

12.1.2 Lazy image loading (all images)

ID Requirement
IMG-01 A single <Img /> component wraps every image and applies the rules below, so behavior is identical across storefront and admin.
IMG-02 Below-the-fold images use native lazy loading: loading="lazy" and decoding="async" (product grids, related products, reviews, category tiles, footer, admin thumbnails).
IMG-03 Above-the-fold / LCP images are never lazy: first hero slide, logo, and the main product image use loading="eager" and fetchpriority="high", with a <link rel="preload" as="image"> for the hero. Only the first product-grid row (on mobile: first 2-4 cards) is eager.
IMG-04 Every image has explicit width and height (or CSS aspect-ratio) so nothing shifts when it loads (CLS target ≤ 0.05).
IMG-05 Responsive images: srcset + sizes with three widths (400 / 800 / 1200), so phones never download desktop images.
IMG-06 Modern formats: WebP is required (current site already converts to WebP); AVIF is optional with WebP fallback via <picture>.
IMG-07 Placeholder while loading: a neutral skeleton block using theme tokens (--accent-soft) or an optional tiny blurred placeholder (LQIP, ≤ 500 bytes, stored per image). Images fade in (150 ms opacity) once loaded.
IMG-08 Hero slider: only slide 1 loads immediately; the remaining slides load after first paint (idle) or when the user swipes/advances. Autoplay pauses when the tab is hidden.
IMG-09 Product gallery: only the main image is eager; thumbnails are lazy; lightbox full-size images load on open and the next image is prefetched.
IMG-10 Below-the-fold sections (Best Sellers, Featured, reviews, related products) render their images only when scrolled near the viewport. Use native lazy loading, plus an IntersectionObserver with a ~300 px root margin for carousels and any content that is not natively lazy.
IMG-11 Broken-image handling: a lightweight fallback placeholder (no extra network request, inline SVG) on error.
IMG-12 Admin previews and payment receipts are lazy too, and the receipt viewer loads the full image on click only.
IMG-13 Do not lazy-load: the logo (all four variants must show instantly, see 9.4 and use eager on the visible variant only), announcement bar, and anything visible in the first viewport.

Server-side image pipeline (new requirement, the current uploadImage() converts to WebP but does not resize): on upload, the API generates 400 / 800 / 1200 px WebP variants (quality ~75-80, strip metadata), stores the paths, and returns a srcset-ready structure. Existing images are backfilled by a one-off script. Uploaded originals over 2 MB are still rejected (existing rule).

12.1.3 JavaScript and CSS weight

  1. Route-level code splitting with React.lazy for every page; the admin area is a separate chunk tree that shoppers never download.
  2. Firebase Auth is lazy-loaded. Use the modular SDK (firebase/auth only, no other Firebase products) and load it with dynamic import(). It loads on /login, /register, /forgot-password, or on first page load only if a "was signed in" hint exists in localStorage, so anonymous shoppers never pay for it. Google sign-in code loads only when the Google button is pressed.
  3. Heavy libraries load on demand: Chart.js only on /admin dashboard; image cropper/uploader only in admin forms; DOMPurify only where rich HTML is rendered.
  4. Small runtime: React 18 + React Router + TanStack Query is the default. If the initial-JS budget cannot be met, swap React for Preact via preact/compat (about 30 KB smaller, same code). Decide with a bundle measurement in Phase 0.
  5. CSS: Tailwind with purge (only used classes shipped), tokens in CSS variables, critical CSS for the header/hero inlined, the rest loaded non-blocking. No Font Awesome, no unused CSS frameworks.
  6. Fonts: self-host Inter as a variable WOFF2, Latin subset only, font-display: swap, preload the primary file; or fall back to the system font stack if the 60 KB font budget is at risk. No Google Fonts request.
  7. Build: Vite production build with minification, tree-shaking, manualChunks for vendor splitting, ES2020 target, no source maps in production, Brotli + gzip pre-compressed assets, content-hashed filenames.
  8. Prefetching: prefetch the product chunk and its API data on link hover/touch-start and when a product card enters the viewport; prefetch /cart and /checkout chunks after add-to-cart.
  9. Third-party scripts: none on first load (no analytics, chat or pixel loaded before the page is interactive; if added later, load them on idle).

12.1.4 API and network

12.1.5 Rendering and perceived speed

12.1.6 PWA caching (vite-plugin-pwa / Workbox)

12.1.7 Monitoring

12.2 SEO and social sharing (biggest risk of going SPA)

The current PHP site delivers full HTML to crawlers and to WhatsApp/Facebook link previews (og: tags, JSON-LD, sitemap.xml). A client-rendered SPA would lose this. Requirements:

12.3 Reliability and operations

12.4 Compatibility

Last 2 versions of Chrome, Safari (iOS 15+), Firefox, Edge; Android Chrome 100+. Graceful degradation for Web Push on iOS Safari (PWA-installed only).

12.5 Environment variables

Frontend (web/.env): VITE_API_BASE, VITE_FIREBASE_API_KEY, VITE_FIREBASE_AUTH_DOMAIN, VITE_FIREBASE_PROJECT_ID, VITE_FIREBASE_APP_ID, VITE_VAPID_PUBLIC_KEY. Backend (api/.env): DB_HOST, DB_USER, DB_PASS, DB_NAME, FIREBASE_PROJECT_ID, FIREBASE_CREDENTIALS_PATH, SMTP_*, ADMIN_ACCESS_CODE, ADMIN_TOKEN_SECRET, NCM_API_TOKEN, NCM_WEBHOOK_SECRET, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT, APP_ENV, APP_DEBUG, ALLOWED_ORIGINS.


13. Delivery plan

Rough estimate for 2 developers (1 frontend-leaning, 1 PHP/backend-leaning). Adjust after the discovery spike.

Phase Scope Duration
0. Foundations Repo, Vite/TS/Tailwind, tokens + theme toggle + <Logo>, UI kit, Firebase project(s), PHP front controller + auth middleware, migrations 0001/0002, CI. Spikes: bcrypt import into Firebase, SEO shim, React vs Preact bundle measurement, lazy Firebase loading. Set up performance budgets in CI from day one. 1-1.5 wks
1. Catalog Home, shop, filters, search, product page, taxonomy endpoints, SEO shim, sitemap, <Img /> lazy component and server-side image resizing (+ backfill script). 2-2.5 wks
2. Identity & purchase Register/login/Google/reset, session sync, cart, coupons, checkout, order placement + receipt upload, confirmation, emails. 2.5-3 wks
3. Customer account Profile/avatar, orders + cancel, wishlist, reviews, notifications, push opt-in, delete account, tracking. 1.5-2 wks
4. Admin Verify gate, products/variants/images, taxonomy, orders (all actions), coupons, users, settings, slides, reviews, stock, audit logs. 3-4 wks
5. Dashboard & integrations Analytics dashboard, NCM webhook + fetch, push sending, PWA polish. 1.5 wks
6. Migration & launch User import to Firebase, data verification, load/UAT, security review, secret rotation, cut-over + rollback plan. 1.5 wks
Total ≈ 13-16 weeks

13.1 Cut-over plan

  1. Stand up staging with a copy of production data and imported users.
  2. Freeze admin writes for a short window; run migration 0002, user import, and smoke tests.
  3. Switch DNS/.htaccess to the new build; keep the old PHP app in a read-only folder for rollback for 2 weeks.
  4. Monitor errors, checkout completions, and email delivery for the first 72 hours.

14. Optional enhancements (P2, not in parity scope)


15. Testing and acceptance

Level Coverage
Unit Price/discount/shipping calculators (client and API), coupon eligibility, order-number generator, slugify
API integration Auth middleware (expired/forged/deleted-user tokens), order transaction (stock race with two concurrent buyers), cancel + stock restore, NCM webhook idempotency, upload validation
E2E (Playwright) Register → add to cart → coupon → prepaid checkout with receipt → confirmation → admin verifies payment → ships → customer sees status; theme toggle persistence with no flash; logo swap on viewport + theme; guest tracking
Visual Screenshot baselines in light and dark for key pages
Accessibility axe-core on every route, keyboard-only checkout
Performance Lighthouse CI budgets (12.1.1); Playwright test asserts below-the-fold images are not requested until scrolled near; test on throttled "Slow 4G + 4x CPU" profile; CLS check on shop and product pages
Migration Row-count and checksum comparisons per table; sample of 50 users sign-in test

Definition of done for launch: all P0/P1 acceptance tests green; no critical/high security findings; all secrets rotated; rollback rehearsed.


16. Risks

Risk Impact Mitigation
SPA hurts SEO and link previews Traffic and WhatsApp sharing loss Crawler shim (12.2), sitemap, early validation in Phase 0
Firebase cannot import $2y$ bcrypt hashes Users forced to reset passwords Spike in Phase 0; lazy-migration fallback with clear messaging
Stock overselling on concurrent checkouts Cancelled orders Row locking / conditional update + concurrency test
Schema drift (columns in comments only, ALTER before CREATE) Broken fresh installs and staging Baseline migration reconciled against production SHOW CREATE TABLE output first
Exposed credentials in the shared archive Account takeover, spam email, data leak Rotate all secrets immediately (section 11.1)
Admin functions partly commented out in source Missing features at launch Discovery pass on order_detail.php, settings.php, users.php before Phase 4
Shared-hosting limits (PHP memory, cron, SMTP) Slow uploads, failed emails Test on target host in Phase 0; consider transactional email provider later
Firebase outage No login/sign-up (browsing still works) Acceptable; show clear status message
Bundle growth over time Slower loads CI size budgets, dependency policy, bundle report on every PR
Lazy-loading the wrong image Worse LCP Only above-the-fold images eager with fetchpriority="high"; Lighthouse CI catches regressions

17. Open questions

  1. Hosting target: shared cPanel, VPS, or a mix (static on CDN + PHP elsewhere)? This decides CORS, deployment and SEO shim details.
  2. TypeScript: OK to use TypeScript (recommended) or JavaScript only?
  3. Guest checkout / guest cart: keep "login required to add to cart" or promote the P2 guest cart to P1?
  4. Firebase sign-in methods: email/password + Google only, or also phone (SMS) OTP, which is popular in Nepal?
  5. Logos: are the four files in assets/img/ (grip, grip-dark, grip-icon, grip-icon-dark) the final ones, or will you supply new files? Can we get SVG or @2x versions?
  6. Timezone storage: keep local +05:45 timestamps in DB (as now) or normalise to UTC?
  7. Superadmin-only actions and whether admins can bypass maintenance mode.
  8. Which order lifecycle actions are live in production (several are commented out in order_detail.php)?
  9. Shipping values: real production shipping_cost and free_shipping_threshold settings.
  10. Account deletion policy: what personal data must be erased vs retained for accounting?
  11. Review eligibility: does a verified purchase require the order to be delivered?

Appendix A. Legacy file → new module map

Legacy New
index.php, shop.php, product.php, search_ajax.php features/catalog/*, GET /home /products /search
cart.php, cart_action.php, coupon_action.php, assets/js/cart.js features/cart/*, cart & coupon endpoints
checkout.php, order_confirmation.php, order_action.php, upload-payment.php features/checkout/*, features/orders/*, POST /orders
wishlist*.php, review_action.php features/wishlist, features/reviews
account.php, account_action.php, sync_email.php features/account/*, /me/*, /auth/session
login/register/forgot_password/reset_password/auth.php, includes/google_auth.php Firebase Auth UI + /auth/session (custom OAuth and OTP-reset removed)
includes/security.php (rate limits) Support/RateLimiter.php (same table)
includes/audit.php Support/Audit.php (same table)
includes/mailer.php Services/Mailer.php (same templates, re-skinned)
includes/ncm.php, api/ncm-webhook.php Services/Ncm.php, POST /ncm/webhook
ordernotification/*, api/notifications.php, api/mark_read.php, service-worker.js Services/Push.php, notification endpoints, vite-plugin-pwa
admin/* web/src/admin/* + /admin/* API
includes/header.php, footer.php, assets/css/style.css Layout components + tokens.css
assets/data/nepal-address.json Static import in lib/nepalAddress.ts
cache/*.cache API cache layer (file/APCu) + HTTP cache headers
.htaccess SPA fallback + API routing + crawler shim + security headers
database.sql, database-seed.sql migrations/0001_baseline.sql, 0002_firebase.sql, seeds/
test_db.php, css-optimization-test.html, graphify-out/, .kilo/, .git/, .env Do not deploy

Appendix B. Order lifecycle (reference)

online / whatsapp:   pending(unpaid) ─▶ processing ─▶ shipped ─▶ delivered
                          └─▶ cancelled (customer if pending; admin any time; stock restored)
online / prepaid:    pending(verifying) ─▶ [admin verify] ─▶ processing(paid) ─▶ shipped ─▶ delivered
                          └─ reject payment ─▶ failed ─▶ cancelled
paid + cancelled ─▶ payment_status = refunded
offline (in-store):  created by admin, payment_method instore, gateway cash/esewa/bank

[Confirm exact transitions against admin/orders.php and order_detail.php.]