feat: initial implementation of modern modular webshop (shop core)
This commit is contained in:
57
shop/app/admin/layout.tsx
Normal file
57
shop/app/admin/layout.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import Link from 'next/link'
|
||||
import { LayoutDashboard, Package, Settings, Users, LogOut } from 'lucide-react'
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#020617] text-white">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 border-r border-white/5 bg-black/20 backdrop-blur-xl flex flex-col">
|
||||
<div className="p-6">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||
<Package className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-xl tracking-tighter">Admin Panel</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-4 space-y-2 py-4">
|
||||
<Link href="/admin" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all">
|
||||
<LayoutDashboard className="w-5 h-5" />
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link href="/admin/products" className="flex items-center gap-3 px-3 py-2 rounded-lg bg-primary/10 text-primary border border-primary/20">
|
||||
<Package className="w-5 h-5" />
|
||||
Produkte
|
||||
</Link>
|
||||
<Link href="/admin/orders" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all">
|
||||
<Users className="w-5 h-5" />
|
||||
Bestellungen
|
||||
</Link>
|
||||
<Link href="/admin/settings" className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all">
|
||||
<Settings className="w-5 h-5" />
|
||||
Einstellungen
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-white/5">
|
||||
<button className="flex w-full items-center gap-3 px-3 py-2 rounded-lg text-slate-400 hover:text-white hover:bg-white/5 transition-all">
|
||||
<LogOut className="w-5 h-5" />
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="container mx-auto max-w-7xl">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
87
shop/app/admin/page.tsx
Normal file
87
shop/app/admin/page.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Package, ShoppingCart, Users, TrendingUp } from 'lucide-react'
|
||||
|
||||
export default function AdminDashboard() {
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-gradient">Dashboard</h2>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card className="glass-dark border-white/5">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Umsatz gesamt</CardTitle>
|
||||
<TrendingUp className="h-4 w-4 text-primary" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">€12,234.56</div>
|
||||
<p className="text-xs text-muted-foreground">+20.1% zum Vormonat</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="glass-dark border-white/5">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Bestellungen</CardTitle>
|
||||
<ShoppingCart className="h-4 w-4 text-primary" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+573</div>
|
||||
<p className="text-xs text-muted-foreground">+12% zur Vorwoche</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="glass-dark border-white/5">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Produkte</CardTitle>
|
||||
<Package className="h-4 w-4 text-primary" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">12</div>
|
||||
<p className="text-xs text-muted-foreground">+2 neue Module</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="glass-dark border-white/5">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Kunden</CardTitle>
|
||||
<Users className="h-4 w-4 text-primary" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2,450</div>
|
||||
<p className="text-xs text-muted-foreground">+180 heute</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||
<Card className="col-span-4 glass-dark border-white/5">
|
||||
<CardHeader>
|
||||
<CardTitle>Übersicht</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pl-2">
|
||||
<div className="h-[200px] flex items-center justify-center text-muted-foreground">
|
||||
[Chart Platzhalter]
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="col-span-3 glass-dark border-white/5">
|
||||
<CardHeader>
|
||||
<CardTitle>Letzte Bestellungen</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="flex items-center gap-4">
|
||||
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs font-bold">
|
||||
JD
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm font-medium">John Doe</p>
|
||||
<p className="text-xs text-muted-foreground">john.doe@example.com</p>
|
||||
</div>
|
||||
<div className="font-medium">+€450.00</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
shop/app/admin/products/page.tsx
Normal file
39
shop/app/admin/products/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getProducts } from '@/lib/actions/products'
|
||||
|
||||
import { ProductList } from '@/components/admin/product-list'
|
||||
import { CreateProductDialog } from '@/components/admin/create-product-dialog'
|
||||
import { Package, Plus } from 'lucide-react'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default async function AdminProductsPage() {
|
||||
return (
|
||||
<div className="flex-1 space-y-8 p-8 pt-6">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-gradient flex items-center gap-3">
|
||||
<Package className="w-8 h-8 text-primary" />
|
||||
Produkte & Module
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Verwalte deine Hauptprodukte und deren zubuchbare Module.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CreateProductDialog>
|
||||
<button className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50">
|
||||
<Plus className="mr-2 h-4 w-4" /> Produkt hinzufügen
|
||||
</button>
|
||||
</CreateProductDialog>
|
||||
</div>
|
||||
</div>
|
||||
<Suspense fallback={<div className="h-40 w-full animate-pulse bg-white/5 rounded-xl" />}>
|
||||
<ProductDataWrapper />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function ProductDataWrapper() {
|
||||
const products = await getProducts()
|
||||
return <ProductList initialProducts={products} />
|
||||
}
|
||||
30
shop/app/auth/confirm/route.ts
Normal file
30
shop/app/auth/confirm/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import { type EmailOtpType } from "@supabase/supabase-js";
|
||||
import { redirect } from "next/navigation";
|
||||
import { type NextRequest } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const token_hash = searchParams.get("token_hash");
|
||||
const type = searchParams.get("type") as EmailOtpType | null;
|
||||
const next = searchParams.get("next") ?? "/";
|
||||
|
||||
if (token_hash && type) {
|
||||
const supabase = await createClient();
|
||||
|
||||
const { error } = await supabase.auth.verifyOtp({
|
||||
type,
|
||||
token_hash,
|
||||
});
|
||||
if (!error) {
|
||||
// redirect user to specified redirect URL or root of app
|
||||
redirect(next);
|
||||
} else {
|
||||
// redirect the user to an error page with some instructions
|
||||
redirect(`/auth/error?error=${error?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// redirect the user to an error page with some instructions
|
||||
redirect(`/auth/error?error=No token hash or type`);
|
||||
}
|
||||
51
shop/app/auth/error/page.tsx
Normal file
51
shop/app/auth/error/page.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Suspense } from "react";
|
||||
|
||||
async function ErrorContent({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ error: string }>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
|
||||
return (
|
||||
<>
|
||||
{params?.error ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Code error: {params.error}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
An unspecified error occurred.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ error: string }>;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">
|
||||
Sorry, something went wrong.
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Suspense>
|
||||
<ErrorContent searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
shop/app/auth/forgot-password/page.tsx
Normal file
11
shop/app/auth/forgot-password/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ForgotPasswordForm } from "@/components/forgot-password-form";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<ForgotPasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
shop/app/auth/login/page.tsx
Normal file
11
shop/app/auth/login/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { LoginForm } from "@/components/login-form";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
shop/app/auth/sign-up-success/page.tsx
Normal file
32
shop/app/auth/sign-up-success/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex flex-col gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">
|
||||
Thank you for signing up!
|
||||
</CardTitle>
|
||||
<CardDescription>Check your email to confirm</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You've successfully signed up. Please check your email to
|
||||
confirm your account before signing in.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
shop/app/auth/sign-up/page.tsx
Normal file
11
shop/app/auth/sign-up/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { SignUpForm } from "@/components/sign-up-form";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<SignUpForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
shop/app/auth/update-password/page.tsx
Normal file
11
shop/app/auth/update-password/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { UpdatePasswordForm } from "@/components/update-password-form";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
|
||||
<div className="w-full max-w-sm">
|
||||
<UpdatePasswordForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
shop/app/favicon.ico
Normal file
BIN
shop/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
72
shop/app/globals.css
Normal file
72
shop/app/globals.css
Normal file
@@ -0,0 +1,72 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 220 33% 98%;
|
||||
--foreground: 220 40% 2%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 220 40% 2%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 220 40% 2%;
|
||||
--primary: 220 90% 56%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 220 14% 90%;
|
||||
--secondary-foreground: 220 40% 2%;
|
||||
--muted: 220 14% 90%;
|
||||
--muted-foreground: 220 14% 40%;
|
||||
--accent: 220 14% 90%;
|
||||
--accent-foreground: 220 40% 2%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--border: 220 14% 85%;
|
||||
--input: 220 14% 85%;
|
||||
--ring: 220 90% 56%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%;
|
||||
--foreground: 213 31% 91%;
|
||||
--card: 224 71% 4%;
|
||||
--card-foreground: 213 31% 91%;
|
||||
--popover: 224 71% 4%;
|
||||
--popover-foreground: 213 31% 91%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 1.2%;
|
||||
--secondary: 222.2 47.4% 11.2%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 222.2 47.4% 11.2%;
|
||||
--muted-foreground: 215.4 16.3% 56.9%;
|
||||
--accent: 222.2 47.4% 11.2%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 63% 31%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 222.2 47.4% 11.2%;
|
||||
--input: 222.2 47.4% 11.2%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.glass {
|
||||
@apply bg-white/10 backdrop-blur-md border border-white/20;
|
||||
}
|
||||
.glass-dark {
|
||||
@apply bg-black/20 backdrop-blur-md border border-white/10;
|
||||
}
|
||||
.text-gradient {
|
||||
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400;
|
||||
}
|
||||
}
|
||||
41
shop/app/layout.tsx
Normal file
41
shop/app/layout.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist } from "next/font/google";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import "./globals.css";
|
||||
|
||||
const defaultUrl = process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: "http://localhost:3000";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(defaultUrl),
|
||||
title: "CloudShop | Modulare Software-Lösungen",
|
||||
description: "Konfigurieren und bestellen Sie Ihre maßgeschneiderte Business-Software in Minuten.",
|
||||
};
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
display: "swap",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${geistSans.className} antialiased`}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
BIN
shop/app/opengraph-image.png
Normal file
BIN
shop/app/opengraph-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
46
shop/app/order/page.tsx
Normal file
46
shop/app/order/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { getProducts } from '@/lib/actions/products'
|
||||
import { OrderWizard } from '@/components/order-wizard'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default async function OrderPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-white selection:bg-primary/30">
|
||||
<div className="container mx-auto py-10">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-5xl font-extrabold tracking-tight mb-4 text-gradient">
|
||||
Konfigurieren Sie Ihre Lösung
|
||||
</h1>
|
||||
<p className="text-slate-400 text-lg max-w-2xl mx-auto">
|
||||
Wählen Sie das passende Paket und die benötigten Module für Ihr Business.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<div className="h-96 w-full animate-pulse bg-white/5 rounded-2xl" />}>
|
||||
<OrderDataWrapper />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function OrderDataWrapper() {
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
redirect('/auth/login')
|
||||
}
|
||||
|
||||
const products = await getProducts()
|
||||
|
||||
// Fetch profile if exists
|
||||
const { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', user.id)
|
||||
.single()
|
||||
|
||||
return <OrderWizard products={products} initialProfile={profile} />
|
||||
}
|
||||
96
shop/app/page.tsx
Normal file
96
shop/app/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import Link from 'next/link'
|
||||
import { Rocket, Shield, Cpu, ArrowRight } from 'lucide-react'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-[#020617] text-white">
|
||||
<header className="px-4 lg:px-6 h-16 flex items-center border-b border-white/5 backdrop-blur-md sticky top-0 z-50">
|
||||
<Link className="flex items-center justify-center gap-2" href="#">
|
||||
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
||||
<Rocket className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-xl tracking-tighter">CloudShop</span>
|
||||
</Link>
|
||||
<nav className="ml-auto flex gap-4 sm:gap-6 items-center">
|
||||
<Link className="text-sm font-medium hover:text-primary transition-colors" href="/order">
|
||||
Bestellen
|
||||
</Link>
|
||||
<Link className="text-sm font-medium hover:text-primary transition-colors" href="/admin/products">
|
||||
Admin
|
||||
</Link>
|
||||
<Button variant="outline" className="border-white/10 hover:bg-white/5">
|
||||
Login
|
||||
</Button>
|
||||
</nav>
|
||||
</header>
|
||||
<main className="flex-1">
|
||||
<section className="w-full py-12 md:py-24 lg:py-32 xl:py-48 px-4 overflow-hidden relative">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[600px] bg-primary/20 blur-[120px] rounded-full -z-10 opacity-30" />
|
||||
<div className="container mx-auto">
|
||||
<div className="flex flex-col items-center space-y-4 text-center">
|
||||
<div className="space-y-2">
|
||||
<div className="inline-block rounded-full bg-primary/10 px-3 py-1 text-sm text-primary font-medium border border-primary/20 mb-4">
|
||||
Version 2.0 ist da
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl lg:text-7xl/none">
|
||||
Die Zukunft der <span className="text-gradient">Software-Bestellung</span>
|
||||
</h1>
|
||||
<p className="mx-auto max-w-[700px] text-slate-400 md:text-xl dark:text-zinc-400 mt-6">
|
||||
Modulare Lösungen, blitzschnelles Deployment und ein intuitives Interface für Ihre Business-Anforderungen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-x-4 mt-8">
|
||||
<Link href="/order">
|
||||
<Button size="lg" className="h-12 px-8 text-lg font-semibold shadow-lg shadow-primary/20 transition-all hover:scale-105">
|
||||
Jetzt konfigurieren <ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/admin/products">
|
||||
<Button size="lg" variant="outline" className="h-12 px-8 text-lg border-white/10 hover:bg-white/5">
|
||||
Admin Bereich
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="w-full py-12 md:py-24 lg:py-32 border-t border-white/5 bg-white/[0.02]">
|
||||
<div className="container mx-auto px-4 md:px-6">
|
||||
<div className="grid gap-12 lg:grid-cols-3">
|
||||
<div className="flex flex-col items-center space-y-4 text-center p-6 rounded-2xl glass-dark">
|
||||
<div className="p-3 bg-primary/10 rounded-xl">
|
||||
<Cpu className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">Modulare Architektur</h3>
|
||||
<p className="text-slate-400">Wählen Sie genau die Module, die Sie benötigen. Keine unnötigen Kosten für Features, die Sie nicht nutzen.</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center space-y-4 text-center p-6 rounded-2xl glass-dark">
|
||||
<div className="p-3 bg-primary/10 rounded-xl">
|
||||
<Shield className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">Maximale Sicherheit</h3>
|
||||
<p className="text-slate-400">Enterprise-grade Security mit Supabase Auth und granularen Row Level Security Policies.</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center space-y-4 text-center p-6 rounded-2xl glass-dark">
|
||||
<div className="p-3 bg-primary/10 rounded-xl">
|
||||
<Rocket className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">Sofort Einsatzbereit</h3>
|
||||
<p className="text-slate-400">Nach der Bestellung wird Ihre Instanz automatisch provisioniert und ist in Minuten online.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer className="flex flex-col gap-2 sm:flex-row py-6 w-full shrink-0 items-center px-4 md:px-6 border-t border-white/5 bg-[#020617]">
|
||||
<p className="text-xs text-slate-500">© 2026 CloudShop GmbH. Alle Rechte vorbehalten.</p>
|
||||
<nav className="sm:ml-auto flex gap-4 sm:gap-6">
|
||||
<Link className="text-xs hover:underline underline-offset-4 text-slate-500" href="#">Impressum</Link>
|
||||
<Link className="text-xs hover:underline underline-offset-4 text-slate-500" href="#">Datenschutz</Link>
|
||||
</nav>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
shop/app/protected/layout.tsx
Normal file
55
shop/app/protected/layout.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { DeployButton } from "@/components/deploy-button";
|
||||
import { EnvVarWarning } from "@/components/env-var-warning";
|
||||
import { AuthButton } from "@/components/auth-button";
|
||||
import { ThemeSwitcher } from "@/components/theme-switcher";
|
||||
import { hasEnvVars } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function ProtectedLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col items-center">
|
||||
<div className="flex-1 w-full flex flex-col gap-20 items-center">
|
||||
<nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
|
||||
<div className="w-full max-w-5xl flex justify-between items-center p-3 px-5 text-sm">
|
||||
<div className="flex gap-5 items-center font-semibold">
|
||||
<Link href={"/"}>Next.js Supabase Starter</Link>
|
||||
<div className="flex items-center gap-2">
|
||||
<DeployButton />
|
||||
</div>
|
||||
</div>
|
||||
{!hasEnvVars ? (
|
||||
<EnvVarWarning />
|
||||
) : (
|
||||
<Suspense>
|
||||
<AuthButton />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
<div className="flex-1 flex flex-col gap-20 max-w-5xl p-5">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<footer className="w-full flex items-center justify-center border-t mx-auto text-center text-xs gap-8 py-16">
|
||||
<p>
|
||||
Powered by{" "}
|
||||
<a
|
||||
href="https://supabase.com/?utm_source=create-next-app&utm_medium=template&utm_term=nextjs"
|
||||
target="_blank"
|
||||
className="font-bold hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Supabase
|
||||
</a>
|
||||
</p>
|
||||
<ThemeSwitcher />
|
||||
</footer>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
43
shop/app/protected/page.tsx
Normal file
43
shop/app/protected/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { FetchDataSteps } from "@/components/tutorial/fetch-data-steps";
|
||||
import { Suspense } from "react";
|
||||
|
||||
async function UserDetails() {
|
||||
const supabase = await createClient();
|
||||
const { data, error } = await supabase.auth.getClaims();
|
||||
|
||||
if (error || !data?.claims) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
return JSON.stringify(data.claims, null, 2);
|
||||
}
|
||||
|
||||
export default function ProtectedPage() {
|
||||
return (
|
||||
<div className="flex-1 w-full flex flex-col gap-12">
|
||||
<div className="w-full">
|
||||
<div className="bg-accent text-sm p-3 px-5 rounded-md text-foreground flex gap-3 items-center">
|
||||
<InfoIcon size="16" strokeWidth={2} />
|
||||
This is a protected page that you can only see as an authenticated
|
||||
user
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 items-start">
|
||||
<h2 className="font-bold text-2xl mb-4">Your user details</h2>
|
||||
<pre className="text-xs font-mono p-3 rounded border max-h-32 overflow-auto">
|
||||
<Suspense>
|
||||
<UserDetails />
|
||||
</Suspense>
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold text-2xl mb-4">Next steps</h2>
|
||||
<FetchDataSteps />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
shop/app/twitter-image.png
Normal file
BIN
shop/app/twitter-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 283 KiB |
Reference in New Issue
Block a user