A full-stack luxury fashion e-commerce platform built with React, Supabase, and Tailwind CSS.
- Overview
- Live Demo
- Tech Stack
- Architecture
- Design System
- Features
- Database Schema
- Project Structure
- Getting Started
- Environment Variables
- Database Setup
- Key Technical Decisions
- Known Limitations
Kova is an atmosphere-first luxury clothing storefront inspired by houses like Bottega Veneta and The Row. The project explores how editorial design principles — restraint, proportion, silence — can be applied to e-commerce UI without sacrificing functionality.
The platform ships two distinct experiences from a single codebase:
- The Storefront — a client-facing browsing and purchase experience with editorial content, personalised recommendations, and a wishlist/compare system.
- The Dashboard — a data-dense admin interface for product management, order fulfilment, customer oversight, and category taxonomy.
Storefront: your-deployment-url.vercel.app Admin access: Log in with
admin@kova.com/[your demo password](Set up a demo admin account in your Supabase profiles table before submitting)
| Layer | Technology | Rationale |
|---|---|---|
| Framework | React 19 | Concurrent features, React.lazy for code splitting |
| Build tool | Vite 8 | Sub-second HMR, native ESM, Rolldown bundler |
| Styling | Tailwind CSS v3 + PostCSS | Utility-first with a custom luxury design token system |
| Animation | Framer Motion 12 | Physics-based springs, AnimatePresence for route transitions |
| Backend | Supabase (PostgreSQL) | Real-time DB, Auth, Storage, and Row Level Security in one service |
| Forms | React Hook Form + Yup | Uncontrolled inputs, schema validation, minimal re-renders |
| Routing | React Router DOM v7 | File-based route splitting with React.lazy + Suspense |
| Notifications | React Toastify | Silent, non-blocking feedback consistent with the luxury tone |
The app uses a decoupled layered architecture:
src/
├── routes/ # Route definitions, ProtectedRoute, RequireAdmin guards
├── pages/ # Route-level components (lazy loaded)
│ ├── client/ # Storefront pages
│ └── admin/ # Dashboard pages
├── components/ # Shared UI components
├── context/ # Global state (Auth, Cart, Wishlist, Compare, Theme)
├── hooks/ # Data-fetching hooks that wrap services
├── services/ # Supabase query functions — the only layer that touches the DB
├── utils/ # Pure helpers (categoryConfig, etc.)
└── data/ # Static content (editorial articles)
Data flow:
Page Component
→ Custom Hook (useCart, useProducts…)
→ Service Function (fetchCartItems, fetchProducts…)
→ Supabase Client
→ PostgreSQL (with RLS enforced at DB level)
This separation means the database query logic lives in one place (/services), hooks handle loading state and side effects, and pages stay declarative.
- Authentication — Supabase Auth with JWT. A
handle_new_usertrigger automatically creates aprofilesrow on signup. - Row Level Security — every table has RLS enabled. Users can only read/write their own rows. Admin writes are gated behind an
is_admin()function that checksprofiles.role = 'admin'. - Storage — product images upload to a
product-imagesbucket. Public read, admin-only write enforced via storage RLS policies.
All design tokens are defined in two places and kept in sync: tailwind.config.js and src/index.css.
The palette deliberately avoids pure black (#000) and pure white (#fff). Every tone is slightly warm.
| Token | Light mode | Dark mode | Usage |
|---|---|---|---|
ivory |
#F7F5F0 |
#0F0E0C |
Page background |
parchment |
#EFECE5 |
#1A1916 |
Card backgrounds |
silk |
#E5E1D8 |
#2A2823 |
Borders |
charcoal |
#1A1916 |
#F0EDE6 |
Primary text |
warmgray |
#8A877F |
#7A776F |
Secondary text, labels |
obsidian |
#0D0C0A |
#FDFCF9 |
Primary CTAs |
cream |
#FDFCF9 |
#0D0C0A |
Text on dark surfaces |
Dark mode is toggled via a data-theme="dark" attribute on <html>. All CSS variables re-map automatically — no component needs a dark: class. The toggle is persisted to localStorage via ThemeContext.
- Display / Headings — Cormorant Garamond (300, 400, 600) — serif with high contrast strokes
- Body / UI — DM Sans (300, 400, 500) — geometric grotesque with optical sizing
- Labels — tracked out uppercase via
tracking-luxury: 0.18em— a custom Tailwind token
All borders and radii are rounded-none. The design language is strictly rectilinear — no pill buttons, no card rounding.
| Feature | Implementation notes |
|---|---|
| Full-screen editorial hero | Framer Motion AnimatePresence image crossfade, per-letter entrance animation on headline |
| Category strip | Live product counts fetched from Supabase, CSS overflow-x scroll with hidden scrollbar |
| Product grid | Filtered by category, price, search, sort — all via Supabase query composition |
| Infinite pagination | Offset-based loadMore with deduplication via Set of existing IDs |
| Product detail | Accordion specs, size/colour/volume selectors driven by categoryConfig.js per product type |
| Optimistic cart updates | UI reflects changes immediately; Supabase write happens async; reload reconciles |
| Wishlist | Same optimistic pattern as cart; heart icon toggles instantly |
| Compare table | Up to 4 products side-by-side; persisted to localStorage via CompareContext |
| Recently viewed | Logged on product detail mount via Supabase upsert; capped at 20 per user with auto-pruning |
| Personalised recommendations | Fetches products from the same categories as recently viewed items, excluding already-seen |
| Review system | Star rating breakdown, per-user review submission, profiles joined for display name |
| Order history | Full order list with status badges; expandable detail view with line items |
| Checkout | React Hook Form + Yup schema, simulated payment flow, order + order_items inserted transactionally |
| Editorial journal | Static articles in data/editorial.js, slug-based routing, related articles section |
| Dark mode | System-agnostic toggle, localStorage persistence, zero flicker on load |
| Responsive nav | Full-screen mobile overlay with translate-y transition; desktop transparent-on-scroll |
| Feature | Implementation notes |
|---|---|
| Stats overview | 4 parallel Supabase queries aggregated in fetchDashboardStats |
| Product CRUD | Create/edit form with Supabase Storage image upload; delete with confirmation guard |
| Image upload | File → Supabase Storage bucket → public URL stored in product row |
| Order management | Status filter tabs, inline status select with optimistic table update |
| Customer list | Profiles with role=user, order count joined |
| Category management | Auto-slug generation from name input, create/delete with table reflection |
| Breadcrumb nav | Derived from useLocation().pathname — no manual config |
| Role-based access | RequireAdmin wrapper checks isAdmin from AuthContext; DB-level RLS is the actual enforcement |
profiles -- extends auth.users (id, full_name, avatar_url, role)
products -- (id, title, description, price, category, image_url, rating, rating_count, is_featured)
categories -- (id, name, slug, image_url)
cart_items -- (id, user_id, product_id, quantity) UNIQUE(user_id, product_id)
wishlist_items -- (id, user_id, product_id) UNIQUE(user_id, product_id)
orders -- (id, user_id, status, subtotal, tax, total)
order_items -- (id, order_id, product_id, quantity, price_at_purchase)
reviews -- (id, product_id, user_id, rating 1-5, comment)
recently_viewed -- (id, user_id, product_id, viewed_at) UNIQUE(user_id, product_id)All tables have RLS enabled. The is_admin() function gates all admin write operations at the database level — frontend guards are a UX convenience, not a security mechanism.
kova/
├── db/
│ └── seeds/
│ ├── seed.js # Script to fetch DummyJSON products
│ ├── seed_products.sql # Generated seed output
│ ├── supabase_seed_luxury.sql # First luxury catalog (8 products)
│ └── supabase_seed_luxury_expanded.sql # Full catalog (40+ products)
├── public/
│ ├── favicon.svg
│ └── icons.svg
├── src/
│ ├── assets/
│ ├── components/
│ │ ├── admin/ # ProductForm, Primitives (StatCard, DataTable, etc.)
│ │ ├── Filters/ # Sidebar category/sort/size/colour filters
│ │ ├── Footer/
│ │ ├── home/ # Hero, CategoryStrip, EditorialTeaser, etc.
│ │ ├── layout/ # AppLayout, AdminLayout, ErrorBoundary
│ │ ├── Navbar/
│ │ ├── ProductCard/
│ │ ├── ProductGrid/
│ │ └── ReviewSection/
│ ├── context/
│ │ ├── AuthContext.jsx
│ │ ├── CartContext.jsx
│ │ ├── CompareContext.jsx
│ │ ├── ThemeContext.jsx
│ │ └── WishlistContext.jsx
│ ├── data/
│ │ └── editorial.js # Static article content
│ ├── hooks/
│ │ ├── useAdminOrders.js
│ │ ├── useAdminProducts.js
│ │ ├── useAdminStats.js
│ │ ├── useCart.js
│ │ ├── useDebounce.js
│ │ ├── useFilter.js
│ │ ├── useOrders.js
│ │ ├── useProducts.js
│ │ ├── useRecentlyViewed.js
│ │ ├── useReviews.js
│ │ └── useWishlist.js
│ ├── lib/
│ │ └── supabaseClient.js # Singleton client, null-safe export
│ ├── pages/
│ │ ├── admin/ # Dashboard, ProductsAdmin, OrdersAdmin, CustomersAdmin, CategoriesAdmin
│ │ └── client/ # Home, Products, ProductDetail, Cart, Checkout, Wishlist,
│ │ # Compare, Orders, OrderDetail, Login, Register, About,
│ │ # Editorial, EditorialArticle
│ ├── routes/
│ │ ├── ProtectedRoute.jsx
│ │ ├── RequireAdmin.jsx
│ │ └── routes.jsx
│ ├── services/
│ │ ├── admin.js
│ │ ├── cart.js
│ │ ├── orders.js
│ │ ├── products.js
│ │ ├── recentlyViewed.js
│ │ ├── reviews.js
│ │ └── wishlist.js
│ ├── utils/
│ │ └── categoryConfig.js # Per-category size/colour/volume config
│ ├── App.jsx
│ ├── index.css
│ └── main.jsx
├── supabase/
│ ├── rls.sql # is_admin() function + all RLS policies
│ └── storage.sql # product-images bucket policies
├── supabase_schema.sql # Full schema + triggers + basic policies
├── tailwind.config.js
├── vite.config.js
├── postcss.config.js
└── eslint.config.js
- Node.js >= 20
- A Supabase project (free tier works)
# Clone the repository
git clone https://github.com/Siddhant2713/KOVA.git
cd KOVA
# Install dependencies
npm install
# Create your environment file
cp .env.example .env
# Fill in your Supabase credentials (see Environment Variables below)
# Start the development server
npm run devNavigate to http://localhost:5173.
Create a .env file in the project root:
VITE_SUPABASE_URL=https://your-project-id.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key-hereBoth values are found in your Supabase project under Settings → API.
The app is designed to degrade gracefully when Supabase is not configured —
supabaseClient.jsexportsnulland all service functions check for it before executing. The storefront UI renders but data operations silently no-op.
Run the following SQL files in your Supabase SQL Editor in this exact order:
1. supabase_schema.sql -- Creates all tables, triggers, and basic RLS policies
2. supabase/rls.sql -- Adds is_admin() function and fine-grained admin policies
3. supabase/storage.sql -- Configures the product-images storage bucket
4. db/seeds/supabase_seed_luxury_expanded.sql -- Populates the full product catalog
- Register a new account through the storefront (
/register) - In your Supabase dashboard, go to Table Editor → profiles
- Find the row for your new user and set
roleto'admin' - Log out and back in — you'll be redirected to
/adminon login
Supabase gives PostgreSQL with Row Level Security enforced at the database layer, not the application layer. This means even if someone bypasses the frontend guards entirely and hits the Supabase REST API directly with a stolen anon key, they cannot read or write data they're not authorised for. A custom backend would require building and maintaining that access control separately.
The app has three pieces of genuinely global state: authentication status, cart, and wishlist. Redux would add significant boilerplate for data that rarely changes shape. Context API with useMemo on the value object is sufficient and keeps the dependency tree lighter. The trade-off is tree-wide re-renders on cart mutations — acceptable at this scale.
This is a single-page application with no SEO requirement (it's a college project, not a production store). Next.js server components and file-based routing would add complexity without benefit. React Router v7 with React.lazy and Suspense gives route-level code splitting that achieves the same bundle characteristics without the Next.js mental model overhead.
The hero letter-stagger, image crossfade, and scroll-triggered reveals require precise timing coordination between multiple elements. Framer Motion's variants and staggerChildren make this declarative. The alternative — coordinating multiple @keyframes with animation-delay — would be brittle and harder to read.
This was a deliberate scoping decision for the project timeline. TypeScript would catch the class of bug where a component uses an undeclared import or passes the wrong shape to a service function. It's the first thing that should be added in a production version.
- No real payment processing — the checkout form simulates a transaction. Card details are held only in React state and never transmitted anywhere. Replace with Stripe Elements before any real deployment.
- Offset pagination — the
loadMoreimplementation uses offset-based pagination. In a live catalogue with frequent inserts, this can produce duplicate or missing results. Cursor-based pagination is the correct fix. - No image optimisation — product images are loaded at full resolution from Unsplash. A production version should use
srcSet,loading="lazy", and serve WebP via an image CDN. - Static editorial content — articles live in
src/data/editorial.js. A real implementation would store them in a CMS or a Supabasearticlestable with a rich text field. - No test coverage — there are no unit, integration, or end-to-end tests. Vitest + React Testing Library for unit tests, and Playwright for e2e flows are the recommended additions.