Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 21 additions & 22 deletions apps/web/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Full layout spec: see apps/web/WEB_DESIGN.md Section 3.

WHAT THE WEB APP SERVES:
├── Public pages: product discovery, store pages, landing page (no auth needed)
├── Shopper pages: cart, orders, profile, checkout (/buyer/*)
├── Shopper pages: /home, /cart, /orders, /wishlist, /saved, /messages
└── Store dashboard: product management, orders, payouts (/store/*)

WHAT THE WEB APP IS NOT:
Expand Down Expand Up @@ -65,8 +65,8 @@ apps/web/src/app/
│ │ └── page.tsx ← /explore — product discovery feed
│ ├── p/[code]/
│ │ └── page.tsx ← /p/TWZ-ABC123 — product detail
│ ├── @[handle]/
│ │ └── page.tsx ← /@chiastyles — store page (public)
│ ├── stores/[handle]/
│ │ └── page.tsx ← /stores/chiastyles — store page (public)
│ ├── c/[category]/
│ │ └── page.tsx ← /c/fashion — category browse
│ ├── about/page.tsx
Expand All @@ -81,18 +81,18 @@ apps/web/src/app/
│ ├── forgot-password/page.tsx
│ └── verify-email/page.tsx
├── (shopper)/ ← Authenticated shopper pages
├── (app)/ ← Authenticated shopper pages
│ ├── layout.tsx ← Auth gate + 3-COLUMN SOCIAL LAYOUT
│ ├── buyer/
├── feed/page.tsx ← /buyer/feed — personalised feed
├── cart/page.tsx ← /buyer/cart
├── checkout/[productId]/page.tsx ← focus mode (no nav chrome)
│ │ ├── orders/
│ │ ├── page.tsx ← order list with filter tabs
│ │ └── [id]/page.tsx ← order detail + tracking timeline
├── saved/page.tsx
├── wishlist/page.tsx
└── profile/page.tsx
│ ├── home/page.tsx ← /home — personalised feed
│ ├── cart/page.tsx ← /cart
│ ├── checkout/[productId]/page.tsx ← focus mode (no nav chrome)
│ ├── orders/
│ │ ├── page.tsx ← /orders list with filter tabs
│ │ ├── [id]/page.tsx ← order detail + tracking timeline
│ │ └── confirmed/[orderId]/page.tsx
│ ├── saved/page.tsx
│ ├── wishlist/page.tsx
│ └── messages/page.tsx
└── (store)/ ← Authenticated store owner pages
├── layout.tsx ← Auth gate + STORE MODE LAYOUT (dark, no right panel)
Expand All @@ -106,7 +106,6 @@ apps/web/src/app/
│ │ ├── page.tsx
│ │ └── [id]/page.tsx
│ ├── messages/page.tsx
│ ├── my-store/page.tsx ← store profile preview
│ ├── payouts/page.tsx
│ ├── settings/page.tsx
│ └── verification/page.tsx ← KYB flows
Expand Down Expand Up @@ -344,7 +343,7 @@ VARIANT SELECTION GATE:
Do not proceed to checkout
```

### Public — Store Page (`/@[handle]`)
### Public — Store Page (`/stores/[handle]`)

```
DATA: GET /stores/:handle (server component)
Expand Down Expand Up @@ -485,7 +484,7 @@ SCREEN 4 — SIZE GUIDE AND PUBLISH:
[Preview] → opens product page preview in new tab
```

### Shopper — Order Detail (`/buyer/orders/[id]`)
### Shopper — Order Detail (`/orders/[id]`)

```
DATA: GET /orders/:id (protected — own orders only)
Expand Down Expand Up @@ -585,19 +584,19 @@ export function middleware(request: NextRequest) {
const session = request.cookies.get('access_token')

// Routes always public (no check):
const publicRoutes = ['/', '/explore', '/p/', '/@', '/c/', '/about', '/terms', '/privacy', '/help']
const publicRoutes = ['/', '/explore', '/stores', '/u/', '/p/', '/c/', '/about', '/terms', '/privacy', '/help']
const authRoutes = ['/login', '/register', '/forgot-password', '/verify-email']

// Public route — allow through:
if (publicRoutes.some(r => pathname.startsWith(r))) return NextResponse.next()

// Auth page + already logged in — redirect to feed:
// Auth page + already logged in — redirect to home:
if (authRoutes.some(r => pathname.startsWith(r)) && session) {
return NextResponse.redirect(new URL('/buyer/feed', request.url))
return NextResponse.redirect(new URL('/home', request.url))
}

// Protected route + no session — redirect to login:
if (!session && (pathname.startsWith('/buyer/') || pathname.startsWith('/store/'))) {
if (!session && (pathname.startsWith('/home') || pathname.startsWith('/cart') || pathname.startsWith('/orders') || pathname.startsWith('/store/'))) {
return NextResponse.redirect(new URL(`/login?redirect=${pathname}`, request.url))
Comment on lines +587 to 600
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the middleware example to match the shared route policy.

The protected-route snippet still only covers /home, /cart, /orders, and /store/, so anyone copying it will miss /checkout, /wishlist, /saved, /messages, /notifications, and /settings/*. At this point it would be safer to reference apps/web/src/lib/route-policy.ts directly instead of duplicating prefix lists here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/AGENTS.md` around lines 587 - 600, Replace the hardcoded
protected-route checks in the middleware (the explicit
pathname.startsWith('/home') || ... block) with the shared route policy exported
from route-policy.ts: import and call the exported helper (e.g.,
isProtectedRoute or protectedPrefixes) to determine if a pathname is protected,
and use that result in the session check that performs NextResponse.redirect;
also update the example to reference the centralized publicRoutes/authRoutes
constants (or use exported helpers) instead of duplicating lists so the
middleware covers /checkout, /wishlist, /saved, /messages, /notifications,
/settings/* and stays in sync.

}

Expand Down Expand Up @@ -666,7 +665,7 @@ WHEN IN STORE MODE AND USER TAPS [Browse Feed]:
- Like / Comment / Share: rendered and functional
- Interactions fire as STORE identity (not personal user)
API call includes: { interactAs: 'store', storeId: currentStoreId }
- Header shows: "Browsing as @chiastyles" subtle indicator
- Header shows active store handle subtle indicator

WHAT THIS MEANS IN CODE:
const { mode } = useMode() // 'shopping' | 'store'
Expand Down
2 changes: 1 addition & 1 deletion apps/web/WEB_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ INFINITE SCROLL:
ROOT LAYOUT (applies to all pages using the 3-column layout):

// apps/web/src/app/(public)/layout.tsx
// apps/web/src/app/(shopper)/buyer/layout.tsx
// apps/web/src/app/(app)/layout.tsx

<div className="min-h-screen bg-[var(--color-bianca)]">
{/* Left Nav — hidden on mobile */}
Expand Down
17 changes: 0 additions & 17 deletions apps/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,6 @@ const nextConfig = {
],
},
transpilePackages: ["@twizrr/shared"],
async rewrites() {
return [
{
source: '/@:slug',
destination: '/_at_/:slug',
},
];
},
async redirects() {
return [
{ source: '/buyer/catalogue', destination: '/buyer/feed', permanent: true },
{ source: '/buyer/products/:id', destination: '/p/:id', permanent: true },
{ source: '/buyer/merchants/:id', destination: '/@:id', permanent: true },
{ source: '/buyer/merchants', destination: '/merchants', permanent: true },
{ source: '/m/:slug', destination: '/@:slug', permanent: true },
];
},
};

export default nextConfig;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import { CartClient } from "./CartClient";
import { CartClient } from "@/app/(shopper)/buyer/cart/CartClient";

export const metadata: Metadata = {
title: "Your cart twizrr",
title: "Your cart | twizrr",
robots: { index: false, follow: false },
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import { CheckoutClient } from "./CheckoutClient";
import { CheckoutClient } from "@/app/(shopper)/buyer/checkout/[productId]/CheckoutClient";

export const metadata: Metadata = {
title: "Checkout twizrr",
title: "Checkout | twizrr",
robots: { index: false, follow: false },
};

Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/app/(app)/home/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Metadata } from "next";
import { FeedClient } from "@/components/feed/FeedClient";

export const metadata: Metadata = {
title: "Home | twizrr",
description:
"Your personalised feed of products and posts from stores you follow on twizrr.",
};

export default function HomePage() {
return (
<>
<section className="border-b border-[var(--border)] px-4 py-5 sm:px-6">
<h1 className="font-cabinet text-xl font-bold text-[var(--color-espresso)]">
Home
</h1>
</section>

<FeedClient
defaultTab="for-you"
isAuthenticated={true}
loginRedirect="/home"
/>
</>
);
}
6 changes: 6 additions & 0 deletions apps/web/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ReactNode } from "react";
import { ShopperShell } from "@/app/(shopper)/buyer/_components/ShopperShell";

export default function AppRouteLayout({ children }: { children: ReactNode }) {
return <ShopperShell>{children}</ShopperShell>;
}
18 changes: 18 additions & 0 deletions apps/web/src/app/(app)/messages/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Metadata } from "next";
import { EmptyState } from "@/components/ui/State";

export const metadata: Metadata = {
title: "Chats | twizrr",
robots: { index: false, follow: false },
};

export default function MessagesPage() {
return (
<section className="px-4 py-10 sm:px-6">
<EmptyState
title="Chats are coming soon"
description="Store and shopper conversations will appear here when chat is available."
/>
</section>
);
}
18 changes: 18 additions & 0 deletions apps/web/src/app/(app)/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Metadata } from "next";
import { EmptyState } from "@/components/ui/State";

export const metadata: Metadata = {
title: "Notifications | twizrr",
robots: { index: false, follow: false },
};

export default function NotificationsPage() {
return (
<section className="px-4 py-10 sm:px-6">
<EmptyState
title="No notifications yet"
description="Order updates, store activity, and important account alerts will appear here."
/>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import { OrderDetailClient } from "./OrderDetailClient";
import { OrderDetailClient } from "@/app/(shopper)/buyer/orders/[id]/OrderDetailClient";

export const metadata: Metadata = {
title: "Order details twizrr",
title: "Order details | twizrr",
robots: { index: false, follow: false },
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import { OrderConfirmedClient } from "./OrderConfirmedClient";
import { OrderConfirmedClient } from "@/app/(shopper)/buyer/orders/confirmed/[orderId]/OrderConfirmedClient";

export const metadata: Metadata = {
title: "Order Confirmed twizrr",
title: "Order Confirmed - twizrr",
robots: { index: false, follow: false },
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import { OrdersListClient } from "./OrdersListClient";
import { OrdersListClient } from "@/app/(shopper)/buyer/orders/OrdersListClient";

export const metadata: Metadata = {
title: "Your orders twizrr",
title: "Your orders - twizrr",
robots: { index: false, follow: false },
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import { SavedClient } from "./SavedClient";
import { SavedClient } from "@/app/(shopper)/buyer/saved/SavedClient";

export const metadata: Metadata = {
title: "Saved posts twizrr",
title: "Saved posts - twizrr",
robots: { index: false, follow: false },
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Metadata } from "next";
import { WishlistClient } from "./WishlistClient";
import { WishlistClient } from "@/app/(shopper)/buyer/wishlist/WishlistClient";

export const metadata: Metadata = {
title: "Your wishlist twizrr",
title: "Your wishlist - twizrr",
robots: { index: false, follow: false },
};

Expand Down
11 changes: 6 additions & 5 deletions apps/web/src/app/(public)/p/[code]/ProductDetailClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useToast } from "@/components/ui/Toast";
import { ProductDetailsSheet } from "@/components/product/ProductDetailsSheet";
import { SizeGuide } from "@/components/product/SizeGuide";
import { DeliveryPreview } from "@/components/product/DeliveryPreview";
import { getPublicStoreHref } from "@/lib/routes";
import type { PublicProduct, ProductVariant, ProductImage } from "./page";

// ─── Helpers ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -322,7 +323,7 @@ export function ProductDetailClient({ product }: ProductDetailClientProps) {
if (product.productCode) params.set("code", product.productCode);
if (selectedVariant?.id) params.set("variantId", selectedVariant.id);
const qs = params.toString();
router.push(`/buyer/checkout/${product.productId}${qs ? `?${qs}` : ""}`);
router.push(`/checkout/${product.productId}${qs ? `?${qs}` : ""}`);
}

// ── Render ─────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -388,7 +389,7 @@ export function ProductDetailClient({ product }: ProductDetailClientProps) {
{/* Store card */}
<div className="flex items-center gap-3 rounded-xl border border-[var(--border)] bg-[var(--card)] p-3">
{product.store.logoUrl ? (
<div className="relative h-10 w-10 shrink-0 overflow-hidden rounded-full">
<div className="relative h-10 w-10 shrink-0 overflow-hidden rounded-xl">
<Image
src={product.store.logoUrl}
alt={product.store.name ?? "Store logo"}
Expand All @@ -398,7 +399,7 @@ export function ProductDetailClient({ product }: ProductDetailClientProps) {
/>
</div>
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[var(--surface-muted)]">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--surface-muted)]">
<Store
className="h-5 w-5 text-[var(--muted-foreground)]"
aria-hidden="true"
Expand All @@ -419,13 +420,13 @@ export function ProductDetailClient({ product }: ProductDetailClientProps) {
</div>
{product.store.handle && (
<p className="text-xs font-cabinet text-[var(--muted-foreground)]">
@{product.store.handle}
{product.store.handle}
</p>
)}
</div>
{product.store.handle && (
<Link
href={`/stores/${product.store.handle}`}
href={getPublicStoreHref(product.store.handle)}
className="shrink-0 text-xs font-medium font-cabinet text-[var(--color-saffron)] hover:underline min-h-[44px] flex items-center"
>
Visit
Expand Down
Loading
Loading