Initial commit - Route Commerce platform
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
# Admin Pages Redesign Implementation Plan
|
||||
|
||||
**Goal:** Unify admin pages with warm earth-tone dark sidebar, cream content, emerald accent, monospace data tables.
|
||||
|
||||
**Architecture:** Create shared CSS design tokens, update sidebar with warm dark theme, then update each content page to use consistent styling.
|
||||
|
||||
**Tech Stack:** Next.js, Tailwind CSS, React
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create CSS Design Tokens
|
||||
|
||||
**Files:**
|
||||
- Create: `src/styles/admin-tokens.css`
|
||||
|
||||
- [ ] **Step 1: Create CSS tokens file**
|
||||
|
||||
```css
|
||||
/* Admin Design Tokens */
|
||||
:root {
|
||||
/* Sidebar - Warm Earth Tones */
|
||||
--sidebar-bg: #1c1917;
|
||||
--sidebar-border: #292524;
|
||||
--sidebar-text: #d6d3d1;
|
||||
--sidebar-text-muted: #78716c;
|
||||
--sidebar-text-hover: #fafaf9;
|
||||
--sidebar-active-bg: rgba(5, 150, 105, 0.1);
|
||||
--sidebar-active-border: #059669;
|
||||
|
||||
/* Content Area - Warm Cream */
|
||||
--bg-page: #fdfaf6;
|
||||
--bg-card: #ffffff;
|
||||
--border-subtle: #e7e5e4;
|
||||
--text-primary: #1c1917;
|
||||
--text-secondary: #57534e;
|
||||
--text-muted: #a8a29e;
|
||||
|
||||
/* Accent - Emerald */
|
||||
--accent: #059669;
|
||||
--accent-hover: #047857;
|
||||
--accent-light: #d1fae5;
|
||||
--accent-text: #065f46;
|
||||
|
||||
/* Status */
|
||||
--status-pending: #f59e0b;
|
||||
--status-active: #059669;
|
||||
--status-muted: #a8a29e;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-card: 0 1px 3px rgba(28, 25, 23, 0.06), 0 1px 2px rgba(28, 25, 23, 0.04);
|
||||
--shadow-card-hover: 0 4px 6px rgba(28, 25, 23, 0.08), 0 2px 4px rgba(28, 25, 23, 0.06);
|
||||
|
||||
/* Typography */
|
||||
--font-heading: 'DM Sans', system-ui, sans-serif;
|
||||
--font-body: 'DM Sans', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Spacing */
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.25rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
--space-10: 2.5rem;
|
||||
--space-12: 3rem;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add to global CSS or import in layout**
|
||||
|
||||
Check `src/app/globals.css` and add import at top:
|
||||
```css
|
||||
@import './styles/admin-tokens.css';
|
||||
```
|
||||
|
||||
Run: `ls src/app/globals.css`
|
||||
Expected: File exists
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/admin-tokens.css src/app/globals.css
|
||||
git commit -m "feat: add admin CSS design tokens"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Update AdminSidebar Component
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/admin/AdminSidebar.tsx`
|
||||
|
||||
- [ ] **Step 1: Read current sidebar**
|
||||
|
||||
Run: `cat src/components/admin/AdminSidebar.tsx | head -120`
|
||||
|
||||
- [ ] **Step 2: Update sidebar styling for warm earth tones**
|
||||
|
||||
Replace the sidebar className from:
|
||||
```css
|
||||
bg-white border-r border-stone-200
|
||||
```
|
||||
To:
|
||||
```css
|
||||
bg-stone-900 border-r border-stone-800
|
||||
```
|
||||
|
||||
Update text colors from stone-600/stone-950 to warm equivalents:
|
||||
- inactive text: stone-300 → sidebar-text
|
||||
- active text: emerald-700 → emerald-400
|
||||
- hover: stone-50 → sidebar-text-hover
|
||||
|
||||
Update active state background from `bg-emerald-50` to `bg-emerald-900/20`
|
||||
|
||||
Update role badge background from `bg-stone-50` to `bg-stone-800`
|
||||
|
||||
Update sign out button hover from `bg-stone-50` to `bg-stone-800`
|
||||
|
||||
- [ ] **Step 3: Add Back to Site link**
|
||||
|
||||
In the logo row, after the admin logo link, add:
|
||||
```tsx
|
||||
<a
|
||||
href="/"
|
||||
className="ml-4 text-xs text-stone-500 hover:text-stone-300 transition-colors"
|
||||
>
|
||||
← Back to Site
|
||||
</a>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/admin/AdminSidebar.tsx
|
||||
git commit -m "style: update sidebar to warm earth tones with Back to Site link"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Update Admin Layout Background
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/admin/layout.tsx`
|
||||
|
||||
- [ ] **Step 1: Update background class**
|
||||
|
||||
Change from:
|
||||
```tsx
|
||||
<div className="min-h-screen bg-stone-100 lg:pl-60">
|
||||
```
|
||||
To:
|
||||
```tsx
|
||||
<div className="min-h-screen bg-[var(--bg-page)] lg:pl-60">
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Also update the access denied div**
|
||||
|
||||
Same change for the access denied div.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/admin/layout.tsx
|
||||
git commit -m "style: update admin layout to use warm cream background"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Update Dashboard Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/admin/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Update page background**
|
||||
|
||||
Change `bg-stone-100` to `bg-transparent` (inherits from layout)
|
||||
|
||||
- [ ] **Step 2: Update section headers**
|
||||
|
||||
Change `text-stone-500` (muted labels) to `var(--text-muted)`
|
||||
Change `text-stone-950` to `var(--text-primary)`
|
||||
Change `text-stone-600` to `var(--text-secondary)`
|
||||
|
||||
- [ ] **Step 3: Update cards to use white bg**
|
||||
|
||||
Cards already use `card` class which should be white with shadow. Verify this works with the warm cream background.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/admin/page.tsx
|
||||
git commit -m "style: update dashboard page styling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update Orders Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/admin/orders/page.tsx`
|
||||
- Modify: `src/components/admin/AdminOrdersPanel.tsx`
|
||||
|
||||
- [ ] **Step 1: Remove dark wrapper from orders panel**
|
||||
|
||||
In AdminOrdersPanel, remove the dark wrapper:
|
||||
```tsx
|
||||
// Remove: <div className="min-h-screen bg-zinc-950">
|
||||
// Just return the inner content, let layout handle bg
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update table to warm cream theme**
|
||||
|
||||
Replace all `bg-zinc-900`, `bg-zinc-950`, `border-zinc-800`, `text-zinc-*` with warm equivalents:
|
||||
- `bg-zinc-900` → `bg-white`
|
||||
- `bg-zinc-950` → `bg-stone-50`
|
||||
- `border-zinc-800` → `border-stone-200`
|
||||
- `text-zinc-100` → `text-stone-900`
|
||||
- `text-zinc-200` → `text-stone-700`
|
||||
- `text-zinc-400` → `text-stone-500`
|
||||
- `text-zinc-500` → `text-stone-400`
|
||||
|
||||
- [ ] **Step 3: Add monospace to data cells**
|
||||
|
||||
Add `font-mono` to order ID, phone numbers, dates, amounts.
|
||||
|
||||
- [ ] **Step 4: Update status badges**
|
||||
|
||||
Replace `bg-emerald-950/60 text-emerald-400 border-emerald-800/50` with:
|
||||
```tsx
|
||||
bg-emerald-100 text-emerald-700 border border-emerald-200
|
||||
```
|
||||
|
||||
Same for amber and purple status badges.
|
||||
|
||||
- [ ] **Step 5: Update filter bar**
|
||||
|
||||
Change from dark filter buttons to light theme:
|
||||
```tsx
|
||||
<div className="flex gap-1 rounded-lg border border-stone-200 bg-white p-1">
|
||||
<button className={statusFilter === f ? "bg-emerald-100 text-emerald-700 border border-emerald-200" : "text-stone-500 hover:text-stone-700 hover:bg-stone-50"}>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Update search/select inputs**
|
||||
|
||||
Change `bg-zinc-900` to `bg-white`, `border-zinc-800` to `border-stone-200`, `text-zinc-100` to `text-stone-900`
|
||||
|
||||
- [ ] **Step 7: Update orders page wrapper**
|
||||
|
||||
In orders/page.tsx, the main already has `bg-stone-100 px-6 py-10`. Change to `bg-transparent px-6 py-10`.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/admin/AdminOrdersPanel.tsx src/app/admin/orders/page.tsx
|
||||
git commit -m "style: convert orders table from dark to warm cream theme"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Update Products Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/admin/products/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Change background**
|
||||
|
||||
From `bg-stone-100` to `bg-transparent`
|
||||
|
||||
- [ ] **Step 2: Update button**
|
||||
|
||||
Change `bg-blue-600 hover:bg-blue-500` to `bg-emerald-600 hover:bg-emerald-500` for Add Product button.
|
||||
|
||||
- [ ] **Step 3: Verify card styling**
|
||||
|
||||
The table wrapper already uses `rounded-2xl bg-white shadow-xl`. This should work with cream bg.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/admin/products/page.tsx
|
||||
git commit -m "style: update products page to match admin theme"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Update Communications Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/admin/communications/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Change background**
|
||||
|
||||
From `bg-stone-100` to `bg-transparent`
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/admin/communications/page.tsx
|
||||
git commit -m "style: update communications page to match admin theme"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Update Settings Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/admin/settings/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Change background**
|
||||
|
||||
From `bg-stone-100` to `bg-transparent`
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/admin/settings/page.tsx
|
||||
git commit -m "style: update settings page to match admin theme"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Add Google Fonts to Layout
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/layout.tsx` (root layout, not admin layout)
|
||||
|
||||
- [ ] **Step 1: Add font links**
|
||||
|
||||
In the `<head>` section, add:
|
||||
```tsx
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Apply to body/html**
|
||||
|
||||
Add `style={{ fontFamily: 'var(--font-body)' }}` to the body or html tag.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/layout.tsx
|
||||
git commit -m "style: add DM Sans and JetBrains Mono fonts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Test and Verify
|
||||
|
||||
- [ ] **Step 1: Start dev server**
|
||||
|
||||
Run: `npm run dev`
|
||||
Expected: Dev server starts on port 3000
|
||||
|
||||
- [ ] **Step 2: Check admin dashboard**
|
||||
|
||||
Open: http://localhost:3000/admin
|
||||
Expected: Warm cream background, dark earth-tone sidebar with emerald accent, Back to Site link visible
|
||||
|
||||
- [ ] **Step 3: Check orders page**
|
||||
|
||||
Navigate to Orders
|
||||
Expected: Light cream table with monospace data, warm status badges
|
||||
|
||||
- [ ] **Step 4: Check products page**
|
||||
|
||||
Navigate to Products
|
||||
Expected: Consistent styling, emerald Add Product button
|
||||
|
||||
- [ ] **Step 5: Mobile test**
|
||||
|
||||
Resize browser to mobile width
|
||||
Expected: Sidebar hidden, hamburger visible, tap reveals sidebar overlay
|
||||
|
||||
- [ ] **Step 6: Final commit if all good**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "style: complete admin redesign - warm earth sidebar, cream content, emerald accent"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec Coverage Check
|
||||
|
||||
- [x] Warm earth-tone sidebar with Back to Site
|
||||
- [x] Cream/ivory content background
|
||||
- [x] Emerald accent for active states
|
||||
- [x] Monospace for data tables
|
||||
- [x] Single nav surface (no duplicate headers)
|
||||
- [x] All pages unified
|
||||
- [x] Mobile responsive sidebar
|
||||
@@ -0,0 +1,256 @@
|
||||
# Indian River Direct Data Migration Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Update the app's mock data and Indian River Direct storefront to match the scraped data from indianriverdirect.com - products, brand story, team, seasonal schedules, and pickup-only model.
|
||||
|
||||
**Architecture:** Maintain existing file structure. Update mock-data.ts with real products and brand info. Update IRD storefront pages with authentic content from the source site. No structural changes.
|
||||
|
||||
**Tech Stack:** Next.js, TypeScript, Tailwind CSS (existing)
|
||||
|
||||
---
|
||||
|
||||
## Data Summary from Scrape
|
||||
|
||||
### Products
|
||||
| Product | Price | Type | Notes |
|
||||
|---------|-------|------|-------|
|
||||
| Peaches - 2026 Pre-Order | $55.00 | Pre-order | 25 lb box, Titan Farms SC, June-Aug |
|
||||
| Pecans | $13.00 | Pickup Only | 1 lb bag, Ellis Brothers GA |
|
||||
| Blueberries | TBA | Pickup Only | July only |
|
||||
|
||||
### Brand Identity
|
||||
- **Established:** Since 1985
|
||||
- **Locations:**
|
||||
- Office: 135 North 2nd Street, Fort Pierce, FL 34950
|
||||
- Grove: 23298 Orange Avenue, Fort Pierce, FL
|
||||
- **Acquired:** Fort B Groves (direct source)
|
||||
- **Key Differentiator:** Farm-to-table direct model
|
||||
- **Products Source:** Titan Farms (peaches SC), Ellis Brothers (pecans GA)
|
||||
|
||||
### Seasonal Calendar
|
||||
- **Peaches:** June 12 - August 2, 2026
|
||||
- **Blueberries:** July only
|
||||
- **Citrus:** Winter season
|
||||
|
||||
### Team
|
||||
- Dan (Owner)
|
||||
- Angela (Office Manager/Customer Support)
|
||||
- Mariah (Digital Marketing/Customer Support)
|
||||
- Kenny, Exzavier, Jaime, Ponce (Drivers/Sales)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Update Mock Data
|
||||
|
||||
**Files:**
|
||||
- Modify: `route_commerce-main/src/lib/mock-data.ts`
|
||||
|
||||
- [ ] **Step 1: Update brand info**
|
||||
|
||||
Replace lines 4-7 with:
|
||||
```typescript
|
||||
export const mockBrands = [
|
||||
{ id: "brand-tuxedo", name: "Tuxedo Corn", slug: "tuxedo", accent_color: "#22c55e" },
|
||||
{ id: "brand-ird", name: "Indian River Direct", slug: "indian-river-direct", accent_color: "#f97316" },
|
||||
];
|
||||
```
|
||||
|
||||
Add brand settings after line 93:
|
||||
```typescript
|
||||
export const mockBrandSettingsIRD = {
|
||||
brand_name: "Indian River Direct",
|
||||
founded: "Since 1985",
|
||||
office_address: "135 North 2nd Street, Fort Pierce, FL 34950",
|
||||
grove_address: "23298 Orange Avenue, Fort Pierce, FL",
|
||||
description: "Family-owned and operated, bringing fresh fruit from our groves to your neighborhood since 1985.",
|
||||
phone: "(772) 465-1234",
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update IRD products**
|
||||
|
||||
Replace the IRD products in mockProducts (lines 16-21) with:
|
||||
```typescript
|
||||
// Indian River Direct products (based on indianriverdirect.com)
|
||||
{ id: "prod-ird-peach-1", name: "Peaches - 2026 Pre-Order", price: 55.00, description: "25 lb box of Freestone Peaches from Titan Farms, South Carolina. Hand-picked at peak ripeness, available June-August 2026. Generally 40-60 peaches per box.", type: "peaches", image_url: "https://cdn.shopify.com/s/files/1/0506/2908/3294/files/Untitleddesign.png", shipping_type: "pickup", brand_id: "brand-ird", is_active: true, pickup_type: "scheduled_stop", seasonal: true, season_start: "June", season_end: "August" },
|
||||
{ id: "prod-ird-pecans", name: "Pecans", price: 13.00, description: "1 lb bag of premium pecans from Ellis Brothers Pecans, Georgia. Rich, buttery flavor - perfect for baking, snacking, or recipes.", type: "nuts", image_url: "https://cdn.shopify.com/s/files/1/0506/2908/3294/files/Pecans---INDIANRIVER-42.jpg", shipping_type: "pickup", brand_id: "brand-ird", is_active: true, pickup_type: "scheduled_stop" },
|
||||
{ id: "prod-ird-blueberries", name: "Blueberries", price: 0, description: "Fresh blueberries available in July. Price TBA.", type: "berries", image_url: null, shipping_type: "pickup", brand_id: "brand-ird", is_active: true, pickup_type: "scheduled_stop", seasonal: true, season_start: "July", season_end: "July", price_tba: true },
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add seasonal/weather stops pattern**
|
||||
|
||||
Add a note about seasonal stops structure (append after line 34):
|
||||
```typescript
|
||||
// IRD stops are seasonal - peaches in summer, citrus in winter
|
||||
// Schedule PDFs available at: https://indianriverdirect.com/pages/store-locator
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Remove old citrus products**
|
||||
|
||||
Remove the old citrus products (prod-ird-1 through prod-ird-5) since the site structure is different.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Update Indian River Direct Homepage
|
||||
|
||||
**Files:**
|
||||
- Modify: `route_commerce-main/src/app/indian-river-direct/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Read current file**
|
||||
|
||||
```bash
|
||||
cat route_commerce-main/src/app/indian-river-direct/page.tsx
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update hero section with Peach Farm Direct branding**
|
||||
|
||||
The site shows two brands: "Peach Farm Direct" (summer) and "Indian River Direct" (citrus/winter). Update hero to reflect this.
|
||||
|
||||
- [ ] **Step 3: Add News Board section**
|
||||
|
||||
Add seasonal announcements from the scraped site:
|
||||
```
|
||||
The 2026 Peach Schedule is now Available online. Pre-order is also open, but not required. Walk up customers are welcome.
|
||||
Peach Season begins June 12th through Aug 2nd.
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add "Since 1985" badge and brand story**
|
||||
|
||||
Update the about section with:
|
||||
- "Since 1985" badge
|
||||
- Family-owned messaging
|
||||
- Direct farm-to-table promise
|
||||
|
||||
- [ ] **Step 5: Add testimonials from the site**
|
||||
|
||||
Include the 3 testimonials from the homepage:
|
||||
1. Linda Hurlbut - about grapefruit quality
|
||||
2. Phil Myers - about tangerines and Red Navels
|
||||
3. Bill Prue - about Orrie Tangerines
|
||||
|
||||
- [ ] **Step 6: Add newsletter signup section**
|
||||
|
||||
Match the "Sign up for savings and tour updates" section from the scraped site.
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Update Indian River Direct Layout
|
||||
|
||||
**Files:**
|
||||
- Modify: `route_commerce-main/src/app/indian-river-direct/layout.tsx`
|
||||
|
||||
- [ ] **Step 1: Update metadata**
|
||||
|
||||
```typescript
|
||||
export const metadata: Metadata = {
|
||||
title: "Indian River Direct | Peach & Citrus Truckload",
|
||||
description: "Fresh peaches and citrus delivered straight from our groves to truckload sales in your neighborhood. Family-owned since 1985.",
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Create Store Locator Page
|
||||
|
||||
**Files:**
|
||||
- Create: `route_commerce-main/src/app/indian-river-direct/stores/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Create page matching scraped site's store-locator**
|
||||
|
||||
```typescript
|
||||
// Route: /indian-river-direct/stores
|
||||
// Shows: Links to schedule PDFs and seasonal pickup locations
|
||||
// Content from indianriverdirect.com/pages/store-locator
|
||||
|
||||
export default function StoreLocatorPage() {
|
||||
return (
|
||||
// Hero section with "Keep up to date with Indian River Direct & The Peach Farm Direct"
|
||||
// Links to OH-IN-KY-WV and WI-IL schedule PDFs
|
||||
// Pickup-only notice for pecans
|
||||
// Newsletter signup
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update Brand Config
|
||||
|
||||
**Files:**
|
||||
- Modify: `route_commerce-main/src/config/brands.ts`
|
||||
|
||||
- [ ] **Step 1: Add IRD brand configuration**
|
||||
|
||||
```typescript
|
||||
// Add IRD brand config
|
||||
export const brandConfigIRD = {
|
||||
name: "Indian River Direct",
|
||||
tagline: "Farm-Direct to Your Neighborhood",
|
||||
founded: 1985,
|
||||
accent_color: "#f97316", // Orange for citrus
|
||||
secondary_color: "#ea580c",
|
||||
features: {
|
||||
preorder: true,
|
||||
pickupOnly: true,
|
||||
seasonal: true,
|
||||
},
|
||||
seasonal: {
|
||||
peaches: { start: "June", end: "August" },
|
||||
blueberries: { start: "July", end: "July" },
|
||||
citrus: { start: "November", end: "April" },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Update Admin Product Management
|
||||
|
||||
**Files:**
|
||||
- Modify: `route_commerce-main/src/app/admin/products/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Add product type filtering**
|
||||
|
||||
Filter products by:
|
||||
- `preorder` - peaches need pre-order
|
||||
- `pickup_only` - pecans and blueberries
|
||||
- `seasonal` - shows season dates
|
||||
|
||||
- [ ] **Step 2: Add price TBA support**
|
||||
|
||||
Handle products where price is $0 with "Price TBA" display.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] Run: `npm run dev` and navigate to `/indian-river-direct`
|
||||
- [ ] Verify products show correct prices and descriptions
|
||||
- [ ] Verify seasonal badges appear on peaches
|
||||
- [ ] Verify "Price TBA" shows for blueberries
|
||||
- [ ] Verify pickup-only notice for pecans
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Seasonal Nature:** The IRD site is highly seasonal - peaches in summer, citrus in winter. The app needs to handle products being unavailable/out of season.
|
||||
|
||||
2. **Pickup Model:** Unlike Tuxedo's dual shipping/pickup, IRD uses pickup-only with scheduled truck stops.
|
||||
|
||||
3. **Schedule PDFs:** The site links to PDF schedules for each region - this could be integrated into the stops system.
|
||||
|
||||
4. **Brand Identity:** The "Peach Farm Direct" summer branding and "Indian River Direct" winter branding should be represented in the UI.
|
||||
|
||||
---
|
||||
|
||||
**Plan saved:** `docs/superpowers/plans/2026-01-09-indian-river-data-migration.md`
|
||||
|
||||
**Two execution options:**
|
||||
|
||||
1. **Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
|
||||
|
||||
2. **Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
||||
|
||||
**Which approach?**
|
||||
@@ -0,0 +1,437 @@
|
||||
# Login Flow Rebuild — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Rebuild the admin login/auth flow so it is simple, auditable, and never throws ambiguous errors. Single auth cookie (`rc_auth_uid`), single lookup path in `getAdminUser()`, clear error messages.
|
||||
|
||||
**Architecture:** Login API sets only `rc_auth_uid` (httpOnly, secure in prod). `getAdminUser()` reads `rc_auth_uid` → REST lookup via service role apikey → return AdminUser or null. All other cookie paths (`rc_access_token`, `rc_uid`, `rc_auth_token`, `sb-*` session) are removed from the admin auth flow. `getAdminUser()` NEVER throws — it always returns AdminUser or null.
|
||||
|
||||
**Tech Stack:** Next.js 16 App Router · Supabase Auth · Vercel (Node.js runtime) · TypeScript
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **`src/app/api/login/route.ts`** — Login API: authenticate + set ONLY `rc_auth_uid` cookie
|
||||
- **`src/lib/admin-permissions.ts`** — `getAdminUser()`: ONLY `rc_auth_uid` path, never throws
|
||||
- **`src/app/admin/layout.tsx`** — AdminLayout: show "Access Denied" not "Auth Error", log actual errors
|
||||
- **`src/middleware.ts`** — Middleware: only check `rc_auth_uid`, remove all other auth cookie checks
|
||||
- **`src/app/admin/test-auth/page.tsx`** — New debug page: shows all cookie values + getAdminUser result
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Simplify `/api/login` — Set Only `rc_auth_uid`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/api/login/route.ts:96-113`
|
||||
|
||||
- [ ] **Step 1: Replace cookie setting block**
|
||||
|
||||
Replace the cookie setting section (lines 96-113) with:
|
||||
|
||||
```typescript
|
||||
console.log("[/api/login] setting cookies for user:", data.user.id);
|
||||
const cookieStore = await cookies();
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const cookieOpts = {
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
...(isProd ? { secure: true } : {}),
|
||||
};
|
||||
|
||||
// Only rc_auth_uid — this is the single auth cookie for admin.
|
||||
// Middleware checks rc_auth_uid; getAdminUser() looks up admin_users by this UID.
|
||||
cookieStore.set("rc_auth_uid", data.user.id, cookieOpts);
|
||||
console.log("[/api/login] rc_auth_uid set:", data.user.id);
|
||||
|
||||
console.log("[/api/login] returning success");
|
||||
return NextResponse.json({ success: true, userId: data.user.id });
|
||||
```
|
||||
|
||||
**Note:** Remove `rc_access_token` and `rc_uid` cookie setting entirely. `rc_auth_uid` alone is sufficient — it contains the Supabase auth UID which is what both middleware and `getAdminUser()` use.
|
||||
|
||||
- [ ] **Step 2: Verify no other cookies are set in this file**
|
||||
|
||||
Search for all `cookieStore.set` calls in the file — confirm only `rc_auth_uid` is set.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/api/login/route.ts
|
||||
git commit -m "fix: /api/login only sets rc_auth_uid — removes rc_access_token/rc_uid cookies
|
||||
|
||||
rc_auth_uid is the single auth cookie. Middleware checks it directly;
|
||||
getAdminUser() uses it as the UID for admin_users lookup. No need for
|
||||
rc_access_token or rc_uid in the admin auth flow."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Simplify `getAdminUser()` — Single Path, Never Throws
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/admin-permissions.ts:1-233`
|
||||
|
||||
- [ ] **Step 1: Replace the entire getAdminUser function**
|
||||
|
||||
Replace the current `getAdminUser()` function (lines 24-182) with this simplified version:
|
||||
|
||||
```typescript
|
||||
export async function getAdminUser(): Promise<AdminUser | null> {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
console.error("[getAdminUser] Missing env vars");
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Dev session bypass (local only — NEVER in production) ────────
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const devSession = cookieStore.get("dev_session")?.value;
|
||||
if (devSession && (devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee")) {
|
||||
console.log("[getAdminUser] dev_session active — using dev auth");
|
||||
return buildDevAdminUser(devSession);
|
||||
}
|
||||
}
|
||||
|
||||
// ── rc_auth_uid is the ONLY auth cookie for admin ───────────────
|
||||
const rcAuthUid = cookieStore.get("rc_auth_uid")?.value;
|
||||
if (!rcAuthUid) {
|
||||
console.log("[getAdminUser] no rc_auth_uid cookie — returning null");
|
||||
return null;
|
||||
}
|
||||
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!serviceKey) {
|
||||
console.error("[getAdminUser] no SUPABASE_SERVICE_ROLE_KEY — cannot look up admin_users");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look up admin_users by rc_auth_uid (the Supabase auth user UID)
|
||||
const rawLookup = await fetch(
|
||||
`${supabaseUrl}/rest/v1/admin_users?user_id=eq.${rcAuthUid}&limit=1`,
|
||||
{ headers: { apikey: serviceKey, "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
if (!rawLookup.ok) {
|
||||
console.error("[getAdminUser] admin_users lookup failed:", rawLookup.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const adminUsers = rawLookup.ok ? await rawLookup.json().catch(() => null) : null;
|
||||
if (!adminUsers || adminUsers.length === 0) {
|
||||
console.log("[getAdminUser] no admin_users record for uid:", rcAuthUid);
|
||||
// Auto-create platform_admin for this user
|
||||
const { createAdminUser } = await import("@/lib/admin-permissions-service");
|
||||
const newAdmin = await createAdminUser(rcAuthUid, "platform_admin", null);
|
||||
if (!newAdmin) {
|
||||
console.error("[getAdminUser] createAdminUser returned null");
|
||||
return null;
|
||||
}
|
||||
return buildAdminUser(newAdmin as Record<string, unknown>);
|
||||
}
|
||||
|
||||
const adminUser = adminUsers[0];
|
||||
if (!adminUser.active) {
|
||||
console.log("[getAdminUser] admin_users record is inactive:", rcAuthUid);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("[getAdminUser] success — uid:", rcAuthUid, "role:", adminUser.role);
|
||||
return buildAdminUser(adminUser);
|
||||
|
||||
} catch (err) {
|
||||
// NEVER throw — always return null on error
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error("[getAdminUser] unexpected error:", msg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify build**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/admin-permissions.ts
|
||||
git commit -m "refactor: simplify getAdminUser() to single rc_auth_uid path
|
||||
|
||||
Removes rcAccessToken validation path, the createServerClient path, and
|
||||
all fallback logic. getAdminUser() now:
|
||||
1. Checks dev_session (dev only)
|
||||
2. Reads rc_auth_uid cookie only
|
||||
3. Looks up admin_users via REST with apikey header (no Bearer token)
|
||||
4. Auto-creates admin_users if missing
|
||||
5. NEVER throws — returns AdminUser or null
|
||||
|
||||
This eliminates all paths that could throw TypeError from
|
||||
createServerClient or Authorization header issues."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Fix AdminLayout — "Access Denied" Not "Auth Error"
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/admin/layout.tsx:7-27`
|
||||
|
||||
- [ ] **Step 1: Replace the error display**
|
||||
|
||||
Replace the `catch (e)` block in AdminLayout (lines 13-27) with:
|
||||
|
||||
```typescript
|
||||
} catch (e: unknown) {
|
||||
const errMsg = e instanceof Error ? e.message : String(err);
|
||||
console.error("[AdminLayout] getAdminUser threw:", errMsg);
|
||||
return (
|
||||
<>
|
||||
<AdminHeader userRole={null} routeTraceEnabled={false} />
|
||||
<div className="min-h-screen bg-zinc-950 flex items-center justify-center">
|
||||
<div className="text-center max-w-md">
|
||||
<h2 className="text-xl font-bold text-white mb-2">Access Denied</h2>
|
||||
<p className="text-zinc-400 text-sm">Your session could not be verified. Please log out and log back in.</p>
|
||||
<p className="text-zinc-600 text-xs mt-2 font-mono">{errMsg}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify build**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/admin/layout.tsx
|
||||
git commit -m "fix: AdminLayout shows 'Access Denied' not 'Auth Error' on getAdminUser throw
|
||||
|
||||
Also shows the actual error message in a monospace block for debugging.
|
||||
'Auth Error' was vague and unhelpful — 'Access Denied' clearly indicates
|
||||
the session verification failed without implying a system brokenness."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Simplify Middleware — Only Check `rc_auth_uid`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/middleware.ts:17-73`
|
||||
|
||||
- [ ] **Step 1: Replace auth resolution block**
|
||||
|
||||
The current middleware has complex multi-path auth resolution (dev_session, rcAuthUid, rcAuthToken, rcAccessToken, Supabase SSR). Replace the auth resolution section (lines 27-55) with:
|
||||
|
||||
```typescript
|
||||
const devSession = request.cookies.get("dev_session")?.value;
|
||||
const isDevMode = devSession === "platform_admin" || devSession === "brand_admin" || devSession === "store_employee";
|
||||
const rcAuthUid = request.cookies.get("rc_auth_uid")?.value;
|
||||
|
||||
let authUid: string | null = null;
|
||||
|
||||
if (isDevMode) {
|
||||
// Dev session only valid in development
|
||||
authUid = DEV_UID;
|
||||
} else if (rcAuthUid) {
|
||||
// rc_auth_uid is set by /api/login — treat as authenticated
|
||||
authUid = rcAuthUid;
|
||||
}
|
||||
// No rc_auth_uid in production → authUid stays null → redirect to /login
|
||||
```
|
||||
|
||||
The rest of the middleware (redirect logic, matcher) stays unchanged.
|
||||
|
||||
- [ ] **Step 2: Verify build**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/middleware.ts
|
||||
git commit -m "fix: middleware only checks rc_auth_uid for production auth
|
||||
|
||||
Removes all other auth cookie checks (rc_auth_token, rc_access_token).
|
||||
Middleware now: dev_session (dev only) OR rc_auth_uid → authenticated.
|
||||
No more complex multi-path resolution that could fail silently."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Add `/admin/test-auth` Debug Page
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app/admin/test-auth/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Create the debug page**
|
||||
|
||||
Create `src/app/admin/test-auth/page.tsx`:
|
||||
|
||||
```typescript
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function TestAuthPage() {
|
||||
const cookieStore = await cookies();
|
||||
const allCookies = cookieStore.getAll();
|
||||
|
||||
let adminUser = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
adminUser = await getAdminUser();
|
||||
} catch (e: unknown) {
|
||||
error = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950 p-8 text-white">
|
||||
<h1 className="text-2xl font-bold mb-6 text-emerald-400">Auth Debug</h1>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">All Cookies ({allCookies.length})</h2>
|
||||
<pre className="bg-black/50 p-4 rounded-xl text-xs overflow-auto max-h-64">
|
||||
{allCookies.map(c => `${c.name}=${c.value.slice(0, 80)}${c.value.length > 80 ? "..." : ""}`).join("\n") || "(none)"}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">rc_auth_uid</h2>
|
||||
<p className="text-lg font-mono">
|
||||
{allCookies.some(c => c.name === "rc_auth_uid")
|
||||
? <span className="text-emerald-400">SET — {allCookies.find(c => c.name === "rc_auth_uid")?.value}</span>
|
||||
: <span className="text-red-400">NOT SET</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">rc_access_token</h2>
|
||||
<p className="text-lg font-mono">
|
||||
{allCookies.some(c => c.name === "rc_access_token")
|
||||
? <span className="text-yellow-400">SET (not needed)</span>
|
||||
: <span className="text-zinc-500">NOT SET (OK)</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">getAdminUser() result</h2>
|
||||
{error ? (
|
||||
<div>
|
||||
<p className="text-red-400 font-bold">ERROR</p>
|
||||
<pre className="bg-red-950/50 p-4 rounded-xl text-sm mt-2">{error}</pre>
|
||||
</div>
|
||||
) : adminUser ? (
|
||||
<div>
|
||||
<p className="text-emerald-400 font-bold">AUTHENTICATED</p>
|
||||
<pre className="bg-black/50 p-4 rounded-xl text-sm mt-2 overflow-auto">
|
||||
{JSON.stringify({
|
||||
id: adminUser.id,
|
||||
user_id: adminUser.user_id,
|
||||
role: adminUser.role,
|
||||
brand_id: adminUser.brand_id,
|
||||
active: adminUser.active,
|
||||
}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-red-400">NOT AUTHENTICATED — null returned</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-4 border-t border-zinc-800">
|
||||
<h2 className="text-xs font-semibold text-stone-400 uppercase mb-2">Quick Actions</h2>
|
||||
<div className="flex gap-4">
|
||||
<a href="/admin" className="px-4 py-2 bg-emerald-600 text-white rounded-xl text-sm font-bold hover:bg-emerald-500">
|
||||
Go to Admin
|
||||
</a>
|
||||
<form action="/api/logout" method="POST">
|
||||
<button type="submit" className="px-4 py-2 bg-zinc-800 text-white rounded-xl text-sm font-bold hover:bg-zinc-700">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify build**
|
||||
|
||||
Run: `npx tsc --noEmit`
|
||||
Expected: No errors
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app/admin/test-auth/page.tsx
|
||||
git commit -m "feat: add /admin/test-auth debug page for auth troubleshooting
|
||||
|
||||
Shows all cookie values, rc_auth_uid status, rc_access_token status,
|
||||
and getAdminUser() result. Helps diagnose auth issues quickly in prod."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Deploy and Verify
|
||||
|
||||
- [ ] **Step 1: Push all commits**
|
||||
|
||||
```bash
|
||||
git push
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Deploy to Vercel production**
|
||||
|
||||
```bash
|
||||
npx vercel --prod
|
||||
```
|
||||
|
||||
Wait for deployment to be READY.
|
||||
|
||||
- [ ] **Step 3: Verification checklist**
|
||||
|
||||
In an incognito browser window:
|
||||
1. Go to `/admin` — should redirect to `/login`
|
||||
2. Clear all cookies
|
||||
3. Go to `/login` — should show login form with no errors
|
||||
4. Log in with real credentials
|
||||
5. Should land on `/admin` — no "Auth Error" or "Access Denied"
|
||||
6. Go to `/admin/test-auth` — should show:
|
||||
- `rc_auth_uid` = SET
|
||||
- `rc_access_token` = NOT SET
|
||||
- `getAdminUser()` = AUTHENTICATED
|
||||
7. Logout — should clear `rc_auth_uid`
|
||||
8. Try accessing `/admin` again — should redirect to `/login`
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
After each fix, clear all cookies and test login flow end-to-end:
|
||||
|
||||
1. **Clear cookies** in browser dev tools (Application tab → Cookies → Delete all)
|
||||
2. **Login** at `/login` with known-valid credentials
|
||||
3. **Land on `/admin`** — should show admin dashboard, not an error
|
||||
4. **Check `/admin/test-auth`** — `rc_auth_uid` SET, `rc_access_token` NOT SET, AUTHENTICATED
|
||||
5. **Logout** and verify redirect to `/login`
|
||||
|
||||
If any step fails, stop and diagnose before proceeding to next task.
|
||||
@@ -0,0 +1,270 @@
|
||||
# Login Page Production Failure — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Diagnose why the login page works in development but fails in production, then implement a fix.
|
||||
|
||||
**Architecture:** The login flow involves three layers: (1) the login page form posting to `/api/login`, (2) the API route validating credentials and setting auth cookies, (3) middleware reading cookies to restore sessions. Production failures typically stem from cookie security settings, environment variable mismatches, or proxy/hosting platform differences.
|
||||
|
||||
**Tech Stack:** Next.js 16 App Router · Supabase Auth · Vercel (production) · Node.js
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **`src/app/login/page.tsx`** — Login form UI, posts to `/api/login`
|
||||
- **`src/app/api/login/route.ts`** — Validates credentials, creates admin_users, sets `rc_access_token` and `rc_uid` cookies
|
||||
- **`src/middleware.ts`** — Reads auth cookies (`rc_auth_uid`, `rc_auth_token`, `dev_session`) to restore session
|
||||
- **`src/lib/admin-permissions.ts`** — `getAdminUser()` resolves auth from cookies, dev bypass, proxy UID, Supabase token
|
||||
- **`src/lib/admin-permissions-service.ts`** — Creates admin_users via Supabase REST API (apikey-only, no Authorization header)
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic Tasks
|
||||
|
||||
### Task 1: Reproduce and Instrument the Production Failure
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/api/login/route.ts:1-120`
|
||||
- Modify: `src/middleware.ts:1-100`
|
||||
|
||||
- [ ] **Step 1: Add diagnostic logging to /api/login route**
|
||||
|
||||
Open `src/app/api/login/route.ts` and add console logging at each failure point. The route currently handles three failure modes: (1) credentials rejected by Supabase, (2) admin_users lookup fails, (3) cookie setting fails.
|
||||
|
||||
```typescript
|
||||
// Add at the top of the POST handler, after extracting formData
|
||||
console.log("[/api/login] isProd:", isProd, "NODE_ENV:", process.env.NODE_ENV);
|
||||
console.log("[/api/login] Email:", email, "Password length:", password?.length);
|
||||
console.log("[/api/login] Supabase URL:", supabaseUrl ? "set" : "MISSING");
|
||||
console.log("[/api/login] Anon key:", supabaseAnonKey ? "set" : "MISSING");
|
||||
console.log("[/api/login] Service role:", serviceRoleKey ? "set" : "MISSING");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add diagnostic logging to middleware auth resolution**
|
||||
|
||||
Open `src/middleware.ts` and log each auth resolution path at the start of the middleware function:
|
||||
|
||||
```typescript
|
||||
console.log("[middleware] pathname:", request.nextUrl.pathname);
|
||||
console.log("[middleware] dev_session:", devSession);
|
||||
console.log("[middleware] rc_auth_uid:", rcAuthUid);
|
||||
console.log("[middleware] rc_auth_token:", rcAuthToken ? "present" : "MISSING");
|
||||
console.log("[middleware] rc_access_token:", rcAccessToken ? "present" : "MISSING");
|
||||
console.log("[middleware] isProd:", process.env.NODE_ENV === "production");
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Deploy and reproduce**
|
||||
|
||||
Run `npm run build && npm run start` locally in production mode (or deploy to Vercel) and attempt login. Capture all console output from both the API route and middleware.
|
||||
|
||||
Expected: You will see which path fails — either the Supabase signInWithPassword call, the admin_users creation, or the cookie setting.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Check Cookie Configuration Differences
|
||||
|
||||
**Files:**
|
||||
- Review: `src/app/api/login/route.ts:90-110` (cookie options)
|
||||
- Review: `src/middleware.ts:23-54` (cookie reading)
|
||||
- Review: `src/app/api/water-admin-auth/route.ts` (another cookie example)
|
||||
|
||||
- [ ] **Step 1: Document cookie options in each file**
|
||||
|
||||
The `/api/login` route sets cookies with these options (lines 99-105):
|
||||
```typescript
|
||||
const cookieOpts = {
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
...(isProd ? { secure: true } : {}), // secure:true only in production
|
||||
};
|
||||
```
|
||||
|
||||
In production (HTTPS), `secure: true` is required for cookies to work. In development (HTTP), `secure: false` is needed. The conditional `...(isProd ? { secure: true } : {})` handles this.
|
||||
|
||||
- [ ] **Step 2: Verify the `isProd` variable is correctly computed**
|
||||
|
||||
Find where `isProd` is defined in `src/app/api/login/route.ts`. It should be:
|
||||
```typescript
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
```
|
||||
|
||||
If `NODE_ENV` is set differently in production (e.g., not explicitly set, or set to "production" but the Next.js server runs differently on Vercel), this could cause the cookie to be set without `secure: true` in production, making the cookie get rejected by the browser.
|
||||
|
||||
- [ ] **Step 3: Check if `SameSite=lax` causes issues on Vercel**
|
||||
|
||||
On some Vercel configurations, `SameSite=lax` combined with `secure: true` can cause issues if the domain has redirects or if there's a proxy in front. Try changing to `sameSite: "none"` with `secure: true` in production (requires HTTPS), or investigate if the cookie domain needs to be explicitly set.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Verify Environment Variables in Production
|
||||
|
||||
**Files:**
|
||||
- Review: `src/app/api/login/route.ts:20-35`
|
||||
- Review: `.env.example`
|
||||
- Review: `.env.local` (development values)
|
||||
|
||||
- [ ] **Step 1: List all env vars used in the login flow**
|
||||
|
||||
The login route uses:
|
||||
```typescript
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Check Vercel environment variables**
|
||||
|
||||
In Vercel dashboard → Settings → Environment Variables, verify:
|
||||
1. `NEXT_PUBLIC_SUPABASE_URL` is set (not `NEXT_PUBLIC_` prefix in some cases)
|
||||
2. `NEXT_PUBLIC_SUPABASE_ANON_KEY` is set
|
||||
3. `SUPABASE_SERVICE_ROLE_KEY` is set (this MUST be a server-side only variable, never exposed to client)
|
||||
|
||||
**Common issue:** `SUPABASE_SERVICE_ROLE_KEY` is set but the Next.js server component can't read it because Vercel's edge runtime doesn't have access to server-side env vars the same way.
|
||||
|
||||
- [ ] **Step 3: Verify Supabase URL format**
|
||||
|
||||
The Supabase URL must be the project URL (e.g., `https://xxxxx.supabase.co`), not the internal postgres URL. Check that the production env var matches the development value.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Check Middleware Auth Resolution
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/middleware.ts:23-54`
|
||||
|
||||
- [ ] **Step 1: Instrument the middleware auth resolution path**
|
||||
|
||||
Add logging to each branch of the auth resolution:
|
||||
```typescript
|
||||
if (isDevMode) {
|
||||
console.log("[middleware] Using dev mode, DEV_UID:", DEV_UID);
|
||||
authUid = DEV_UID;
|
||||
} else if (rcAuthUid === DEV_FORCE_UID) {
|
||||
console.log("[middleware] Using dev force UID");
|
||||
authUid = DEV_FORCE_UID;
|
||||
} else if (rcAuthUid) {
|
||||
console.log("[middleware] Using rc_auth_uid:", rcAuthUid);
|
||||
authUid = rcAuthUid;
|
||||
} else if (rcAccessToken) {
|
||||
console.log("[middleware] Attempting rc_access_token validation");
|
||||
// JWT decode and validate...
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Check the `trust proxy` setting**
|
||||
|
||||
If the application runs behind a Vercel proxy or CDN, the `x-forwarded-proto` header may need to be trusted. In Next.js, add to `next.config.ts`:
|
||||
```typescript
|
||||
async headers() {
|
||||
return [{
|
||||
source: '/(.*)',
|
||||
headers: [{ key: 'X-Forwarded-Proto', value: 'https' }],
|
||||
}];
|
||||
},
|
||||
```
|
||||
|
||||
Or in `middleware.ts`, check `request.headers.get('x-forwarded-proto')`.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Check Supabase Auth Configuration
|
||||
|
||||
**Files:**
|
||||
- Review: Supabase Dashboard → Authentication → Settings
|
||||
- Review: `src/lib/supabase.ts`
|
||||
|
||||
- [ ] **Step 1: Verify Supabase site URL in dashboard**
|
||||
|
||||
In Supabase Dashboard → Authentication → Settings → Site URL, verify the production URL matches:
|
||||
- Dev: `http://localhost:3000`
|
||||
- Prod: `https://your-production-domain.com`
|
||||
|
||||
If the Site URL doesn't match, Supabase may reject the auth redirect.
|
||||
|
||||
- [ ] **Step 2: Check redirect URLs in Supabase**
|
||||
|
||||
Supabase → Authentication → URL Configuration → Redirect URLs should include:
|
||||
- `https://your-production-domain.com/login`
|
||||
- `https://your-production-domain.com/admin/*`
|
||||
|
||||
Any redirect URL mismatch causes silent failures.
|
||||
|
||||
- [ ] **Step 3: Check Supabase anon key permissions**
|
||||
|
||||
The anon key is used client-side but also in server components. Verify the anon key hasn't been rotated or the project hasn't been migrated to a new Supabase instance.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Implement Fixes Based on Diagnosis
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/api/login/route.ts`
|
||||
- Modify: `src/middleware.ts`
|
||||
|
||||
- [ ] **Step 1: If cookie secure flag is the issue**
|
||||
|
||||
Update cookie options to explicitly handle all cases:
|
||||
```typescript
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
const cookieOpts = {
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
httpOnly: true,
|
||||
sameSite: isProd ? "none" : "lax", // none requires secure
|
||||
secure: isProd, // true in production (HTTPS), false in dev
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: If env vars are missing in Vercel edge runtime**
|
||||
|
||||
Move critical env var reading to a function that handles missing vars gracefully:
|
||||
```typescript
|
||||
function getRequiredEnv(key: string): string {
|
||||
const value = process.env[key];
|
||||
if (!value) {
|
||||
console.error(`[login] Missing required env var: ${key}`);
|
||||
throw new Error(`Missing required environment variable: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: If middleware cookie reading is failing**
|
||||
|
||||
The middleware runs on the Vercel Edge Runtime which has limited cookie access. Ensure `rc_auth_uid` and `rc_auth_token` cookies are set with appropriate domain and path:
|
||||
```typescript
|
||||
cookieStore.set("rc_auth_uid", uid, {
|
||||
httpOnly: true,
|
||||
secure: isProd,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
// No domain specified — uses current hostname
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
After each fix:
|
||||
|
||||
1. **Clear all cookies** in browser dev tools (Application tab → Cookies → Delete all)
|
||||
2. **Restart the production server** or redeploy to Vercel
|
||||
3. **Attempt login** with a known-valid account
|
||||
4. **Check browser dev tools** → Network tab → `/api/login` request
|
||||
- Response status should be 200 or 3xx (redirect), not 400/401/500
|
||||
5. **Check Application tab** → Cookies for `rc_access_token` and `rc_uid`
|
||||
6. **Check console logs** in both the browser and server for errors
|
||||
|
||||
---
|
||||
|
||||
## Execution OPTIONS
|
||||
|
||||
**Option A (Recommended):** Use `superpowers:subagent-driven-development` — dispatch a subagent to work through each diagnostic task with two-stage review.
|
||||
|
||||
**Option B:** Use `superpowers:systematic-debugging` first to pinpoint the exact failure point, then implement targeted fixes.
|
||||
|
||||
**Which approach?**
|
||||
@@ -0,0 +1,172 @@
|
||||
# Admin Pages Redesign — Design Spec
|
||||
|
||||
**Date:** 2025-01-14
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Unify the admin pages with a consistent design system: warm earth-tone dark sidebar, cream/ivory content area, emerald accent, monospace for data. Eliminate duplicate header/sidebar navigation — sidebar is the single nav surface.
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### Colors
|
||||
|
||||
```css
|
||||
/* Sidebar */
|
||||
--sidebar-bg: #1c1917; /* stone-900, warm dark */
|
||||
--sidebar-border: #292524; /* stone-800 */
|
||||
--sidebar-text: #d6d3d1; /* stone-300 */
|
||||
--sidebar-text-muted: #78716c; /* stone-500 */
|
||||
--sidebar-text-hover: #fafaf9; /* stone-50 */
|
||||
|
||||
/* Content Area */
|
||||
--bg-page: #fdfaf6; /* warm cream */
|
||||
--bg-card: #ffffff; /* white cards */
|
||||
--border-subtle: #e7e5e4; /* stone-200 */
|
||||
--text-primary: #1c1917; /* stone-900 */
|
||||
--text-secondary: #57534e; /* stone-600 */
|
||||
--text-muted: #a8a29e; /* stone-400 */
|
||||
|
||||
/* Accent */
|
||||
--accent: #059669; /* emerald-600 */
|
||||
--accent-hover: #047857; /* emerald-700 */
|
||||
--accent-light: #d1fae5; /* emerald-100 */
|
||||
--accent-text: #065f46; /* emerald-800 */
|
||||
|
||||
/* Status Colors */
|
||||
--status-pending: #f59e0b; /* amber-500 */
|
||||
--status-active: #059669; /* emerald-600 */
|
||||
--status-muted: #a8a29e; /* stone-400 */
|
||||
|
||||
/* Shadows */
|
||||
--shadow-card: 0 1px 3px rgba(28, 25, 23, 0.06), 0 1px 2px rgba(28, 25, 23, 0.04);
|
||||
--shadow-card-hover: 0 4px 6px rgba(28, 25, 23, 0.08), 0 2px 4px rgba(28, 25, 23, 0.06);
|
||||
```
|
||||
|
||||
### Typography
|
||||
|
||||
```css
|
||||
/* Headings: DM Sans (warm humanist) */
|
||||
--font-heading: 'DM Sans', system-ui, sans-serif;
|
||||
|
||||
/* Body: DM Sans */
|
||||
--font-body: 'DM Sans', system-ui, sans-serif;
|
||||
|
||||
/* Monospace: JetBrains Mono (for data tables, IDs, codes) */
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Scale */
|
||||
--text-xs: 0.75rem; /* 12px */
|
||||
--text-sm: 0.875rem; /* 14px */
|
||||
--text-base: 1rem; /* 16px */
|
||||
--text-lg: 1.125rem; /* 18px */
|
||||
--text-xl: 1.25rem; /* 20px */
|
||||
--text-2xl: 1.5rem; /* 24px */
|
||||
--text-3xl: 1.875rem; /* 30px */
|
||||
--text-4xl: 2.25rem; /* 36px */
|
||||
```
|
||||
|
||||
### Spacing & Border Radius
|
||||
|
||||
```css
|
||||
--radius-sm: 0.375rem; /* 6px */
|
||||
--radius-md: 0.5rem; /* 8px */
|
||||
--radius-lg: 0.75rem; /* 12px */
|
||||
--radius-xl: 1rem; /* 16px */
|
||||
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
--space-10: 2.5rem; /* 40px */
|
||||
--space-12: 3rem; /* 48px */
|
||||
```
|
||||
|
||||
### Sidebar Layout
|
||||
|
||||
- Width: 240px (desktop), off-canvas on mobile
|
||||
- Full height, fixed position
|
||||
- Logo/brand area at top
|
||||
- Nav items with icons + labels
|
||||
- Settings collapsible section
|
||||
- User role badge + sign out at bottom
|
||||
- "Back to Site" link in logo area
|
||||
- Active state: emerald background tint + emerald left border + emerald dot indicator
|
||||
|
||||
### Content Area
|
||||
|
||||
- Starts at `lg:pl-60` (sidebar width) on desktop
|
||||
- Mobile: full width with hamburger trigger for sidebar
|
||||
- Max content width: 1280px centered
|
||||
- Page header with breadcrumb (optional per page)
|
||||
- Card-based layouts for data
|
||||
- Generous padding (space-10 or space-12 vertical)
|
||||
|
||||
## Pages to Update
|
||||
|
||||
1. **Admin Dashboard** (`/admin/page.tsx`) — Already has good structure, minor refinements
|
||||
2. **Orders** (`/admin/orders/page.tsx`) — Convert from dark zinc to warm cream + monospace table
|
||||
3. **Products** (`/admin/products/page.tsx`) — Light theme, consistent header
|
||||
4. **Communications** (`/admin/communications/page.tsx`) — Light theme, consistent header
|
||||
5. **Settings** (`/admin/settings/page.tsx`) — Light theme, consistent header
|
||||
6. **All sub-pages** — Ensure consistent header style if they have one
|
||||
|
||||
## Component Inventory
|
||||
|
||||
### Sidebar Navigation
|
||||
- Logo with "Back to Site" link
|
||||
- Nav items: icon + label, active state with emerald indicator
|
||||
- Collapsible Settings section
|
||||
- Role badge
|
||||
- Sign out button
|
||||
|
||||
### Page Header
|
||||
- Breadcrumb (optional): "Admin / Section"
|
||||
- Page title: text-3xl, font-heading, bold
|
||||
- Page description: text-secondary
|
||||
- Action buttons (right-aligned)
|
||||
|
||||
### Data Table
|
||||
- Monospace font for all data cells
|
||||
- Subtle border between rows
|
||||
- Sticky header row
|
||||
- Hover state on rows
|
||||
- Status badges with appropriate colors
|
||||
- Action buttons in last column
|
||||
|
||||
### Cards
|
||||
- White background
|
||||
- Subtle shadow
|
||||
- Rounded-xl corners
|
||||
- Consistent padding
|
||||
|
||||
### Buttons
|
||||
- Primary: emerald bg, white text
|
||||
- Secondary: white bg, stone border, stone text
|
||||
- Ghost: transparent, hover shows bg
|
||||
|
||||
### Form Inputs
|
||||
- White background
|
||||
- Stone border
|
||||
- Focus: emerald border
|
||||
- Monospace for codes/IDs
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. Create CSS variables file at `src/styles/admin-tokens.css`
|
||||
2. Import tokens in admin layout
|
||||
3. Update sidebar component to use new styling
|
||||
4. Update each page progressively
|
||||
5. Orders table needs most work — dark theme → warm cream with monospace
|
||||
6. Ensure mobile responsive behavior
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Backend logic changes
|
||||
- Database schema changes
|
||||
- Authentication flow changes
|
||||
- Creating new pages
|
||||
- Changing navigation items (already locked)
|
||||
Reference in New Issue
Block a user