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
53 changes: 53 additions & 0 deletions src/app/api/menu-items/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { getAuthenticatedUser, unauthorizedResponse } from '@/lib/auth/guard';

export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const { id } = await params;
const supabase = await createClient();
const body = await request.json();

const updates: Record<string, unknown> = {};

if (body.name !== undefined) updates.name = body.name?.trim();
if (body.description !== undefined) updates.description = body.description?.trim() || null;
if (body.category !== undefined) updates.category = body.category?.trim() || null;
if (body.image_url !== undefined) updates.image_url = body.image_url?.trim() || null;
if (body.is_available !== undefined) updates.is_available = body.is_available;

if (body.price !== undefined) {
if (typeof body.price !== 'number' || body.price < 0) {
return NextResponse.json({ error: 'price must be a non-negative number' }, { status: 400 });
}
updates.price = body.price;
}

if (Object.keys(updates).length === 0) {
return NextResponse.json({ error: 'No fields to update' }, { status: 400 });
}

const { data: item, error } = await supabase
.from('menu_items')
.update(updates)
.eq('id', id)
.select()
.single();

if (error) {
console.error('Menu item update error:', error);
return NextResponse.json({ error: 'Failed to update menu item' }, { status: 500 });
}

return NextResponse.json({ item });
} catch (error) {
console.error('Menu item PATCH error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
109 changes: 109 additions & 0 deletions src/app/api/menu-items/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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 } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const supabase = await createClient();
const { searchParams } = new URL(request.url);
const vendor_id = searchParams.get('vendor_id');

if (!vendor_id) {
return NextResponse.json({ error: 'vendor_id is required' }, { status: 400 });
}

const { data: items, error } = await supabase
.from('menu_items')
.select('*')
.eq('vendor_id', vendor_id)
.order('category', { ascending: true, nullsFirst: false })
.order('name', { ascending: true });

if (error) {
console.error('Menu items fetch error:', error);
return NextResponse.json({ error: 'Failed to fetch menu items' }, { status: 500 });
}

return NextResponse.json({ items });
} catch (error) {
console.error('Menu items GET error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

export async function DELETE(request: NextRequest) {
try {
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const supabase = await createClient();
const { searchParams } = new URL(request.url);
const vendor_id = searchParams.get('vendor_id');

if (!vendor_id) {
return NextResponse.json({ error: 'vendor_id is required' }, { status: 400 });
}

const { error } = await supabase
.from('menu_items')
.delete()
.eq('vendor_id', vendor_id);

if (error) {
console.error('Menu items bulk delete error:', error);
return NextResponse.json({ error: 'Failed to clear menu items' }, { status: 500 });
}

return NextResponse.json({ success: true });
} catch (error) {
console.error('Menu items DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

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

const { vendor_id, name, price, description, category, image_url, is_available } = body;

if (!vendor_id || !name?.trim()) {
return NextResponse.json({ error: 'vendor_id and name are required' }, { status: 400 });
}

if (price === undefined || price === null || typeof price !== 'number' || price < 0) {
return NextResponse.json({ error: 'price must be a non-negative number' }, { status: 400 });
}

const { data: item, error } = await supabase
.from('menu_items')
.insert({
vendor_id,
name: name.trim(),
description: description?.trim() || null,
price,
category: category?.trim() || null,
image_url: image_url?.trim() || null,
is_available: is_available ?? true,
} as Record<string, unknown>)
.select()
.single();

if (error) {
console.error('Menu item create error:', error);
return NextResponse.json({ error: 'Failed to create menu item' }, { status: 500 });
}

return NextResponse.json({ item }, { status: 201 });
} catch (error) {
console.error('Menu items POST error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
72 changes: 72 additions & 0 deletions src/app/api/vendors/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { getAuthenticatedUser, unauthorizedResponse } from '@/lib/auth/guard';

export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

const { id } = await params;
const supabase = await createClient();
const body = await request.json();

const updates: Record<string, unknown> = {};
if (body.name !== undefined) updates.name = body.name?.trim();
if (body.address !== undefined) updates.address = body.address?.trim() || null;
if (body.description !== undefined) updates.description = body.description?.trim() || null;
if (body.is_active !== undefined) updates.is_active = body.is_active;

if (Object.keys(updates).length === 0) {
return NextResponse.json({ error: 'No fields to update' }, { status: 400 });
}

const { data: vendor, error } = await supabase
.from('vendors')
.update(updates)
.eq('id', id)
.select()
.single();

if (error) {
console.error('Vendor update error:', error);
return NextResponse.json({ error: 'Failed to update vendor' }, { status: 500 });
}

return NextResponse.json({ vendor });
} catch (error) {
console.error('Vendor PATCH error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { error: authError } = await getAuthenticatedUser();
if (authError) return unauthorizedResponse();

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

const { error } = await supabase
.from('vendors')
.update({ is_active: false })
.eq('id', id);

if (error) {
console.error('Vendor deactivate error:', error);
return NextResponse.json({ error: 'Failed to deactivate vendor' }, { status: 500 });
}

return NextResponse.json({ success: true });
} catch (error) {
console.error('Vendor DELETE error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
52 changes: 17 additions & 35 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
'use client';

import { useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";

Check warning on line 6 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / build

'CardTitle' is defined but never used

Check warning on line 6 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / build

'CardHeader' is defined but never used

Check warning on line 6 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / build

'CardDescription' is defined but never used

Check warning on line 6 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / build

'CardContent' is defined but never used

Check warning on line 6 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / build

'Card' is defined but never used
import { Button } from "@/components/ui/button";
import { ShoppingBag, Store, Truck } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
import { useRole } from "@/hooks/use-role";
import type { UserRole } from "@/types";

const roleRoutes: Record<string, string> = {
customer: '/vendors',
vendor: '/vendor',
delivery: '/delivery',
};

const roles = [
{
name: "Customer",
Expand Down Expand Up @@ -47,47 +54,22 @@
const { setRole } = useRole();
const router = useRouter();

useEffect(() => {
if (user && !loading) {
const role = user.default_role || 'customer';
setRole(role as UserRole);
router.replace(roleRoutes[role] ?? '/vendors');
}
}, [user, loading, router, setRole]);

const handleRoleClick = (role: UserRole, href: string) => {

Check warning on line 65 in src/app/page.tsx

View workflow job for this annotation

GitHub Actions / build

'handleRoleClick' is assigned a value but never used
setRole(role);
router.push(href);
};

// Authenticated user: show role-based navigation
// Authenticated user: redirect in progress
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>
);
return null;
}

// Not authenticated: show sign-in / create account
Expand Down
Loading
Loading