feat(branding): add global dynamic theme system and fix 2fa mail

This commit is contained in:
DanielS
2026-08-14 15:57:29 +02:00
parent 009b167cbd
commit 9ecdc645e8
22 changed files with 1764 additions and 313 deletions

View File

@@ -0,0 +1,267 @@
'use client'
import { useState } from 'react'
import { PRESET_COLOR_SCHEMES, ColorPreset } from '@/lib/constants/branding'
import { Check, Palette, Sparkles, Building2, ShoppingCart, Tag, Layers } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
interface ColorThemePickerProps {
colorScheme: string
primaryColor: string
accentColor: string
companyName?: string
onChange: (scheme: string, primary: string, accent: string) => void
}
export function ColorThemePicker({
colorScheme,
primaryColor,
accentColor,
companyName = 'CASPOS Shop',
onChange,
}: ColorThemePickerProps) {
const [customPrimary, setCustomPrimary] = useState(primaryColor || '#2563eb')
const [customAccent, setCustomAccent] = useState(accentColor || '#38bdf8')
const isCustom = colorScheme === 'custom'
const activePreset = PRESET_COLOR_SCHEMES.find(p => p.id === colorScheme) || PRESET_COLOR_SCHEMES[3]
const handleSelectPreset = (preset: ColorPreset) => {
onChange(preset.id, preset.primary, preset.accent)
}
const handleCustomPrimaryChange = (val: string) => {
setCustomPrimary(val)
onChange('custom', val, customAccent)
}
const handleCustomAccentChange = (val: string) => {
setCustomAccent(val)
onChange('custom', customPrimary, val)
}
return (
<div className="space-y-6">
{/* Grid of Presets */}
<div>
<div className="flex items-center justify-between mb-3">
<Label className="text-sm font-bold text-slate-200 flex items-center gap-2">
<Palette className="w-4 h-4 text-blue-400" />
Vorgegebene Farbschemata (9 Stile mit je 10 Farbtokens)
</Label>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{PRESET_COLOR_SCHEMES.map((preset) => {
const isSelected = colorScheme === preset.id
const tokenSwatches = [
preset.primary,
preset.accent,
preset.bgGlow1,
preset.bgGlow2,
preset.gradientFrom,
preset.gradientTo,
preset.textHighlight,
preset.buttonBg,
preset.ringColor,
preset.cardBorder,
]
return (
<button
key={preset.id}
type="button"
onClick={() => handleSelectPreset(preset)}
className={`relative p-3.5 rounded-2xl border text-left transition-all duration-200 group flex flex-col justify-between space-y-3 ${
isSelected
? 'bg-slate-900/90 border-blue-500 shadow-lg shadow-blue-500/10 ring-2 ring-blue-500/30'
: 'bg-slate-950/60 border-slate-800 hover:border-slate-700 hover:bg-slate-900/40'
}`}
>
<div className="flex items-center justify-between">
<span className="font-semibold text-xs text-white truncate pr-2">
{preset.name}
</span>
{isSelected && (
<span className="w-5 h-5 rounded-full bg-blue-500 text-white flex items-center justify-center shrink-0">
<Check className="w-3.5 h-3.5" />
</span>
)}
</div>
<p className="text-[11px] text-slate-400 line-clamp-1">
{preset.description}
</p>
{/* 10 Color Token Swatches Bar */}
<div className="space-y-1 pt-1 border-t border-slate-800/60">
<div className="flex items-center gap-1 justify-between">
{tokenSwatches.map((color, i) => (
<span
key={i}
className="w-3 h-3 rounded-full border border-white/10 shadow-sm shrink-0"
style={{ backgroundColor: color }}
title={`Token ${i + 1}: ${color}`}
/>
))}
</div>
</div>
</button>
)
})}
</div>
</div>
{/* Custom Color Picker */}
<div
className={`p-4 rounded-2xl border transition-all duration-200 space-y-4 ${
isCustom
? 'bg-slate-900/90 border-blue-500 ring-2 ring-blue-500/30'
: 'bg-slate-950/40 border-slate-800'
}`}
>
<div className="flex items-center justify-between">
<button
type="button"
onClick={() => onChange('custom', customPrimary, customAccent)}
className="flex items-center gap-2.5 font-bold text-xs text-white hover:text-blue-400 transition"
>
<span className={`w-4 h-4 rounded-full border flex items-center justify-center ${
isCustom ? 'border-blue-500 bg-blue-500/20 text-blue-400' : 'border-slate-700'
}`}>
{isCustom && <Check className="w-3 h-3" />}
</span>
<span>Benutzerdefiniertes Farbschema (Custom Hex Picker)</span>
</button>
</div>
{isCustom && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2 border-t border-slate-800">
{/* Primary Color Picker */}
<div className="space-y-1.5">
<Label className="text-xs text-slate-300">Hauptfarbe (Primary)</Label>
<div className="flex items-center gap-2">
<input
type="color"
value={customPrimary}
onChange={(e) => handleCustomPrimaryChange(e.target.value)}
className="w-9 h-9 rounded-xl border border-slate-700 bg-transparent cursor-pointer shrink-0"
/>
<Input
type="text"
value={customPrimary}
onChange={(e) => handleCustomPrimaryChange(e.target.value)}
placeholder="#2563eb"
className="font-mono text-xs uppercase bg-slate-950 border-slate-800 text-white"
/>
</div>
</div>
{/* Accent Color Picker */}
<div className="space-y-1.5">
<Label className="text-xs text-slate-300">Akzentfarbe (Accent)</Label>
<div className="flex items-center gap-2">
<input
type="color"
value={customAccent}
onChange={(e) => handleCustomAccentChange(e.target.value)}
className="w-9 h-9 rounded-xl border border-slate-700 bg-transparent cursor-pointer shrink-0"
/>
<Input
type="text"
value={customAccent}
onChange={(e) => handleCustomAccentChange(e.target.value)}
placeholder="#38bdf8"
className="font-mono text-xs uppercase bg-slate-950 border-slate-800 text-white"
/>
</div>
</div>
</div>
)}
</div>
{/* Live Micro-Preview Card with Animated Background Orbs */}
<div className="p-5 rounded-2xl bg-slate-950 border border-slate-800 space-y-3 relative overflow-hidden">
<div className="flex items-center justify-between border-b border-slate-800 pb-2.5 relative z-10">
<span className="text-xs font-bold text-slate-400 flex items-center gap-1.5 uppercase tracking-wider">
<Sparkles className="w-3.5 h-3.5 text-amber-400" />
Live Vorschau & Animierter Background Glow
</span>
<span className="text-[10px] font-mono text-slate-500">
{primaryColor} / {accentColor}
</span>
</div>
<div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800/80 space-y-4 relative overflow-hidden z-10 backdrop-blur-md">
{/* Animated Background Orbs Preview */}
<div
className="absolute -top-10 -left-10 w-40 h-40 rounded-full opacity-30 blur-2xl pointer-events-none transition-all duration-500"
style={{ backgroundColor: activePreset.bgGlow1 || primaryColor }}
/>
<div
className="absolute -bottom-10 -right-10 w-40 h-40 rounded-full opacity-30 blur-2xl pointer-events-none transition-all duration-500"
style={{ backgroundColor: activePreset.bgGlow2 || accentColor }}
/>
{/* Header Preview */}
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-950/80 border border-slate-800 relative z-10">
<div className="flex items-center gap-2">
<div
className="w-6 h-6 rounded-md flex items-center justify-center text-white text-xs font-bold shadow-md"
style={{ backgroundColor: primaryColor }}
>
<Building2 className="w-3.5 h-3.5" />
</div>
<span className="font-bold text-xs text-white">
{companyName || 'Ihr Unternehmen'}
</span>
</div>
<span
className="text-[10px] font-semibold px-2 py-0.5 rounded-full text-slate-950 font-bold"
style={{ backgroundColor: accentColor }}
>
Aktiv
</span>
</div>
{/* Buttons & Badges Preview */}
<div className="flex flex-wrap items-center gap-2.5 relative z-10">
<button
type="button"
className="px-4 py-2 rounded-lg text-xs font-bold text-white shadow-md flex items-center gap-1.5 transition-all"
style={{
background: `linear-gradient(135deg, ${primaryColor} 0%, ${activePreset.gradientTo || primaryColor} 100%)`,
}}
>
<ShoppingCart className="w-3.5 h-3.5" />
In den Warenkorb
</button>
<button
type="button"
className="px-3.5 py-2 rounded-lg text-xs font-semibold bg-slate-800/90 text-slate-200 border"
style={{ borderColor: accentColor }}
>
Details anzeigen
</button>
<span
className="text-[10px] font-bold px-2.5 py-1 rounded-full border flex items-center gap-1"
style={{
borderColor: `${accentColor}40`,
backgroundColor: `${accentColor}15`,
color: activePreset.textHighlight || accentColor,
}}
>
<Tag className="w-3 h-3" />
Empfohlen
</span>
</div>
</div>
</div>
</div>
)
}