A polished, full-stack Next.js 16 (App Router) + TypeScript + Tailwind v4 + Firebase e-commerce / product-catalog reference app, built for the Odyssey Next.js Assessment Task.
It ships a public marketing site, a searchable product catalog with filters and pagination, dynamic product detail pages, Firebase email/password + Google authentication, an in-app cart, a working contact form, and protected screens to add and manage products — all responsive, accessible, and production-built.
Live demo: https://jikmunn-odyssey-task-one.vercel.app/ Repository: https://github.com/muhammad-jiku/jikmunn-odyssey-task-one
- App Router + TypeScript (strict) — Next.js 16.2 with Turbopack, typed routes, and a clean
src/layout. - Tailwind CSS v4 — single-file design tokens (
@theme inline) with brand palette, surfaces, radii, shadows, focus-visible rings, micro-animations, andprefers-reduced-motionsupport. - In-app theme toggle — light / dark / system preference persisted to
localStorage, applied viadata-themewith a synchronous<head>bootstrap script (no FOUC). Custom Tailwinddarkvariant keyed off[data-theme="dark"]. - Reusable UI primitives —
Button,Input,PasswordInput(with show/hide toggle),Card,Badge,Container,Section,Spinner,Skeleton(+ItemCardSkeleton/ItemsGridSkeleton). - Layout shell — sticky
Navbarwith mobile sheet, theme toggle, cart link with live item-count badge, user dropdown, active-link styling; 4-columnFooter. - Polished landing page — Hero, Features, Featured items, Testimonials, CTA banner.
- Product catalog — search by title/description, category filter, max-price range slider, sort options, active-filter chips, 8-per-page pagination with prev/next + numbered buttons, empty-state CTA, responsive 1→2→3→4-col grid.
- Dynamic product details — SSG via
generateStaticParamsfor static items + client fallback for user-added items (same URL, same layout), related-products section, Add-to-cart button, branded 404-style not-found state. - Shopping cart —
CartContextpersisted tolocalStorage, full/cartpage with image, quantity stepper, line removal, Order Summary aside, demo checkout, Clear all, and empty-state UI. - Firebase Authentication — Email/Password + Google OAuth (
signInWithPopup),displayNameset viaupdateProfile,onAuthStateChangedsession persistence, friendly error mapping (auth/invalid-credential,auth/too-many-requests, …), lazy SDK init that gracefully no-ops when env keys are empty (build-safe). - Protected routes —
<ProtectedRoute>guard with redirect to/login?redirect=<path>, full add-product form (react-hook-form+zod), manage-products screen (desktop table / mobile stacked cards) with confirm-delete. - Working contact form —
react-hook-form+zodvalidation (name, email, subject, message) that submits viamailto:to the project owner. - localStorage-backed user items —
useSyncExternalStorehooks with cached snapshots, cross-tab (storageevent) and same-tab (custom event) sync; merged with the static seed for a unified Shop view. - A11y & polish — global focus-visible ring (scoped away from text inputs to avoid double borders),
aria-invalid/aria-describedbyon inputs, skeleton loading states,react-hot-toastnotifications, custom 404 page, brand::selection, smooth-scroll, reduced-motion override. - Quality gates green —
npm run lint,npm run typecheck, andnpm run buildall clean; 22 prerendered routes (12 framework + 10 SSG product pages).
| Area | Choice |
|---|---|
| Framework | Next.js 16.2 (App Router, Turbopack, typed routes) |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS v4 (@theme inline tokens, custom dark variant) |
| Forms | react-hook-form + zod (@hookform/resolvers) |
| Auth | Firebase Web SDK v12 (Email/Password + Google) |
| State | React Context + useSyncExternalStore over localStorage |
| Icons | lucide-react |
| Notifications | react-hot-toast |
| Utilities | clsx |
src/
├── app/
│ ├── layout.tsx # Theme bootstrap > Providers > Navbar > <main> > Footer
│ ├── page.tsx # Landing
│ ├── globals.css # Tailwind + design tokens + dark variant + animations
│ ├── not-found.tsx # Custom branded 404
│ ├── about/page.tsx
│ ├── contact/page.tsx # Channels + ContactForm + FAQ
│ ├── cart/page.tsx # Cart line items + Order Summary
│ ├── login/page.tsx # Suspense-wrapped (useSearchParams) + PasswordInput
│ ├── register/page.tsx # PasswordInput x2 (password + confirm)
│ └── items/
│ ├── page.tsx # Shop (client AllItemsBrowser)
│ ├── loading.tsx # Suspense skeleton for Shop
│ ├── [id]/page.tsx # SSG static + client fallback
│ ├── add/page.tsx # Protected
│ └── manage/page.tsx # Protected
├── components/
│ ├── Providers.tsx # ThemeProvider > AuthProvider > CartProvider + Toaster
│ ├── auth/ProtectedRoute.tsx
│ ├── contact/ContactForm.tsx
│ ├── items/{ItemCard,ItemsBrowser,AllItemsBrowser,UserItemDetailsClient,AddToCartButton}.tsx
│ ├── layout/{Navbar,Footer,nav-links}.tsx
│ └── ui/{Button,Input,PasswordInput,Card,Badge,Container,Section,Spinner,Skeleton,index}.tsx
├── context/{AuthContext,ThemeContext,CartContext}.tsx
├── data/items.ts # 10 hand-picked static seed products
├── lib/{firebase,itemsStore,items-utils}.ts
└── types/item.ts
- Node.js ≥ 20 (tested on 22.17.1)
- npm ≥ 10
git clone https://github.com/muhammad-jiku/jikmunn-odyssey-task-one.git
cd jikmunn-odyssey-task-one
npm installCopy the template and fill it with your Firebase Web App config:
cp .env.example .env.local# .env.local
NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...
NEXT_PUBLIC_FIREBASE_APP_ID=...The app builds and runs even with the keys empty —
firebaseEnabledwill befalse, and auth screens will surface a friendly disabled-state banner.
npm run dev # http://localhost:3000
npm run lint # ESLint
npm run typecheck # tsc --noEmit
npm run build # production build (22 prerendered routes)
npm start # serve the production build- Create a project at https://console.firebase.google.com.
- Build → Authentication → Sign-in method:
- Enable Email/Password.
- Enable Google (set a public-facing project name + support email).
- Project settings → General → Your apps → Web app → register a web app and copy the config values into
.env.local(see above). - Authorized domains (Authentication → Settings): add
localhost(already there) and your deployed domain (e.g.jikmunn-odyssey-task-one.vercel.app).
User items are persisted to localStorage under the key jikmunn-odyssey:user-items:v1 (no Firestore required for this assessment). They sync across tabs and within the current tab via a custom event. The cart and theme preference use jikmunn-odyssey:cart:v1 and jikmunn-odyssey:theme respectively.
| Route | Type | Auth | Purpose |
|---|---|---|---|
/ |
Static | Public | Landing — Hero, Features, Featured items, Testimonials, CTA |
/about |
Static | Public | About — story + values + create-account CTA |
/contact |
Static | Public | Contact — channels + working mailto: form + FAQ |
/cart |
Static | Public | Cart line items, qty steppers, Order Summary, demo checkout |
/items |
Static | Public | Shop — search + category + price + sort + pagination |
/items/[id] |
SSG (10) + client fallback | Public | Product details + Add to cart + related products |
/login |
Static (Suspense) | Public | Email/Password + Google login (?redirect= aware) |
/register |
Static | Public | Create account (sets displayName) |
/items/add |
Static | Protected | Add a product (RHF + zod) |
/items/manage |
Static | Protected | Table / cards with View + Delete |
/_not-found |
Static | Public | Custom branded 404 |
The live deployment is hosted at https://jikmunn-odyssey-task-one.vercel.app/.
To deploy your own fork:
- Push the repo to GitHub.
- Import the project on https://vercel.com/new.
- Environment variables — paste every
NEXT_PUBLIC_FIREBASE_*key from.env.localinto the Vercel project settings (Production + Preview). - Deploy. Vercel auto-detects Next.js and runs
next build. - Add the resulting
*.vercel.appdomain to Firebase Auth → Settings → Authorized domains. - Smoke-test login, register, Google OAuth, add product, manage product, add-to-cart, and contact form on the live URL.
| Check | Command | Status |
|---|---|---|
| Lint | npm run lint |
✅ Clean (0 errors, 0 warnings) |
| Type-check | npm run typecheck |
✅ Clean |
| Production build | npm run build |
✅ 22 prerendered routes |
Built for the Odyssey Next.js Assessment Task by Muhammad Jiku. All product imagery sourced from Unsplash.