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
7 changes: 7 additions & 0 deletions src/app/api/delivery/assignments/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { getAuthenticatedUser, unauthorizedResponse } from '@/lib/auth/guard';
import type { DeliveryAssignmentStatus, OrderStatus, DeliveryAssignment, Order } from '@/lib/supabase/types';

// Map assignment status to order status (for picked_up and delivered only)
Expand All @@ -14,6 +15,9 @@ export async function PATCH(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();

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

const { id } = await params;
const supabase = await createClient();

Expand Down
4 changes: 4 additions & 0 deletions src/app/api/delivery/assignments/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
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 supabase = await createClient();
const { searchParams } = new URL(request.url);

Expand Down
7 changes: 7 additions & 0 deletions src/app/api/orders/[id]/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { getAuthenticatedUser, unauthorizedResponse } from '@/lib/auth/guard';
import type { OrderStatus, Order, DeliveryAssignment } from '@/lib/supabase/types';

// Valid status transitions
Expand All @@ -18,6 +19,9 @@ export async function PATCH(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError, isDemoMode } = await getAuthenticatedUser();
if (!isDemoMode && authError) return unauthorizedResponse();

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

const { id } = await params;
const supabase = await createClient();

Expand Down
10 changes: 9 additions & 1 deletion src/app/api/orders/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { getAuthenticatedUser, unauthorizedResponse } from '@/lib/auth/guard';
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 supabase = await createClient();
const body = await request.json();

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

// Validate required fields
if (!vendor_id || !items?.length || !delivery_address || !customer_id) {
Expand Down Expand Up @@ -109,6 +114,9 @@ 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 supabase = await createClient();
const { searchParams } = new URL(request.url);

Expand Down
5 changes: 3 additions & 2 deletions src/app/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { useCart } from '@/hooks/use-cart';
import { DEMO_IDS } from '@/lib/config/demo';
import { useAuth } from '@/hooks/use-auth';

export default function CheckoutPage() {
const router = useRouter();
const { items, totalAmount, vendorId, clearCart } = useCart();
const { userId } = useAuth();
const [address, setAddress] = useState('');
const [notes, setNotes] = useState('');
const [loading, setLoading] = useState(false);
Expand All @@ -40,7 +41,7 @@ export default function CheckoutPage() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customer_id: DEMO_IDS.CUSTOMER_ID,
customer_id: userId,
vendor_id: vendorId,
items: items.map((item) => ({
menu_item_id: item.menu_item_id,
Expand Down
5 changes: 3 additions & 2 deletions src/app/delivery/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import { Separator } from '@/components/ui/separator';
import { OrderStatusBadge } from '@/components/order-status';
import { useDeliveryAssignments, updateAssignmentStatus } from '@/hooks/use-orders';
import type { DeliveryAssignment, OrderWithDetails } from '@/lib/supabase/types';
import { DEMO_IDS } from '@/lib/config/demo';
import { useAuth } from '@/hooks/use-auth';

// Extended order type with vendor_accepted
interface DeliveryOrder extends OrderWithDetails {
vendor_accepted: boolean;
}

export default function DeliveryDashboard() {
const { assignments, loading, error } = useDeliveryAssignments(DEMO_IDS.DELIVERY_PERSON_ID);
const { deliveryPersonId } = useAuth();
const { assignments, loading, error } = useDeliveryAssignments(deliveryPersonId!);
const [updating, setUpdating] = useState<string | null>(null);

const handleStatusUpdate = async (
Expand Down
9 changes: 6 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { RoleProvider } from "@/hooks/use-role";
import { AuthProvider } from "@/hooks/use-auth";
import { CartProvider } from "@/hooks/use-cart";

const geistSans = Geist({
Expand Down Expand Up @@ -43,9 +44,11 @@ export default function RootLayout({
</div>
)}
<RoleProvider>
<CartProvider>
{children}
</CartProvider>
<AuthProvider>
<CartProvider>
{children}
</CartProvider>
</AuthProvider>
</RoleProvider>
</body>
</html>
Expand Down
130 changes: 130 additions & 0 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client';

import { Suspense, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Phone, Lock, Loader2 } from 'lucide-react';
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';

function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const { signIn } = useAuth();
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
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 (!phone.trim() || !password) return;

setLoading(true);
setError(null);

const { error: signInError } = await signIn(phone.trim(), password);

if (signInError) {
setError(signInError);
setLoading(false);
} else {
const redirect = searchParams.get('redirect') || '/';
router.push(redirect);
}
};

return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center px-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl">Sign In</CardTitle>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Welcome back to Firefly
</p>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium flex items-center gap-2">
<Phone className="w-4 h-4" />
Phone Number
</label>
<Input
type="tel"
placeholder="+1 555 000 0000"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="h-12"
required
/>
</div>

<div className="space-y-2">
<label className="text-sm font-medium flex items-center gap-2">
<Lock className="w-4 h-4" />
Password
</label>
<Input
type="password"
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="h-12"
required
/>
</div>

{error && (
<div className="p-3 bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 rounded-lg text-sm text-center">
{error}
</div>
)}

<Button
type="submit"
className="w-full bg-green-600 hover:bg-green-700 h-12 text-base"
disabled={loading}
>
{loading ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Signing in...
</>
) : (
'Sign In'
)}
</Button>
</form>

<p className="text-center text-sm text-gray-500 dark:text-gray-400 mt-6">
Don&apos;t have an account?{' '}
<Link
href="/register"
className="text-green-600 hover:text-green-700 font-medium"
>
Create one
</Link>
</p>
</CardContent>
</Card>
</div>
);
}

export default function LoginPage() {
return (
<Suspense>
<LoginForm />
</Suspense>
);
}
56 changes: 56 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Link from "next/link";
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";

const roles = [
{
Expand Down Expand Up @@ -33,6 +35,60 @@ const roles = [
];

export default function Home() {
if (!DEMO_MODE) {
return (
<div className="min-h-screen bg-background">
<header className="border-b bg-background">
<div className="container flex h-16 items-center justify-center px-4">
<h1 className="text-2xl font-bold">Project Firefly</h1>
</div>
</header>

<main className="container px-4 py-8">
<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">
Order from local vendors, delivered by your neighbors
</p>
</div>

<div className="grid grid-cols-3 gap-4">
{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>
))}
</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>
</div>
);
}

return (
<div className="min-h-screen bg-background">
{/* Header */}
Expand Down
Loading
Loading