feat: initial implementation of modern modular webshop (shop core)
This commit is contained in:
37
shop/lib/actions/orders.ts
Normal file
37
shop/lib/actions/orders.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { Order } from '../types'
|
||||
|
||||
export async function submitOrder(orderData: {
|
||||
product_id: string;
|
||||
selected_modules: string[];
|
||||
total_amount: number;
|
||||
customer_details: any;
|
||||
}) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) throw new Error('Not authenticated')
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('orders')
|
||||
.insert([{
|
||||
user_id: user.id,
|
||||
total_amount: orderData.total_amount,
|
||||
items: {
|
||||
product_id: orderData.product_id,
|
||||
modules: orderData.selected_modules
|
||||
},
|
||||
billing_details: orderData.customer_details,
|
||||
status: 'pending'
|
||||
}])
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
revalidatePath('/admin/orders') // If there was an admin orders page
|
||||
return data
|
||||
}
|
||||
84
shop/lib/actions/products.ts
Normal file
84
shop/lib/actions/products.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { Product, ProductModule } from '../types'
|
||||
|
||||
export async function getProducts() {
|
||||
const supabase = await createClient()
|
||||
const { data, error } = await supabase
|
||||
.from('products')
|
||||
.select('*, modules:product_modules(*)')
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (error) throw error
|
||||
return data as Product[]
|
||||
}
|
||||
|
||||
export async function createProduct(product: Omit<Product, 'id' | 'created_at' | 'updated_at'>, modules: Omit<ProductModule, 'id' | 'product_id' | 'created_at'>[]) {
|
||||
const supabase = await createClient()
|
||||
|
||||
// Start a transaction-like approach (Supabase doesn't have cross-table transactions easily in one call via JS client without RPC)
|
||||
const { data: newProduct, error: productError } = await supabase
|
||||
.from('products')
|
||||
.insert([product])
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (productError) throw productError
|
||||
|
||||
if (modules.length > 0) {
|
||||
const modulesWithId = modules.map(m => ({ ...m, product_id: newProduct.id }))
|
||||
const { error: modulesError } = await supabase
|
||||
.from('product_modules')
|
||||
.insert(modulesWithId)
|
||||
|
||||
if (modulesError) throw modulesError
|
||||
}
|
||||
|
||||
revalidatePath('/admin/products')
|
||||
revalidatePath('/order')
|
||||
return newProduct
|
||||
}
|
||||
|
||||
export async function updateProduct(id: string, product: Partial<Product>, modules: any[]) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error: productError } = await supabase
|
||||
.from('products')
|
||||
.update(product)
|
||||
.eq('id', id)
|
||||
|
||||
if (productError) throw productError
|
||||
|
||||
// For modules, a simple approach is to delete and re-insert or use upsert if they have IDs
|
||||
// To keep it simple: Delete all modules and re-insert the current list
|
||||
await supabase.from('product_modules').delete().eq('product_id', id)
|
||||
|
||||
if (modules.length > 0) {
|
||||
const modulesWithId = modules.map(m => ({
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
additional_price: m.additional_price,
|
||||
is_required: m.is_required,
|
||||
product_id: id
|
||||
}))
|
||||
const { error: modulesError } = await supabase
|
||||
.from('product_modules')
|
||||
.insert(modulesWithId)
|
||||
|
||||
if (modulesError) throw modulesError
|
||||
}
|
||||
|
||||
revalidatePath('/admin/products')
|
||||
revalidatePath('/order')
|
||||
}
|
||||
|
||||
export async function deleteProduct(id: string) {
|
||||
const supabase = await createClient()
|
||||
const { error } = await supabase.from('products').delete().eq('id', id)
|
||||
if (error) throw error
|
||||
|
||||
revalidatePath('/admin/products')
|
||||
revalidatePath('/order')
|
||||
}
|
||||
8
shop/lib/supabase/client.ts
Normal file
8
shop/lib/supabase/client.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
||||
);
|
||||
}
|
||||
76
shop/lib/supabase/proxy.ts
Normal file
76
shop/lib/supabase/proxy.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { hasEnvVars } from "../utils";
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
});
|
||||
|
||||
// If the env vars are not set, skip proxy check. You can remove this
|
||||
// once you setup the project.
|
||||
if (!hasEnvVars) {
|
||||
return supabaseResponse;
|
||||
}
|
||||
|
||||
// With Fluid compute, don't put this client in a global environment
|
||||
// variable. Always create a new one on each request.
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) =>
|
||||
request.cookies.set(name, value),
|
||||
);
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
});
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options),
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Do not run code between createServerClient and
|
||||
// supabase.auth.getClaims(). A simple mistake could make it very hard to debug
|
||||
// issues with users being randomly logged out.
|
||||
|
||||
// IMPORTANT: If you remove getClaims() and you use server-side rendering
|
||||
// with the Supabase client, your users may be randomly logged out.
|
||||
const { data } = await supabase.auth.getClaims();
|
||||
const user = data?.claims;
|
||||
|
||||
if (
|
||||
request.nextUrl.pathname !== "/" &&
|
||||
!user &&
|
||||
!request.nextUrl.pathname.startsWith("/login") &&
|
||||
!request.nextUrl.pathname.startsWith("/auth")
|
||||
) {
|
||||
// no user, potentially respond by redirecting the user to the login page
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/auth/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
// IMPORTANT: You *must* return the supabaseResponse object as it is.
|
||||
// If you're creating a new response object with NextResponse.next() make sure to:
|
||||
// 1. Pass the request in it, like so:
|
||||
// const myNewResponse = NextResponse.next({ request })
|
||||
// 2. Copy over the cookies, like so:
|
||||
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
||||
// 3. Change the myNewResponse object to fit your needs, but avoid changing
|
||||
// the cookies!
|
||||
// 4. Finally:
|
||||
// return myNewResponse
|
||||
// If this is not done, you may be causing the browser and server to go out
|
||||
// of sync and terminate the user's session prematurely!
|
||||
|
||||
return supabaseResponse;
|
||||
}
|
||||
34
shop/lib/supabase/server.ts
Normal file
34
shop/lib/supabase/server.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
/**
|
||||
* Especially important if using Fluid compute: Don't put this client in a
|
||||
* global variable. Always create a new client within each function when using
|
||||
* it.
|
||||
*/
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll();
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options),
|
||||
);
|
||||
} catch {
|
||||
// The `setAll` method was called from a Server Component.
|
||||
// This can be ignored if you have proxy refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
44
shop/lib/types.ts
Normal file
44
shop/lib/types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export type Product = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null | undefined;
|
||||
base_price: number;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
modules?: ProductModule[];
|
||||
};
|
||||
|
||||
export type ProductModule = {
|
||||
id: string;
|
||||
product_id: string;
|
||||
name: string;
|
||||
description?: string | null | undefined;
|
||||
additional_price: number;
|
||||
is_required: boolean;
|
||||
is_active?: boolean;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type Profile = {
|
||||
id: string;
|
||||
company_name: string | null;
|
||||
vat_id: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
address: string | null;
|
||||
city: string | null;
|
||||
zip_code: string | null;
|
||||
country: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type Order = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
status: 'pending' | 'completed' | 'cancelled';
|
||||
total_amount: number;
|
||||
items: any; // Snapshot of product and modules
|
||||
billing_details: any;
|
||||
created_at: string;
|
||||
};
|
||||
11
shop/lib/utils.ts
Normal file
11
shop/lib/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// This check can be removed, it is just for tutorial purposes
|
||||
export const hasEnvVars =
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL &&
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
|
||||
Reference in New Issue
Block a user