| 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.
A server-rendered PHP 8 / MySQLi / session-based store with:
| 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 |
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:
mysqli with prepared statements and returns JSON.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.
.htaccess, Cloudflare X-Forwarded-Proto handling, and Sundarharaicha, Morang shop address suggest this). The frontend deploys as static files; the API deploys as PHP.Rs. currency, Asia/Kathmandu timezone (UTC+05:45).assets/img/ (not assets/image/). Section 9.3 maps them. If you supply new versions, only filenames need to match.| 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 |
| 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.
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)
| 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 |
mysqli with mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT), prepared statements only.phpmailer/phpmailer, minishlink/web-push; add kreait/firebase-php (verification + user management) or firebase/php-jwt + Google JWKS caching if you want fewer dependencies.api/index.php front controller, or a micro-framework such as Slim). No ORM; keep SQL close to today's queries so behavior stays identical.{ "success": true, "data": {...}, "meta": {...} } / { "success": false, "message": "...", "errors": {...} } (extends today's {success, message} convention).multipart/form-data for image uploads.rate_limits table), Firebase auth middleware, role middleware (customer, admin, superadmin), admin-access-code middleware, audit-log helper (existing logAudit).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
Priority: P0 = launch blocker, P1 = required for parity, ships in same release if possible, P2 = post-launch/optional.
| 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 |
| 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 |
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 |
| 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 | Deep links for ordering/sharing using shop_whatsapp setting. |
P0 |
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
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.
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:
smtp_pass and admin_access_code are write-only in the admin UI (never returned to the browser). Prefer storing them only in server env; DB values remain as an override for parity.products → departments/categories/brands (ON DELETE SET NULL), product_images and product_stock (CASCADE).product_stock unique on (product_id, size_id, color_id); all prices live on the variant; a product's displayed price is MIN(price) across in-stock variants.cart unique on (user_id, stock_id, product_id).order_items are snapshots (product_name, price, size, color) with nullable product_id/stock_id (SET NULL).coupon_usage per (coupon, user, order); coupons.used_count incremented in the order transaction.reviews unique on (user_id, product_id, order_item_id).database-seed.sql (settings, sizes, colors) becomes seeds/0001_defaults.sql. Add a dev-only sample catalog seed for demos and Playwright tests.
Base path /api/v1. Auth column: P public, U Firebase user, A admin (Firebase + role + admin token), S superadmin.
| 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 |
| 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 |
| 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 |
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.
aud = Firebase project id, iss, exp; cache Google public keys per Cache-Control.$_SESSION['applied_coupon'] becomes a client-held code that is revalidated at POST /orders.page + per_page and returns meta: {total, pages}.POST /orders accepts an Idempotency-Key header to prevent double orders on flaky mobile networks.jpeg/png/webp/gif (checked with finfo, not extension); re-encoded to WebP; random or order-based filenames; payment_receipts/ never publicly served.+05:45 for NOW() semantics as today, or store UTC and convert in the API (decide once; see open question Q6).users (email, full_name, password hash, google_id).importUsers:
PASSWORD_DEFAULT = bcrypt $2y$). Spike required to confirm Firebase accepts $2y$ hashes as-is (some setups need $2a$); if not, fall back to lazy migration: first login shows "set a new password" via reset email.users.firebase_uid. Users whose email cannot be matched are linked on first POST /auth/session by verified email.pending → processing → shipped → delivered, plus cancelled.unpaid, verifying, partial_paid, paid, failed, refunded.online, offline (admin-created in-store).whatsapp (unpaid until confirmed), prepaid (gateway esewa | bank_transfer, screenshot required, starts as verifying).yymmdd + 8 uppercase hex chars, unique.pending, only owner; restores stock unless already paid; if paid → payment status becomes refunded.UPDATE ... WHERE quantity >= ? so concurrent buyers cannot oversell; restored on cancel/return.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.]
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.
Verified purchaser only (has a non-cancelled purchase of that product). Starts pending; only verified reviews count toward rating aggregates.
Soft-deleted users keep order history for accounting; personal profile fields (phone/address/avatar) are cleared on deletion [Confirm policy]. Payment receipts are private.
prefers-reduced-motion.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.
| 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). |
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:
<Logo /> component renders all four <img> variants and swaps them with CSS only ([data-theme] + media query → display), so there is no layout shift and no flash when the theme or viewport changes.width/height attributes and alt="GripFit".loading="lazy" or are swapped via CSS content/<picture> with media queries so the browser downloads one file, not four. Convert the logos to SVG (or optimized WebP under 10 KB each) to keep them tiny.og:image.assets/icons/ (16/32/180/192/512) are reused; add a maskable 512 icon and a dark-mode favicon variant (favicon-light-32.png already exists).--danger).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).
aria-live for cart/toast updates, semantic landmarks, alt text on all images, form errors linked with aria-describedby.| 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 |
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.
/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.
.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.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..htaccess (no PHP execution) as today; receipts served only through an authorised endpoint.X-Content-Type-Options, Referrer-Policy, a Content-Security-Policy that allows Firebase and Google Fonts only as needed.dangerouslySetInnerHTML.details must not store secrets.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.
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 |
| 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).
React.lazy for every page; the admin area is a separate chunk tree that shoppers never download.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./admin dashboard; image cropper/uploader only in admin forms; DOMPurify only where rich HTML is rendered.preact/compat (about 30 KB smaller, same code). Decide with a bundle measurement in Phase 0.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.manualChunks for vendor splitting, ES2020 target, no source maps in production, Brotli + gzip pre-compressed assets, content-hashed filenames./cart and /checkout chunks after add-to-cart.GET /home returns everything the home page needs in one call. List endpoints return card-sized payloads only (id, slug, name, brand, price, old price, primary image, in-stock flag), never full product objects or all variants. Full detail loads only on the product page.ETag/Cache-Control on public endpoints (/home 5 min, /taxonomy 10 min, /products 60 s with stale-while-revalidate), replacing the file-based cache/*.cache. Server-side cache via APCu or file cache behind it.Cache-Control: immutable (1 year) for hashed JS/CSS/fonts and for product images./uploads images through a CDN (Cloudflare is already implied by the X-Forwarded-Proto handling in .htaccess); enable image caching rules there.staleTime (catalog 60 s) so navigating back to the shop or product pages is instant, plus request de-duplication. Debounce live search (250 ms) and cancel in-flight requests.per_page of 12-24 with numbered pages (or "Load more"); never load the whole catalog. Long admin tables paginate server-side.idx_related plus additions in 6.2); avoid N+1 (batch-fetch variants and images for a page of products, as product.php already does for related items); EXPLAIN review for shop, search, dashboard queries.renderSkeleton* helpers), sized like the real content to avoid layout shift.content-visibility: auto on below-the-fold sections; long lists use windowing only where needed (admin tables).transform/opacity only and respect prefers-reduced-motion.index.html must be under 1 KB and blocking-free; theme switching only swaps CSS variables (no re-render of the tree).vite-plugin-pwa / Workbox)CacheFirst with expiry (≈ 60 days, max ≈ 200 entries); catalog API uses StaleWhileRevalidate; cart, checkout, account and admin APIs are NetworkOnly.web-vitals library, sent to a lightweight /api/v1/vitals endpoint or a free analytics tool, loaded on idle.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:
.htaccess rule detects crawler/preview user agents (Googlebot, facebookexternalhit, WhatsApp, Twitterbot, etc.) for /shop/*, /shop, / and routes them to a small PHP script that returns HTML with the correct <title>, description, canonical, Open Graph image and Product JSON-LD from the DB. Humans get the SPA.sitemap.xml generated from active products/categories/brands; keep robots.txt.react-helmet-async for per-route titles/meta in the SPA.vite-plugin-ssr/Vike or a Node SSR layer; not assumed here.)/api/v1/health; structured error logging; slow-query log review.site_settings toggle honored by both API and SPA.Last 2 versions of Chrome, Safari (iOS 15+), Firefox, Edge; Android Chrome 100+. Graceful degradation for Web Push on iOS Safari (PWA-installed only).
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.
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 |
0002, user import, and smoke tests..htaccess to the new build; keep the old PHP app in a read-only folder for rollback for 2 weeks.localStorage, merged into the DB cart at login (removes the biggest conversion friction).| 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.
| 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 |
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?+05:45 timestamps in DB (as now) or normalise to UTC?order_detail.php)?shipping_cost and free_shipping_threshold settings.delivered?| 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 |
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.]