Merge branch 'staging' of https://gitea.hephex.de/Daniel/webshop into staging
All checks were successful
Staging Build / build (push) Successful in 3m39s

This commit is contained in:
DanielS
2026-07-01 08:41:21 +02:00
7 changed files with 103 additions and 57 deletions

View File

@@ -10,7 +10,7 @@
--card-foreground: 220 40% 2%; --card-foreground: 220 40% 2%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 220 40% 2%; --popover-foreground: 220 40% 2%;
--primary: 220 90% 56%; --primary: 199 100% 43%;
--primary-foreground: 0 0% 100%; --primary-foreground: 0 0% 100%;
--secondary: 220 14% 90%; --secondary: 220 14% 90%;
--secondary-foreground: 220 40% 2%; --secondary-foreground: 220 40% 2%;

View File

@@ -121,7 +121,7 @@ export function HomeClient({ initialUser }: HomeClientProps) {
> >
<Zap className="w-3.5 h-3.5 text-blue-400 fill-blue-400/20" /> <Zap className="w-3.5 h-3.5 text-blue-400 fill-blue-400/20" />
</motion.div> </motion.div>
CASPOS Lizenz-Order-Portal CASPOS Lizenz-Portal
</motion.div> </motion.div>
{/* Headline */} {/* Headline */}
@@ -237,7 +237,7 @@ export function HomeClient({ initialUser }: HomeClientProps) {
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{/* Card 1 */} {/* Card 1 */}
<motion.div <a href="https://support.teamzone.softengine.de" target="_blank" rel="noopener noreferrer"><motion.div
whileHover={{ y: -6 }} whileHover={{ y: -6 }}
className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden" className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden"
> >
@@ -249,13 +249,13 @@ export function HomeClient({ initialUser }: HomeClientProps) {
</div> </div>
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Zulip</h3> <h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Zulip</h3>
<p className="text-slate-400 text-sm leading-relaxed"> <p className="text-slate-400 text-sm leading-relaxed">
Sie möchten sich mit anderen CASPOS Partnern austauschen, Fragen stellen oder Feedback geben? Treten Sie unserem Zulip-Chat bei. Treten Sie unserem Partner-Chat bei! tauschen Sie sich mit anderen CASPOS Partnern aus.
</p> </p>
</div> </div>
</motion.div> </motion.div></a>
{/* Card 2 */} {/* Card 2 */}
<motion.div <a href="https://wiki.caspos.de" target="_blank" rel="noopener noreferrer"><motion.div
whileHover={{ y: -6 }} whileHover={{ y: -6 }}
className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden" className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden"
> >
@@ -270,10 +270,10 @@ export function HomeClient({ initialUser }: HomeClientProps) {
Ihr Wissens-Portal für die CASPOS Produktfamilie. Ihr Wissens-Portal für die CASPOS Produktfamilie.
</p> </p>
</div> </div>
</motion.div> </motion.div></a>
{/* Card 3 */} {/* Card 3 */}
<motion.div <a href="https://support.caspos.de" target="_blank" rel="noopener noreferrer"><motion.div
whileHover={{ y: -6 }} whileHover={{ y: -6 }}
className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden" className="group relative flex flex-col p-6 rounded-2xl border border-slate-900 bg-slate-950/70 hover:bg-slate-900/40 hover:border-slate-800/80 transition-all duration-300 shadow-xl overflow-hidden"
> >
@@ -285,10 +285,10 @@ export function HomeClient({ initialUser }: HomeClientProps) {
</div> </div>
<h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Support</h3> <h3 className="text-xl font-bold text-slate-200 group-hover:text-white transition-colors">Support</h3>
<p className="text-slate-400 text-sm leading-relaxed"> <p className="text-slate-400 text-sm leading-relaxed">
Sie haben ein Problem in der CASPOS? Wir helfen Ihnen gerne weiter! Erstellen Sie jetzt ein Ticket über unser Ticketsystem. Sie haben ein technisches Problem in der CASPOS? Erstellen Sie jetzt ein Ticket!
</p> </p>
</div> </div>
</motion.div> </motion.div></a>
</div> </div>
</div> </div>
</section> </section>

View File

@@ -98,7 +98,7 @@ export function NavbarClient({ user, role = "partner" }: NavbarClientProps) {
<> <>
<header className="px-4 lg:px-6 h-16 flex items-center justify-between border-b border-white/5 backdrop-blur-md sticky top-0 z-50 bg-[#020617]/85"> <header className="px-4 lg:px-6 h-16 flex items-center justify-between border-b border-white/5 backdrop-blur-md sticky top-0 z-50 bg-[#020617]/85">
<Link className="flex items-center justify-center gap-2 group" href="/"> <Link className="flex items-center justify-center gap-2 group" href="/">
<span className="font-bold text-xl tracking-tighter text-white">CASPOS Shop</span> <img src="/assets/CASPOS-logo.webp" alt="CASPOS Logo" className="h-8" />
<DemoWrapper> <DemoWrapper>
<span className="text-[10px] bg-amber-500/20 text-amber-400 px-2 py-0.5 rounded-full font-semibold uppercase tracking-wider border border-amber-500/30"> <span className="text-[10px] bg-amber-500/20 text-amber-400 px-2 py-0.5 rounded-full font-semibold uppercase tracking-wider border border-amber-500/30">
Demo Demo

View File

@@ -75,67 +75,61 @@ export function AdminRecentOrders() {
}, []) }, [])
return ( return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7 bg-[#020617]/85 p-4 rounded-xl">
<Card className="col-span-4 glass-dark border-white/5"> <div className="rounded-xl border border-white/5 bg-[#020617]/85 overflow-hidden backdrop-blur-xl col-span-4">
<CardHeader> <CardHeader>
<CardTitle>Übersicht (letzte 6 Monate)</CardTitle> <CardTitle className="text-white">Übersicht (letzte 6 Monate)</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pl-2"> <CardContent className="pl-2">
<RevenueChart data={chartData} /> <RevenueChart data={chartData} />
</CardContent> </CardContent>
</Card> </div>
<Card className="col-span-3 glass-dark border-white/5"> <div className="flex flex-col gap-4 bg-[#020617]/85 p-4 rounded-xl border border-white/5 col-span-3">
<CardHeader className="flex flex-row items-center justify-between pb-3"> <div className="flex flex-row items-center justify-between pb-3">
<CardTitle>Neue Bestellungen</CardTitle> <h3 className="font-semibold text-lg text-white">Neue Bestellungen</h3>
<span className="text-[10px] text-slate-500"> <span className="text-[10px] text-slate-500">
Aktualisiert: {lastChecked.toLocaleTimeString('de-DE')} Aktualisiert: {lastChecked.toLocaleTimeString('de-DE')}
</span> </span>
</CardHeader> </div>
<CardContent> <div className="space-y-3 max-h-96 overflow-y-auto">
{orders.length === 0 ? ( {orders.length === 0 ? (
<p className="text-slate-500 text-sm italic text-center py-8">Noch keine Bestellungen</p> <p className="text-slate-500 text-sm italic text-center py-8">Noch keine Bestellungen</p>
) : ( ) : (
<div className="space-y-3"> orders.slice(0, 3).map(order => {
{orders.map(order => { const isNew = newOrderIds.has(order.id)
const isNew = newOrderIds.has(order.id) const customerName =
const customerName = order.customer_data?.company_name ||
order.customer_data?.company_name || `${order.customer_data?.first_name ?? ''} ${order.customer_data?.last_name ?? ''}`.trim() ||
`${order.customer_data?.first_name ?? ''} ${order.customer_data?.last_name ?? ''}`.trim() || 'Unbekannt'
'Unbekannt' return (
<Link key={order.id} href="/admin/orders" className="block">
return ( <div
<Link key={order.id} href="/admin/orders" className="block"> className={`flex items-center gap-3 p-3 rounded-lg transition-all duration-500 ${isNew
<div ? 'bg-primary/10 border border-primary/30 shadow-[0_0_12px_rgba(59,130,246,0.2)]'
className={`flex items-center gap-3 p-3 rounded-lg transition-all duration-500 ${ : 'bg-[#020617]/20 hover:bg-[#020617]/30 border border-white/5'
isNew
? 'bg-primary/10 border border-primary/30 shadow-[0_0_12px_rgba(59,130,246,0.2)]'
: 'bg-white/5 hover:bg-white/10 border border-white/5'
}`} }`}
> >
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs font-bold shrink-0"> <div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-primary text-xs font-bold shrink-0">
{customerName.slice(0, 2).toUpperCase()} {customerName.slice(0, 2).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">{customerName}</p>
<p className="text-xs text-slate-500 font-mono">#{order.order_number}</p>
</div>
<div className="text-right shrink-0 space-y-1">
<p className="font-medium text-white text-sm">
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)}
</p>
<Badge className={`text-[10px] ${statusClass[order.status] ?? ''}`}>
{statusLabel[order.status] ?? order.status}
</Badge>
</div>
</div> </div>
</Link> <div className="flex-1 min-w-0">
) <p className="text-sm font-medium text-white truncate">{customerName}</p>
})} <p className="text-xs text-slate-500 font-mono">#{order.order_number}</p>
</div> </div>
<div className="text-right shrink-0 space-y-1">
<p className="font-medium text-white text-sm">
{new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(order.total_price)}
</p>
<Badge className={`text-[10px] ${statusClass[order.status] ?? ''}`}>{statusLabel[order.status] ?? order.status}</Badge>
</div>
</div>
</Link>
)
})
)} )}
</CardContent> </div>
</Card> </div>
</div> </div>
) )
} }

View File

@@ -3,6 +3,7 @@
import { createClient } from '@/lib/supabase/server' import { createClient } from '@/lib/supabase/server'
import { createAdminClient } from '@/lib/supabase/admin' import { createAdminClient } from '@/lib/supabase/admin'
import { headers } from 'next/headers' import { headers } from 'next/headers'
import { sendLockoutEmail } from '@/lib/utils/email'
export async function signIn(email: string, password: string) { export async function signIn(email: string, password: string) {
try { try {
@@ -12,6 +13,32 @@ export async function signIn(email: string, password: string) {
password, password,
}) })
if (error) { if (error) {
// Track failed attempts
const { data: usr, error: usrError } = await supabase
.from('users')
.select('failed_attempts, email')
.eq('email', email)
.single()
const attempts = (usr?.failed_attempts ?? 0) + 1
// Update attempts count
await supabase
.from('users')
.update({ failed_attempts: attempts })
.eq('email', email)
// Lock account on 5th failure
if (attempts >= 5) {
await supabase
.from('users')
.update({ role: 'gesperrt' })
.eq('email', email)
// Notify admin
await sendLockoutEmail('info@hephex.de')
return { success: false, error: 'Konto gesperrt nach 5 Fehlversuchen.' }
}
return { success: false, error: error.message } return { success: false, error: error.message }
} }

25
shop/lib/utils/email.ts Normal file
View File

@@ -0,0 +1,25 @@
'use server'
import nodemailer from 'nodemailer'
export async function sendLockoutEmail(to: string) {
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
const message = {
from: `"Admin" <${process.env.SMTP_FROM}>`,
to,
subject: 'Konto gesperrt',
text: 'Ihr Konto wurde wegen zu vieler falscher Passwortversuche gesperrt.',
html: `<p>Ihr Konto wurde gesperrt, weil das Passwort zu oft falsch eingegeben wurde.</p>`,
}
await transporter.sendMail(message)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB