Skip to content

feat: Next.js 16 frontend — full customer shop and admin panel - #11

Merged
rezadrian01 merged 8 commits into
mainfrom
feat/frontend
May 31, 2026
Merged

feat: Next.js 16 frontend — full customer shop and admin panel#11
rezadrian01 merged 8 commits into
mainfrom
feat/frontend

Conversation

@rezadrian01

Copy link
Copy Markdown
Owner

Summary

Complete frontend implementation for Auron built on Next.js 16, React 19, TanStack Query v5, and Stripe.

  • Foundation — dependency setup, Tailwind v4, shadcn/ui, QueryProvider, shared types, BFF server utilities, proxy guard
  • Auth — httpOnly cookie relay via BFF route handlers, login/register pages, proxy.ts route guard (Next.js 16 convention), token refresh on protected routes
  • Shop — editorial landing page, product catalog with search/filter/pagination (SSR), product detail with inventory badge, Cormorant Garamond / Geist Sans design system
  • Cart — server-side cart with TanStack Query, quantity stepper, CartCount navbar island, cart summary with free-shipping threshold
  • Orders — order history and detail (SSR), cancel button, order status badges
  • Checkout — shipping form → POST /orders → async Stripe Payment Element (polls for client_secret every 2s while Kafka delivers the payment intent), success page
  • Profile — account details form (isDirty guard), password change, address CRUD with inline expand/collapse forms
  • Admin panel — dashboard with stats, product CRUD, category management, inventory stock control; JWT role check in layout + proxy guard

Architecture notes

  • BFF pattern throughout — all authenticated API calls go through Next.js Route Handlers which read the httpOnly access_token cookie and forward Authorization: Bearer to the gateway. The browser never touches the cookie.
  • Next.js 16 conventionsproxy.ts (not middleware.ts), await params/await searchParams, await cookies() everywhere.
  • StripeloadStripe singleton outside component, <Elements> provider, stripe.confirmPayment({ redirect: 'if_required' }). Stripe appearance uses hex equivalents since the iframe doesn't support oklch.
  • Zod v4 + react-hook-formz.boolean().default() and z.coerce.number() cause Resolver type incompatibilities; fixed throughout by using z.boolean() / z.number() with defaultValues and valueAsNumber: true.

Test plan

  • Register → login → httpOnly cookie set → /profile accessible
  • Logout → cookie cleared → /profile redirects to /login
  • Customer visits /admin → redirected to /
  • Browse /products with JS disabled → SSR content visible
  • Add to cart → badge updates in navbar → cart page shows items
  • Checkout → place order → Stripe Payment Element appears → pay with 4242 4242 4242 4242 → success page
  • /orders shows completed order
  • Profile: update name, change password, add/edit/delete address
  • Admin login → /admin dashboard shows product count
  • Admin creates product → appears in /products
  • Admin inventory page → set stock → product detail stock badge updates

Note: This PR implements the original single-admin store design. The project is being pivoted to a multi-vendor marketplace (customer/seller/admin). Adjustments will follow in a new branch — see docs/MARKETPLACE_PLAN.md.

🤖 Generated with Claude Code

rezadrian01 and others added 8 commits May 30, 2026 17:02
…PI layer

- Initialised shadcn/ui (Tailwind v4 CSS-variable mode); installed button, input,
  label, card, badge, form, select, textarea, dialog, sheet, dropdown-menu, table,
  skeleton, sonner, navigation-menu, separator, avatar, tabs, and pagination
- Added TanStack Query v5, react-hook-form, Zod, Stripe JS, lucide-react,
  server-only
- Updated next.config.ts: reactCompiler, remotePatterns (GCS + localhost),
  turbopack root to silence workspace-root warning
- Created TypeScript types: auth, product (with images[]), order, payment
- Created Zod validation schemas: auth (login/register), product, address
- Created lib/api/server.ts — server-only fetcher using await cookies() (Next.js 16)
- Created lib/api/client.ts — browser-side wrapper with 401 auto-refresh
- Created lib/query-keys.ts — centralised TanStack Query key factory
- Created lib/providers/query-provider.tsx — QueryClientProvider with devtools
- Updated app/layout.tsx — wrapped with QueryProvider + Toaster (sonner)
- Added app/not-found.tsx — friendly 404 page using buttonVariants
- Updated .gitignore to exclude GCP service-account JSON files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Next.js 16 requires this devDependency when reactCompiler is enabled
in next.config.ts — without it the dev server fails to start.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Route Handlers (BFF cookie relay):
- app/api/auth/login — POST: calls backend, sets HttpOnly access_token (15m) +
  refresh_token (7d) both with Path=/ so proxy.ts can read the refresh cookie
- app/api/auth/register — POST: same cookie pattern as login
- app/api/auth/logout — POST: best-effort backend logout, clears both cookies
- app/api/auth/refresh — POST: exchanges refresh_token, issues new pair
- app/api/auth/me — GET: proxies /users/me with the httpOnly access_token

proxy.ts (Next.js 16 — renamed from middleware.ts):
- Redirects logged-in users away from /login and /register
- Guards /cart, /checkout, /orders, /profile, /admin with access_token check
- Falls back to token refresh via /api/auth/refresh when only refresh_token exists
- Decodes JWT payload to enforce admin-only access to /admin routes

Auth pages — luxury editorial dark design:
- app/(auth)/layout.tsx: split panel, Cormorant Garamond, warm-gold accent,
  subtle grid texture, radial glow, staggered reveal animations
- login/page.tsx + login-form.tsx: react-hook-form + loginSchema, inline error
  state, gold focus ring via CSS variable override, callbackUrl redirect
- register/page.tsx + register-form.tsx: full_name, email, phone (optional),
  password, confirm_password with cross-field Zod refinement

lib/hooks/use-auth.ts: useCurrentUser() + useLogout() with TanStack Query
globals.css: added auron-fade-up keyframes + gold color tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Layout:
- app/(shop)/layout.tsx: shop shell wrapping Navbar + Footer around all shop pages
- Navbar: sticky glass-blur header, Cormorant Garamond logo with gold dot, debounced
  search (NavbarSearch client island in Suspense), cart icon, user dropdown with
  avatar initials and role-aware menu (admin link shown for admin role)
- Footer: dark-panel, three link columns, gold top-line gradient, client component
  for hover effects
- Promoted Cormorant Garamond to root layout so both auth and shop share the font

Landing page (app/(shop)/page.tsx):
- Animated ticker band (CSS-only, seamless loop, gold dots as separators)
- Asymmetric editorial hero: oversized stacked serif text left, dark geometric panel
  right with concentric gold circles and "A" monogram watermark
- Featured products grid (8 newest, SSR with 5-min revalidation)
- CategoryGrid client component with gold-border hover on category cards

Product catalog:
- /products: server-rendered listing with ProductFilters sidebar (category, sort,
  price range) and ProductSearch (300ms debounce, updates URL), both wrapped in
  Suspense; searchParams awaited as Promise (Next.js 16)
- /products/[id]: product detail with image gallery strip, stock badge, breadcrumb,
  add-to-cart button (disabled when out-of-stock, wired in next phase)
- loading.tsx files for both routes (skeleton states)

Components:
- ProductCard: 4:5 aspect image, slide-up on hover, fallback "A" monogram
- ProductGrid: responsive auto-fill grid + page-number pagination
- ProductGridSkeleton: matching skeleton layout
- ProductFilters + ProductSearch: client components, URL-param driven
- CategoryGrid: client component extracted to avoid server/client boundary issues

API layer:
- lib/api/products.ts: getProducts, getProduct, getCategories (server-only,
  force-cache with 5-min revalidation)
- lib/hooks/use-products.ts: useProducts, useProduct, useCategories (client hooks)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cart: add/update/remove items, qty stepper, real-time total, CartCount badge in navbar
- Product detail: Add to Cart client component wired with quantity selector and toast feedback
- Cart page: item list with inline controls, order summary panel with shipping threshold logic, empty state
- BFF route handlers for cart and orders (read access_token httpOnly cookie, forward to gateway with Bearer auth)
- Order history list: SSR via serverFetch, status badges, order rows with hover state
- Order detail: SSR, item breakdown table, shipping address, cancel button (client component, available on pending/confirmed/processing)
- TypeScript types for Cart, CartItem, Order, OrderItem

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ling

- Checkout page: shipping form (shipping_name + shipping_address), cart summary sidebar, places order via POST /api/orders on submit
- Pay page (/checkout/[orderId]/pay): polls GET /api/payments/order/:orderId every 2 s until client_secret arrives (Kafka async gap between order creation and Stripe PaymentIntent setup), then mounts Stripe Elements; calls stripe.confirmPayment() with redirect:'if_required' to handle 3DS without full redirect for card payments
- Success page: confirmed/processing states, order reference, links to order detail and shop
- PaymentForm component: PaymentElement with custom Stripe appearance matching the editorial theme (flat theme, gold accent #C9A96E, Geist font)
- BFF route /api/payments/order/[orderId] forwards to gateway with httpOnly access_token
- usePaymentByOrder hook: polling via TanStack Query refetchInterval, stops once client_secret is non-null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… management

Bug fixes:
- /api/auth/me BFF now unwraps the { success, data } envelope and maps backend `name` to frontend `full_name`; useCurrentUser reads the mapped object directly instead of guessing the shape
- Address type corrected to match backend AddressResponse: uses `postal_code` (not `zip_code`), optional fields typed as `string | null`, removed `user_id`/`updated_at` which the backend doesn't include

New BFF routes:
- PUT /api/profile — maps full_name→name outbound and name→full_name inbound
- GET/POST /api/addresses, PUT/DELETE /api/addresses/[id] — proxy to /users/me/addresses with httpOnly cookie auth

Profile page (/profile):
- Two-column layout: circular monogram avatar sidebar + form content area
- Account details form: name and email with save-only-if-dirty guard
- Password change form: new password + confirm, validated with Zod refine
- Addresses section: card grid with inline edit expand, delete with confirm, add-new inline form, default badge with gold star
- All mutations use toast feedback; form validation via Zod v4 + react-hook-form

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin layout and sidebar:
- `app/admin/layout.tsx` — Server Component that decodes the JWT, redirects
  non-admins to / and unauthenticated users to /login (defence-in-depth;
  proxy.ts already guards /admin at the edge)
- `components/admin/admin-sidebar.tsx` — sticky dark sidebar with gold
  active-link indicator; usePathname for exact/prefix matching

Dashboard (app/admin/page.tsx):
- Stat cards: total products and category count
- "Recent Products" table (last 6, newest first) with status badges

Products CRUD:
- `/admin/products` — SSR paginated table, Pencil edit link, DeleteProductButton
  (client island that calls DELETE BFF and calls router.refresh())
- `/admin/products/new` — server page fetches categories, passes as props to
  ProductForm client component (avoids client-side category fetch)
- `/admin/products/[id]/edit` — server page fetches product + categories,
  pre-populates ProductForm
- `components/admin/product-form.tsx` — react-hook-form + Zod; fixed the
  z.coerce.number() Resolver type incompatibility by switching to z.number()
  with valueAsNumber: true on the input registration

Categories:
- `/admin/categories` — fully client-side; useCategories fetches from public
  endpoint; useCreateCategory POSTs via BFF; list auto-refreshes via
  invalidateQueries

Inventory:
- `/admin/inventory` — SSR fetches product list, passes to InventoryTable
  client component; each InventoryRow uses useInventory() per-product query
  (lazy stock fetch, deduped/cached by TanStack Query) and useUpdateInventory
  mutation to call PUT /api/admin/inventory/:productId

BFF routes (all read httpOnly access_token cookie):
- POST /api/admin/products — create product
- PUT/DELETE /api/admin/products/[id] — update/delete product
- POST /api/admin/categories — create category
- GET/PUT /api/admin/inventory/[productId] — read and set stock

Fixed lib/validations/product.ts:
- Added image_url optional field
- Changed is_active from z.boolean().optional().default(true) → z.boolean()
  (default now lives in useForm defaultValues; avoids Resolver type mismatch)
- Changed price from z.coerce.number() → z.number() with valueAsNumber: true
  registration (same Zod v4 + react-hook-form Resolver incompatibility fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 31, 2026 09:19
@rezadrian01
rezadrian01 merged commit 99fd93e into main May 31, 2026
1 of 2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants