feat: initial implementation of modern modular webshop (shop core)

This commit is contained in:
DanielS
2026-04-30 23:41:56 +02:00
commit 5538d87283
72 changed files with 12853 additions and 0 deletions

View 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
}

View 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')
}

View 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!,
);
}

View 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;
}

View 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
View 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
View 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;