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
14 changes: 3 additions & 11 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,12 @@
# -----------------------------------------------------------------------------
# Get these from your Supabase project: Settings → API
#
# To switch between demo and production, point these at different Supabase
# projects. A demo project can be seeded with supabase/seed.sql for test data.
#
# Development: firefly-dev project
# Staging/QA: firefly-staging project (set in Vercel Preview env vars)
# Production: firefly-prod project (set in Vercel Production env vars)
#
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

# -----------------------------------------------------------------------------
# Demo Mode
# -----------------------------------------------------------------------------
# Controls whether the app uses hardcoded demo users or real authentication.
#
# Development: true (local dev uses demo IDs, no login required)
# Staging/QA: true (Vercel preview deployments run in demo mode)
# Production: false (real auth required, no demo IDs)
#
NEXT_PUBLIC_DEMO_MODE=true
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,3 @@ jobs:
env:
NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder-anon-key
NEXT_PUBLIC_DEMO_MODE: 'true'
28 changes: 7 additions & 21 deletions docs/ENVIRONMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ Project Firefly uses three environments, each with its own Supabase project and

## Overview

| Environment | Trigger | Supabase Project | Demo Mode | URL |
|---|---|---|---|---|
| **Development** | `npm run dev` | `firefly-dev` | `true` | `localhost:3000` |
| **Staging / QA** | PR opened → Vercel preview | `firefly-staging` | `true` | `pr-123.vercel.app` |
| **Production** | Merge to `main` → Vercel prod | `firefly-prod` | `false` | `firefly.app` |
| Environment | Trigger | Supabase Project | URL |
|---|---|---|---|
| **Development** | `npm run dev` | `firefly-dev` | `localhost:3000` |
| **Staging / QA** | PR opened → Vercel preview | `firefly-staging` | `pr-123.vercel.app` |
| **Production** | Merge to `main` → Vercel prod | `firefly-prod` | `firefly.app` |

For demo purposes, use a separate Supabase project with seeded data (`supabase/seed.sql`). The same frontend code is used — only the Supabase URL and anon key differ.

## Supabase Projects

Expand All @@ -28,7 +30,6 @@ Create three separate Supabase projects. Each project is fully isolated with its
|---|---|---|---|
| Schema (migrations) | Yes | Yes | Yes |
| Seed data (demo vendor, menu) | Yes | Yes | **No** |
| Demo user IDs in data | Yes | Yes | **No** |

## Vercel Configuration

Expand All @@ -42,7 +43,6 @@ Every PR automatically gets a preview deployment. These use staging config:
|---|---|
| `NEXT_PUBLIC_SUPABASE_URL` | `https://firefly-staging-ref.supabase.co` |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Staging anon key |
| `NEXT_PUBLIC_DEMO_MODE` | `true` |

Set these with the **Preview** environment checkbox in Vercel.

Expand All @@ -54,7 +54,6 @@ Merges to `main` auto-deploy to production:
|---|---|
| `NEXT_PUBLIC_SUPABASE_URL` | `https://firefly-prod-ref.supabase.co` |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Prod anon key |
| `NEXT_PUBLIC_DEMO_MODE` | `false` |

Set these with the **Production** environment checkbox in Vercel.

Expand All @@ -69,22 +68,9 @@ Set these with the **Production** environment checkbox in Vercel.
```
NEXT_PUBLIC_SUPABASE_URL=https://your-dev-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-dev-anon-key
NEXT_PUBLIC_DEMO_MODE=true
```

3. Start the dev server:
```bash
npm run dev
```

## Demo Mode Behavior

When `NEXT_PUBLIC_DEMO_MODE=true`:
- A yellow "DEMO MODE" banner appears at the top of every page
- Hardcoded demo user IDs are used (no login required)
- The landing page shows the role picker for testing all three roles

When `NEXT_PUBLIC_DEMO_MODE=false`:
- No demo banner
- Real Supabase Auth is required (login/register flow)
- API routes validate authentication tokens
8 changes: 4 additions & 4 deletions src/app/api/delivery/assignments/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export async function PATCH(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const { id } = await params;
const supabase = await createClient();
Expand Down Expand Up @@ -116,8 +116,8 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const { id } = await params;
const supabase = await createClient();
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/delivery/assignments/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { getAuthenticatedUser, unauthorizedResponse } from '@/lib/auth/guard';

export async function GET(request: NextRequest) {
try {
const { error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const supabase = await createClient();
const { searchParams } = new URL(request.url);
Expand Down
8 changes: 4 additions & 4 deletions src/app/api/orders/[id]/status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export async function PATCH(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const { id } = await params;
const supabase = await createClient();
Expand Down Expand Up @@ -124,8 +124,8 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const { id } = await params;
const supabase = await createClient();
Expand Down
12 changes: 6 additions & 6 deletions src/app/api/orders/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import type { Order, DeliveryPerson } from '@/lib/supabase/types';

export async function POST(request: NextRequest) {
try {
const { user: authUser, error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();
const { user: authUser, error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const supabase = await createClient();
const body = await request.json();

const { vendor_id, items, delivery_address, delivery_notes, customer_id: bodyCustomerId } = body;
const customer_id = isDemoMode ? bodyCustomerId : authUser!.id;
const { vendor_id, items, delivery_address, delivery_notes } = body;
const customer_id = authUser!.id;

// Validate required fields
if (!vendor_id || !items?.length || !delivery_address || !customer_id) {
Expand Down Expand Up @@ -114,8 +114,8 @@ export async function POST(request: NextRequest) {

export async function GET(request: NextRequest) {
try {
const { error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const supabase = await createClient();
const { searchParams } = new URL(request.url);
Expand Down
7 changes: 0 additions & 7 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useAuth } from '@/hooks/use-auth';
import { DEMO_MODE } from '@/lib/config/demo';

type AuthMethod = 'email' | 'phone';

Expand All @@ -22,12 +21,6 @@ function LoginForm() {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

// Demo mode doesn't need login
if (DEMO_MODE) {
router.push('/');
return null;
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!email.trim() || !password) return;
Expand Down
156 changes: 50 additions & 106 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useRouter } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ShoppingBag, Store, Truck } from "lucide-react";
import { DEMO_MODE } from "@/lib/config/demo";
import { useAuth } from "@/hooks/use-auth";
import { useRole } from "@/hooks/use-role";
import type { UserRole } from "@/types";
Expand Down Expand Up @@ -53,140 +52,85 @@ export default function Home() {
router.push(href);
};

if (!DEMO_MODE) {
// Authenticated user: show role-based navigation
if (user && !loading) {
return (
<main className="container px-4 py-8">
<div className="mx-auto max-w-3xl space-y-8">
<div className="text-center space-y-2">
<p className="text-lg text-muted-foreground">
Welcome back, {user.name}
</p>
<p className="text-sm text-muted-foreground">
Select a role to continue
</p>
</div>

<div className="grid gap-4 md:grid-cols-3">
{roles.map((role) => (
<button key={role.name} onClick={() => handleRoleClick(role.role, role.href)} className="text-left">
<Card className={`h-full transition-colors border-2 ${role.borderColor} ${role.hoverBg} cursor-pointer`}>
<CardHeader className="text-center pb-2">
<div className={`mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-full ${role.color} text-white`}>
<role.icon className="h-6 w-6" />
</div>
<CardTitle className="text-lg">{role.name}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-center">
{role.description}
</CardDescription>
</CardContent>
</Card>
</button>
))}
</div>
</div>
</main>
);
}

// Not authenticated: show sign-in / create account
// Authenticated user: show role-based navigation
if (user && !loading) {
return (
<main className="container px-4 py-8">
<div className="mx-auto max-w-md space-y-8 text-center">
<div className="space-y-2">
<div className="mx-auto max-w-3xl space-y-8">
<div className="text-center space-y-2">
<p className="text-lg text-muted-foreground">
Community-powered food ordering for local co-ops
Welcome back, {user.name}
</p>
<p className="text-sm text-muted-foreground">
Order from local vendors, delivered by your neighbors
Select a role to continue
</p>
</div>

<div className="grid grid-cols-3 gap-4">
<div className="grid gap-4 md:grid-cols-3">
{roles.map((role) => (
<div key={role.name} className="text-center">
<div className={`mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-full ${role.color} text-white`}>
<role.icon className="h-6 w-6" />
</div>
<p className="text-sm font-medium">{role.name}</p>
</div>
<button key={role.name} onClick={() => handleRoleClick(role.role, role.href)} className="text-left">
<Card className={`h-full transition-colors border-2 ${role.borderColor} ${role.hoverBg} cursor-pointer`}>
<CardHeader className="text-center pb-2">
<div className={`mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-full ${role.color} text-white`}>
<role.icon className="h-6 w-6" />
</div>
<CardTitle className="text-lg">{role.name}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-center">
{role.description}
</CardDescription>
</CardContent>
</Card>
</button>
))}
</div>

<div className="space-y-3">
<Link href="/login">
<Button className="w-full bg-green-600 hover:bg-green-700 h-12 text-base">
Sign In
</Button>
</Link>
<Link href="/register">
<Button variant="outline" className="w-full h-12 text-base">
Create Account
</Button>
</Link>
<Link href="/vendors">
<Button variant="ghost" className="w-full text-sm text-muted-foreground">
Browse vendors without signing in
</Button>
</Link>
</div>
</div>
</main>
);
}

// Demo mode
// Not authenticated: show sign-in / create account
return (
<main className="container px-4 py-8">
<div className="mx-auto max-w-3xl space-y-8">
<div className="text-center space-y-2">
<div className="mx-auto max-w-md space-y-8 text-center">
<div className="space-y-2">
<p className="text-lg text-muted-foreground">
Community-powered food ordering for local co-ops
</p>
<p className="text-sm text-muted-foreground">
Select a role below to explore the demo
Order from local vendors, delivered by your neighbors
</p>
</div>

<div className="grid gap-4 md:grid-cols-3">
<div className="grid grid-cols-3 gap-4">
{roles.map((role) => (
<button key={role.name} onClick={() => handleRoleClick(role.role, role.href)} className="text-left">
<Card className={`h-full transition-colors border-2 ${role.borderColor} ${role.hoverBg} cursor-pointer`}>
<CardHeader className="text-center pb-2">
<div className={`mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-full ${role.color} text-white`}>
<role.icon className="h-6 w-6" />
</div>
<CardTitle className="text-lg">{role.name}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-center">
{role.description}
</CardDescription>
</CardContent>
</Card>
</button>
<div key={role.name} className="text-center">
<div className={`mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-full ${role.color} text-white`}>
<role.icon className="h-6 w-6" />
</div>
<p className="text-sm font-medium">{role.name}</p>
</div>
))}
</div>

<Card>
<CardHeader>
<CardTitle className="text-lg">How to Test the Full Flow</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<ol className="list-decimal list-inside space-y-2 text-sm text-muted-foreground">
<li>Open <strong>3 browser tabs</strong> (or use incognito windows)</li>
<li>In Tab 1: Click <strong>Customer</strong> → Browse vendors and place an order</li>
<li>In Tab 2: Click <strong>Vendor</strong> → Watch for incoming orders and accept them</li>
<li>In Tab 3: Click <strong>Delivery</strong> → Accept the delivery and mark it complete</li>
</ol>
<p className="text-xs text-muted-foreground pt-2 border-t">
All roles see real-time updates via Supabase. Changes in one tab appear instantly in others.
</p>
</CardContent>
</Card>
<div className="space-y-3">
<Link href="/login">
<Button className="w-full bg-green-600 hover:bg-green-700 h-12 text-base">
Sign In
</Button>
</Link>
<Link href="/register">
<Button variant="outline" className="w-full h-12 text-base">
Create Account
</Button>
</Link>
<Link href="/vendors">
<Button variant="ghost" className="w-full text-sm text-muted-foreground">
Browse vendors without signing in
</Button>
</Link>
</div>
</div>
</main>
);
Expand Down
Loading
Loading