Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getMockTableData } from "@/lib/mock-data";
import { svcHeaders } from "@/lib/svc-headers";
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
export type CreateProductResult =
| { success: true; id: string }
| { success: false; error: string };
export async function createProduct(
brandId: string,
data: {
name: string;
description: string;
price: number;
type: string;
active: boolean;
image_url?: string | null;
is_taxable: boolean;
pickup_type?: string;
}
): Promise<CreateProductResult> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, error: "Not authenticated" };
if (!adminUser.can_manage_products) return { success: false, error: "Not authorized" };
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return { success: false, error: "Not authorized for this brand" };
}
if (useMockData) {
const mockProducts = getMockTableData("products") as any[];
const newId = `mock-prod-${Date.now()}`;
const newProduct = {
id: newId,
name: data.name,
description: data.description,
price: data.price,
type: data.type,
is_active: data.active,
image_url: data.image_url ?? null,
is_taxable: data.is_taxable,
pickup_type: data.pickup_type ?? "scheduled_stop",
brand_id: brandId,
};
mockProducts.push(newProduct);
return { success: true, id: newId };
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/bulk_upsert_products`, {
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
signal: AbortSignal.timeout(10000),
body: JSON.stringify({
p_brand_id: brandId,
p_products: [{
name: data.name,
description: data.description,
price: data.price,
type: data.type,
active: data.active,
image_url: data.image_url ?? null,
is_taxable: data.is_taxable,
pickup_type: data.pickup_type ?? "scheduled_stop",
}],
}),
});
if (!res.ok) {
const err = await res.text();
return { success: false, error: `Failed: ${err}` };
}
const result = await res.json();
if (result.errors && result.errors.length > 0) {
return { success: false, error: result.errors[0].error };
}
return { success: true, id: result.created > 0 ? "created" : "updated" };
}
+68
View File
@@ -0,0 +1,68 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { getMockTableData } from "@/lib/mock-data";
import { svcHeaders } from "@/lib/svc-headers";
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true";
export type UpdateProductResult =
| { success: true }
| { success: false; error: string };
export async function updateProduct(
productId: string,
brandId: string,
data: {
name: string;
description: string;
price: number;
type: string;
active: boolean;
image_url?: string | null;
is_taxable: boolean;
pickup_type?: string;
}
): Promise<UpdateProductResult> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, error: "Not authenticated" };
if (!adminUser.can_manage_products) return { success: false, error: "Not authorized" };
if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) {
return { success: false, error: "Not authorized for this brand" };
}
if (useMockData) {
return { success: true };
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const res = await fetch(`${supabaseUrl}/rest/v1/products?id=eq.${productId}`, {
method: "PATCH",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json", "Prefer": "return=representation" },
body: JSON.stringify({
name: data.name,
description: data.description,
price: data.price,
type: data.type,
active: data.active,
image_url: data.image_url ?? null,
is_taxable: data.is_taxable,
pickup_type: data.pickup_type ?? "scheduled_stop",
}),
});
if (!res.ok) {
const err = await res.text();
return { success: false, error: `Failed to update product: ${err}` };
}
const updated = await res.json();
if (updated.errors) {
return { success: false, error: updated.errors[0]?.message ?? "Unknown error" };
}
return { success: true };
}
+94
View File
@@ -0,0 +1,94 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { svcHeaders } from "@/lib/svc-headers";
export type UploadProductImageResult =
| { success: true; imageUrl: string }
| { success: false; error: string };
export async function uploadProductImage(
productId: string,
file: File
): Promise<UploadProductImageResult> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, error: "Not authenticated" };
const validTypes = ["image/png", "image/jpeg", "image/webp"];
if (!validTypes.includes(file.type)) {
return { success: false, error: "Invalid file type. Use PNG, JPEG, or WebP." };
}
if (file.size > 5 * 1024 * 1024) {
return { success: false, error: "File too large. Max 5MB." };
}
const ext = file.type.split("/")[1] === "jpeg" ? "jpg" : file.type.split("/")[1];
const path = `products/${productId}/${crypto.randomUUID()}.${ext}`;
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const uploadRes = await fetch(
`${supabaseUrl}/storage/v1/object/product-images/${path}`,
{
method: "PUT",
headers: { ...svcHeaders(supabaseKey), "Authorization": `Bearer ${supabaseKey}`, "Content-Type": `image/${ext}`, "x-upsert": "true" },
body: buffer,
}
);
if (!uploadRes.ok) {
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
}
const publicUrl = `${supabaseUrl}/storage/v1/object/public/product-images/${path}`;
// If productId is "__NEW__", we just upload and return the URL (caller handles saving it)
if (productId === "__NEW__") {
return { success: true, imageUrl: publicUrl };
}
// Update product record with new image URL
const patchRes = await fetch(
`${supabaseUrl}/rest/v1/products?id=eq.${productId}`,
{
method: "PATCH",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify({ image_url: publicUrl }),
}
);
if (!patchRes.ok) {
return { success: false, error: "Upload succeeded but failed to save image URL" };
}
return { success: true, imageUrl: publicUrl };
}
export async function deleteProductImage(
productId: string
): Promise<{ success: boolean; error?: string }> {
const adminUser = await getAdminUser();
if (!adminUser) return { success: false, error: "Not authenticated" };
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const patchRes = await fetch(
`${supabaseUrl}/rest/v1/products?id=eq.${productId}`,
{
method: "PATCH",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify({ image_url: null }),
}
);
if (!patchRes.ok) {
return { success: false, error: "Failed to clear image" };
}
return { success: true };
}