Files
webshop/shop/components/SetupWizard.tsx

680 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { motion, AnimatePresence } from 'framer-motion'
import { Sparkles, ShieldCheck, Mail, ArrowRight, Loader2, Check, Lock, Building, User, MapPin, Receipt, Palette, Send } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { createClient } from '@/lib/supabase/client'
import { completeSetup, testSmtpConfig } from '@/lib/actions/setup'
import { ColorThemePicker } from '@/components/admin/ColorThemePicker'
export function SetupWizard() {
const router = useRouter()
const [step, setStep] = useState(1)
const [loading, setLoading] = useState(false)
const [errorMsg, setErrorMsg] = useState('')
const [testSmtpLoading, setTestSmtpLoading] = useState(false)
const [testSmtpResult, setTestSmtpResult] = useState<{ success: boolean; message: string } | null>(null)
// Step 2: Admin Account
const [adminForm, setAdminForm] = useState({
email: '',
password: '',
confirmPassword: '',
firstName: '',
lastName: '',
})
// Step 3: Firmendaten & Rechnungsadresse
const [brandingForm, setBrandingForm] = useState({
companyName: '',
street: '',
zip: '',
city: '',
billingStreet: '',
billingZip: '',
billingCity: '',
sameBillingAddress: true,
colorScheme: 'modern_blue',
primaryColor: '#2563eb',
accentColor: '#38bdf8',
})
// Step 5: SMTP Config
const [smtpForm, setSmtpForm] = useState({
host: '',
port: '587',
secure: false,
user: '',
pass: '',
})
// Validations
const isAdminFormValid =
adminForm.email.includes('@') &&
adminForm.password.length >= 6 &&
adminForm.password === adminForm.confirmPassword &&
adminForm.firstName.trim().length > 0 &&
adminForm.lastName.trim().length > 0
const isCompanyFormValid =
brandingForm.companyName.trim().length > 0 &&
brandingForm.street.trim().length > 0 &&
brandingForm.zip.trim().length > 0 &&
brandingForm.city.trim().length > 0 &&
(brandingForm.sameBillingAddress ||
(brandingForm.billingStreet.trim().length > 0 &&
brandingForm.billingZip.trim().length > 0 &&
brandingForm.billingCity.trim().length > 0))
const handleTestSmtp = async () => {
setTestSmtpLoading(true)
setTestSmtpResult(null)
const res = await testSmtpConfig(
{
host: smtpForm.host,
port: Number(smtpForm.port),
secure: smtpForm.secure,
user: smtpForm.user,
pass: smtpForm.pass,
},
adminForm.email
)
setTestSmtpResult(res)
setTestSmtpLoading(false)
}
const handleFinishSetup = async () => {
if (!isAdminFormValid || !isCompanyFormValid) return
setLoading(true)
setErrorMsg('')
try {
const res = await completeSetup(
{
email: adminForm.email,
password: adminForm.password,
companyName: brandingForm.companyName,
firstName: adminForm.firstName,
lastName: adminForm.lastName,
},
{
street: brandingForm.street,
zip: brandingForm.zip,
city: brandingForm.city,
billingStreet: brandingForm.billingStreet,
billingZip: brandingForm.billingZip,
billingCity: brandingForm.billingCity,
sameBillingAddress: brandingForm.sameBillingAddress,
colorScheme: brandingForm.colorScheme,
primaryColor: brandingForm.primaryColor,
accentColor: brandingForm.accentColor,
},
{
host: smtpForm.host,
port: Number(smtpForm.port),
secure: smtpForm.secure,
user: smtpForm.user,
pass: smtpForm.pass,
}
)
if (!res.success) {
setErrorMsg(res.error || 'Fehler beim Setup.')
setLoading(false)
return
}
// Setup erfolgreich Weiterleitung zur Login-Seite
router.push('/auth/login?setup=success')
router.refresh()
} catch (e: any) {
setErrorMsg(e.message || 'Ein unerwarteter Fehler ist aufgetreten.')
setLoading(false)
}
}
return (
<div className="min-h-screen bg-[#020617] text-white flex flex-col items-center justify-center p-4 relative overflow-hidden">
{/* Background Glow */}
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] rounded-full bg-blue-500/10 blur-[120px]" />
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] rounded-full bg-purple-500/10 blur-[120px]" />
<div className="w-full max-w-2xl relative z-10 my-8">
{/* Step Indicator Header */}
<div className="flex items-center justify-between mb-6 px-2">
<div className="flex items-center gap-2">
<span className="text-xl font-bold tracking-tight text-blue-400">B2B Shop</span>
<span className="text-xs px-2.5 py-0.5 rounded-full bg-slate-900 text-slate-400 border border-slate-800 font-medium">made by hephex</span>
</div>
<div className="flex gap-1.5">
{[1, 2, 3, 4, 5].map((s) => (
<div
key={s}
className={`h-1.5 rounded-full transition-all duration-300 ${s === step ? 'w-8 bg-blue-500' : 'w-2 bg-slate-800'
}`}
/>
))}
</div>
</div>
<AnimatePresence mode="wait">
{/* STEP 1: Willkommen */}
{step === 1 && (
<motion.div
key="step1"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
className="glass-dark border border-white/10 rounded-3xl p-8 space-y-6 shadow-2xl"
>
<div className="w-16 h-16 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 mx-auto shadow-inner">
<Sparkles className="w-8 h-8 animate-pulse" />
</div>
<div className="text-center space-y-2">
<h1 className="text-3xl font-extrabold tracking-tight">B2B Shop made by hephex</h1>
<p className="text-slate-400 leading-relaxed text-sm">
Richten Sie Ihren persönlichen Lizenz- und Anfrage-Shop ein. Wir konfigurieren Administrator-Zugang, Firmendaten, Rechnungsadresse und Ihr persönliches Farbschema.
</p>
</div>
<div className="p-4 rounded-xl bg-white/5 border border-white/5 text-sm text-slate-400 space-y-2">
<div className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-400 shrink-0" />
<span>Komplett self-hosted & sicher</span>
</div>
<div className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-400 shrink-0" />
<span>9 vordefinierte Farbpaletten + Custom Farbwähler</span>
</div>
<div className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-400 shrink-0" />
<span>Lizenzserver-Anbindung</span>
</div>
<div className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-400 shrink-0" />
<span>Stammdatenverwaltung</span>
</div>
<div className="flex items-center gap-2">
<Check className="w-4 h-4 text-green-400 shrink-0" />
<span>Automatische Adress- & Rechnungsverwaltung</span>
</div>
</div>
<Button
onClick={() => setStep(2)}
className="w-full py-6 text-base font-bold bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 shadow-lg shadow-blue-500/15 group"
>
Setup starten
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</motion.div>
)}
{/* STEP 2: Admin-Konto */}
{step === 2 && (
<motion.div
key="step2"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
className="glass-dark border border-white/10 rounded-3xl p-8 space-y-6 shadow-2xl"
>
<div className="space-y-1">
<h2 className="text-2xl font-bold flex items-center gap-2">
<ShieldCheck className="w-6 h-6 text-blue-400" />
Admin-Konto anlegen
</h2>
<p className="text-slate-400 text-sm">
Erstellen Sie das erste Administrator-Konto für vollen Systemzugriff.
</p>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Vorname *</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
value={adminForm.firstName}
onChange={(e) => setAdminForm({ ...adminForm, firstName: e.target.value })}
placeholder="Vorname"
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Nachname *</Label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
value={adminForm.lastName}
onChange={(e) => setAdminForm({ ...adminForm, lastName: e.target.value })}
placeholder="Nachname"
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">E-Mail-Adresse *</Label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
type="email"
value={adminForm.email}
onChange={(e) => setAdminForm({ ...adminForm, email: e.target.value })}
placeholder="admin@firma.de"
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Passwort * (min. 6 Zeichen)</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
type="password"
value={adminForm.password}
onChange={(e) => setAdminForm({ ...adminForm, password: e.target.value })}
placeholder="••••••"
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Passwort bestätigen *</Label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
type="password"
value={adminForm.confirmPassword}
onChange={(e) => setAdminForm({ ...adminForm, confirmPassword: e.target.value })}
placeholder="••••••"
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
</div>
</div>
</div>
<div className="flex gap-3 pt-2">
<Button variant="ghost" onClick={() => setStep(1)} className="text-white hover:bg-white/5">
Zurück
</Button>
<Button
onClick={() => setStep(3)}
disabled={!isAdminFormValid}
className="flex-1 bg-blue-600 hover:bg-blue-500 text-white font-bold"
>
Weiter zu Firmendaten
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</div>
</motion.div>
)}
{/* STEP 3: Firmendaten & Rechnungsadresse */}
{step === 3 && (
<motion.div
key="step3"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
className="glass-dark border border-white/10 rounded-3xl p-8 space-y-6 shadow-2xl"
>
<div className="space-y-1">
<h2 className="text-2xl font-bold flex items-center gap-2">
<Building className="w-6 h-6 text-blue-400" />
Firmendaten & Adressen
</h2>
<p className="text-slate-400 text-sm">
Tragen Sie Ihren Firmennamen, die Anschrift und die Rechnungsadresse ein.
</p>
</div>
<div className="space-y-4">
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Firmenname *</Label>
<div className="relative">
<Building className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
value={brandingForm.companyName}
onChange={(e) => setBrandingForm({ ...brandingForm, companyName: e.target.value })}
placeholder="z. B. CASPOS Software GmbH"
className="pl-9 bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
</div>
{/* Anschrift */}
<div className="space-y-3 pt-1 border-t border-slate-800">
<span className="text-xs font-bold text-slate-300 flex items-center gap-1.5">
<MapPin className="w-3.5 h-3.5 text-blue-400" /> Firmenanschrift
</span>
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Straße & Hausnummer *</Label>
<Input
value={brandingForm.street}
onChange={(e) => setBrandingForm({ ...brandingForm, street: e.target.value })}
placeholder="Musterstraße 12"
className="bg-white/5 border-white/10 text-white placeholder:text-slate-600 text-xs"
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">PLZ *</Label>
<Input
value={brandingForm.zip}
onChange={(e) => setBrandingForm({ ...brandingForm, zip: e.target.value })}
placeholder="12345"
className="bg-white/5 border-white/10 text-white text-xs"
/>
</div>
<div className="col-span-2 space-y-1.5">
<Label className="text-slate-300 text-xs">Ort *</Label>
<Input
value={brandingForm.city}
onChange={(e) => setBrandingForm({ ...brandingForm, city: e.target.value })}
placeholder="Musterstadt"
className="bg-white/5 border-white/10 text-white text-xs"
/>
</div>
</div>
</div>
{/* Rechnungsadresse */}
<div className="space-y-3 pt-2 border-t border-slate-800">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-slate-300 flex items-center gap-1.5">
<Receipt className="w-3.5 h-3.5 text-blue-400" /> Rechnungsadresse
</span>
<label className="flex items-center gap-2 text-xs text-slate-400 cursor-pointer">
<input
type="checkbox"
checked={brandingForm.sameBillingAddress}
onChange={(e) =>
setBrandingForm({ ...brandingForm, sameBillingAddress: e.target.checked })
}
className="w-4 h-4 rounded border-slate-700 bg-white/5 text-blue-500"
/>
Gleiche wie Anschrift
</label>
</div>
{!brandingForm.sameBillingAddress && (
<div className="space-y-3 p-3 rounded-xl bg-white/5 border border-white/10">
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Rechnungsstraße & Nr. *</Label>
<Input
value={brandingForm.billingStreet}
onChange={(e) =>
setBrandingForm({ ...brandingForm, billingStreet: e.target.value })
}
placeholder="Rechnungsstraße 45"
className="bg-white/5 border-white/10 text-white text-xs"
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">PLZ *</Label>
<Input
value={brandingForm.billingZip}
onChange={(e) =>
setBrandingForm({ ...brandingForm, billingZip: e.target.value })
}
placeholder="54321"
className="bg-white/5 border-white/10 text-white text-xs"
/>
</div>
<div className="col-span-2 space-y-1.5">
<Label className="text-slate-300 text-xs">Ort *</Label>
<Input
value={brandingForm.billingCity}
onChange={(e) =>
setBrandingForm({ ...brandingForm, billingCity: e.target.value })
}
placeholder="Rechnungsstadt"
className="bg-white/5 border-white/10 text-white text-xs"
/>
</div>
</div>
</div>
)}
</div>
</div>
<div className="flex gap-3 pt-2">
<Button variant="ghost" onClick={() => setStep(2)} className="text-white hover:bg-white/5">
Zurück
</Button>
<Button
onClick={() => setStep(4)}
disabled={!isCompanyFormValid}
className="flex-1 bg-blue-600 hover:bg-blue-500 text-white font-bold"
>
Weiter zum Farbschema
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</div>
</motion.div>
)}
{/* STEP 4: Farbschema & Live-Vorschau */}
{step === 4 && (
<motion.div
key="step4"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
className="glass-dark border border-white/10 rounded-3xl p-8 space-y-6 shadow-2xl"
>
<div className="space-y-1">
<h2 className="text-2xl font-bold flex items-center gap-2">
<Palette className="w-6 h-6 text-blue-400" />
Farbschema & Webshop Styling
</h2>
<p className="text-slate-400 text-sm">
Wählen Sie aus 9 vorgegebenen Farbpaletten oder erstellen Sie eine eigene Farbkombination mit Live-Vorschau.
</p>
</div>
<ColorThemePicker
colorScheme={brandingForm.colorScheme}
primaryColor={brandingForm.primaryColor}
accentColor={brandingForm.accentColor}
companyName={brandingForm.companyName}
onChange={(scheme, primary, accent) =>
setBrandingForm({
...brandingForm,
colorScheme: scheme,
primaryColor: primary,
accentColor: accent,
})
}
/>
<div className="flex gap-3 pt-2">
<Button variant="ghost" onClick={() => setStep(3)} className="text-white hover:bg-white/5">
Zurück
</Button>
<Button
onClick={() => setStep(5)}
className="flex-1 bg-blue-600 hover:bg-blue-500 text-white font-bold"
>
Weiter zu SMTP
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</div>
</motion.div>
)}
{/* STEP 5: SMTP Mailserver */}
{step === 5 && (
<motion.div
key="step5"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
className="glass-dark border border-white/10 rounded-3xl p-8 space-y-6 shadow-2xl"
>
<div className="space-y-1">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold flex items-center gap-2">
<Mail className="w-6 h-6 text-blue-400" />
SMTP-Mailserver
</h2>
<span className="text-[10px] uppercase font-bold tracking-wider px-2.5 py-1 rounded-full bg-slate-800 text-amber-400 border border-amber-500/20">
Optional
</span>
</div>
<p className="text-slate-400 text-sm">
Tragen Sie Ihre Mailserver-Daten ein oder überspringen Sie diesen Schritt. Sie können die Einstellungen jederzeit im Admin-Bereich anpassen.
</p>
</div>
{errorMsg && (
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-xs">
{errorMsg}
</div>
)}
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2 space-y-1.5">
<Label className="text-slate-300 text-xs">SMTP Host</Label>
<Input
value={smtpForm.host}
onChange={(e) => setSmtpForm({ ...smtpForm, host: e.target.value })}
placeholder="smtp.domain.de"
className="bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">SMTP Port</Label>
<Input
value={smtpForm.port}
onChange={(e) => setSmtpForm({ ...smtpForm, port: e.target.value })}
placeholder="587"
className="bg-white/5 border-white/10 text-white"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Benutzername</Label>
<Input
value={smtpForm.user}
onChange={(e) => setSmtpForm({ ...smtpForm, user: e.target.value })}
placeholder="mail@domain.de"
className="bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300 text-xs">Passwort</Label>
<Input
type="password"
value={smtpForm.pass}
onChange={(e) => setSmtpForm({ ...smtpForm, pass: e.target.value })}
placeholder="••••••••••••"
className="bg-white/5 border-white/10 text-white placeholder:text-slate-600"
/>
</div>
<div className="flex items-center justify-between pt-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="smtp_secure"
checked={smtpForm.secure}
onChange={(e) => setSmtpForm({ ...smtpForm, secure: e.target.checked })}
className="w-4 h-4 rounded border-slate-700 bg-white/5 text-blue-500 focus:ring-0 focus:ring-offset-0"
/>
<Label htmlFor="smtp_secure" className="text-slate-300 text-sm cursor-pointer select-none">
Sichere Verbindung (SSL/TLS)
</Label>
</div>
<Button
type="button"
variant="secondary"
size="sm"
onClick={handleTestSmtp}
disabled={testSmtpLoading || !smtpForm.host || !smtpForm.user}
className="bg-white/10 hover:bg-white/20 text-white text-xs border border-white/10 flex items-center gap-1.5"
>
{testSmtpLoading ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" /> Testen...
</>
) : (
<>
<Send className="w-3.5 h-3.5 text-blue-400" /> Test-E-Mail senden
</>
)}
</Button>
</div>
{testSmtpResult && (
<div
className={`p-3 rounded-lg border text-xs font-medium ${
testSmtpResult.success
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400'
: 'bg-red-500/10 border-red-500/30 text-red-400'
}`}
>
{testSmtpResult.message}
</div>
)}
</div>
<div className="flex flex-col sm:flex-row gap-3 pt-2">
<Button
variant="ghost"
onClick={() => setStep(4)}
disabled={loading}
className="text-white hover:bg-white/5"
>
Zurück
</Button>
<Button
variant="outline"
onClick={handleFinishSetup}
disabled={loading}
className="border-slate-700 text-slate-300 hover:text-white"
>
Überspringen
</Button>
<Button
onClick={handleFinishSetup}
disabled={loading}
className="flex-1 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> Setup wird abgeschlossen...
</>
) : (
'Einrichtung abschließen'
)}
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
)
}