A full-stack menswear boutique built as a portfolio project to demonstrate production-grade Next.js architecture. Every part of the stack is real: live Stripe payments, PostgreSQL persistence, OAuth authentication, transactional email, image hosting on Cloudinary, and a fully operational admin dashboard.
Storefront
- Responsive product listing with sidebar filters (category, price range, size, color) and multi-criteria sort
- Product detail page with image gallery, color/size variant selector, and customer reviews
- Persistent shopping cart and wishlist powered by Zustand + localStorage — survives page reloads without flicker
- Live search modal with instant results as you type
- Category landing pages and full-text search results
Checkout & Payments
- 3-step checkout: contact & shipping → shipping method → Stripe payment
- Stripe PaymentElement with card, Apple Pay, and Google Pay support
- Standard (5–7 days) and Express (1–2 days) shipping rates with automatic free shipping above €150
- Order confirmation email sent via Resend immediately after payment succeeds
Authentication
- Email/password sign-up with bcrypt password hashing
- One-click OAuth via GitHub and Google
- Protected
/accountand/adminroutes with automatic redirect
Account Area
- Order history with per-order line items, status badges, and totals
- Profile editing — display name and password change
- Wishlist management — add, remove, and browse saved items
Admin Dashboard
- Revenue and orders-over-time charts powered by Recharts
- Full product CRUD with Cloudinary image upload and variant management
- Orders management — filter by status, update fulfillment state
- Customers directory and category management
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS v4 |
| Animation | Framer Motion |
| Database | Neon PostgreSQL (serverless) |
| ORM | Prisma 7 with @prisma/adapter-pg |
| Auth | NextAuth v5 — credentials + GitHub + Google OAuth |
| Payments | Stripe — PaymentIntent API + PaymentElement |
| Resend | |
| Image hosting | Cloudinary |
| State | Zustand with localStorage persistence |
| Data fetching | TanStack Query v5 |
| Forms | React Hook Form + Zod |
| Charts | Recharts |
| Notifications | Sonner toast |
| Deployment | Vercel |
src/
├── app/
│ ├── (shop)/ # Public storefront — homepage, products, cart, checkout
│ ├── account/ # Protected customer area — orders, profile, wishlist
│ ├── admin/ # Protected admin dashboard
│ ├── api/ # REST API routes — products, cart, checkout, orders, admin
│ └── auth/ # Sign-in and sign-up pages
├── components/
│ ├── layout/ # Navbar, Footer, CartDrawer, SearchModal
│ ├── product/ # ProductCard, ProductGallery, VariantSelector, Filters
│ ├── admin/ # Admin-specific forms, charts, and action components
│ └── ui/ # Badge, Skeleton, StarRating
├── lib/ # Prisma client, auth config, Stripe, email, utils
├── store/ # Zustand cart and wishlist stores
└── types/ # Shared TypeScript interfaces
prisma/
├── schema.prisma # Full data model — User, Product, Variant, Order, Review…
└── seed.ts # 29 menswear products, 400+ variants across 4 categories
- Node.js 20+
- A Neon PostgreSQL database
- A Stripe account (test mode keys)
- A Resend account
- A Cloudinary account
- GitHub and/or Google OAuth app credentials
git clone https://github.com/YOUR_USERNAME/cartello.git
cd cartello
npm installCreate a .env file at the root:
# Database
DATABASE_URL="postgresql://..."
# NextAuth
NEXTAUTH_URL="http://localhost:3000"
AUTH_SECRET="your-secret"
# OAuth
AUTH_GITHUB_ID=""
AUTH_GITHUB_SECRET=""
AUTH_GOOGLE_ID=""
AUTH_GOOGLE_SECRET=""
# Stripe
STRIPE_SECRET_KEY="sk_test_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
# Cloudinary
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=""
CLOUDINARY_API_KEY=""
CLOUDINARY_API_SECRET=""
# Resend
RESEND_API_KEY="re_..."
EMAIL_FROM="Cartello <onboarding@resend.dev>"
# App
NEXT_PUBLIC_APP_URL="http://localhost:3000"# Push the schema to your Neon database
npx prisma db push
# Seed with 29 products and variants
npx prisma db seednpm run devOpen http://localhost:3000.
Zustand hydration guard — Zustand's persist middleware rehydrates from localStorage asynchronously after the first client render. Cart totals, wishlist icons, and nav badges all gate on a _hasHydrated flag to prevent a server/client mismatch and avoid the flash of incorrect state.
Multi-step checkout without form value loss — Each checkout step is a separate React component with its own useForm instance. Validated data is lifted to the parent in useState so values survive step transitions. Conditional rendering ({step === N && <Step />}) unmounts fields between steps, making a single shared form unworkable.
Client-side payment confirmation — Rather than relying on a Stripe webhook (which requires a public endpoint and a running tunnel in development), the client calls POST /api/orders/:id/confirm after stripe.confirmPayment() resolves. This endpoint verifies the PaymentIntent status directly with the Stripe API before marking the order as PROCESSING and sending the confirmation email. The endpoint is idempotent — repeat calls for already-confirmed orders are a no-op.
MIT