Compare commits

...

2 Commits

Author SHA1 Message Date
DanielS
ea1906f9e4 feat(wizard): add eye-catcher shake animation for missing required categories
All checks were successful
Staging Build / build (push) Successful in 3m41s
2026-08-18 16:43:22 +02:00
DanielS
9dbbbb8b31 fix(wizard): remove duplicate addToBasket invocation in summary sidebar 2026-08-18 16:43:17 +02:00
4 changed files with 104 additions and 46 deletions

View File

@@ -194,3 +194,29 @@ option {
color: oklch(var(--oklch-text-main)) !important; color: oklch(var(--oklch-text-main)) !important;
background-color: oklch(var(--oklch-surface)) !important; background-color: oklch(var(--oklch-surface)) !important;
} }
/* Eye-Catcher & Rüttel-Animation für fehlende Pflichtkategorien */
@keyframes shake {
0%, 100% { transform: translateX(0); }
15%, 55% { transform: translateX(-4px); }
35%, 75% { transform: translateX(4px); }
}
@keyframes eye-catcher-glow {
0%, 100% {
box-shadow: 0 0 15px rgba(244, 63, 94, 0.6), inset 0 0 8px rgba(244, 63, 94, 0.3);
border-color: rgba(244, 63, 94, 0.9);
}
50% {
box-shadow: 0 0 28px rgba(239, 68, 68, 0.9), inset 0 0 14px rgba(239, 68, 68, 0.5);
border-color: rgba(239, 68, 68, 1);
}
}
.animate-shake {
animation: shake 0.6s ease-in-out infinite;
}
.animate-eye-catcher {
animation: eye-catcher-glow 1.2s ease-in-out infinite, shake 0.8s ease-in-out infinite !important;
}

View File

@@ -933,26 +933,42 @@ export function OrderWizard({
const hasSelection = (sel?.productIds && sel.productIds.length > 0) || !!sel?.productId const hasSelection = (sel?.productIds && sel.productIds.length > 0) || !!sel?.productId
const isRequired = cat.is_required const isRequired = cat.is_required
const isActive = activeCategoryId === cat.id const isActive = activeCategoryId === cat.id
const isMissingRequired = isRequired && !hasSelection
return ( return (
<button <button
key={cat.id} key={cat.id}
onClick={() => setActiveCategoryId(cat.id)} onClick={() => setActiveCategoryId(cat.id)}
className={`flex items-center gap-2.5 p-2.5 rounded-xl border transition-all duration-300 text-left relative overflow-hidden group ${isActive className={`flex items-center gap-2.5 p-2.5 rounded-xl border transition-all duration-300 text-left relative overflow-hidden group ${
isMissingRequired
? 'animate-eye-catcher bg-red-500/10 border-red-500 text-red-400 font-bold shadow-[0_0_20px_rgba(239,68,68,0.4)]'
: isActive
? 'bg-primary/10 border-primary text-white shadow-[0_0_15px_rgba(59,130,246,0.2)]' ? 'bg-primary/10 border-primary text-white shadow-[0_0_15px_rgba(59,130,246,0.2)]'
: 'bg-slate-900/50 border-white/5 text-slate-400 hover:bg-slate-900 hover:border-white/20 hover:text-white' : 'bg-slate-900/50 border-white/5 text-slate-400 hover:bg-slate-900 hover:border-white/20 hover:text-white'
}`} }`}
> >
{isActive && ( {isActive && !isMissingRequired && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-primary" /> <div className="absolute left-0 top-0 bottom-0 w-1 bg-primary" />
)} )}
<div className={`p-1.5 rounded-lg transition-colors ${isActive ? 'bg-primary/20 text-primary' : 'bg-white/5 text-slate-400 group-hover:text-white'}`}> <div className={`p-1.5 rounded-lg transition-colors ${
isMissingRequired
? 'bg-red-500/20 text-red-400'
: isActive
? 'bg-primary/20 text-primary'
: 'bg-white/5 text-slate-400 group-hover:text-white'
}`}>
<CategoryIcon icon={cat.icon} className="w-3.5 h-3.5" /> <CategoryIcon icon={cat.icon} className="w-3.5 h-3.5" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className={`text-xs font-semibold truncate ${isActive ? 'text-white' : 'text-slate-300 group-hover:text-white'}`}> <p className={`text-xs font-semibold truncate ${
isMissingRequired
? 'text-red-300 font-bold'
: isActive
? 'text-white'
: 'text-slate-300 group-hover:text-white'
}`}>
{cat.name} {cat.name}
</p> </p>
</div> </div>
@@ -962,8 +978,8 @@ export function OrderWizard({
</span> </span>
) : isRequired ? ( ) : isRequired ? (
<span className="text-[9px] px-1.5 py-0.2 rounded-full font-semibold shrink-0 border border-amber-500/30 text-amber-400 bg-amber-500/10"> <span className="text-[9px] px-2 py-0.5 rounded-full font-extrabold shrink-0 border border-red-500 text-red-400 bg-red-500/20 animate-pulse tracking-wider">
! FEHLT!
</span> </span>
) : null} ) : null}
</button> </button>

View File

@@ -88,12 +88,28 @@ export function StepSoftware({
className="space-y-6" className="space-y-6"
> >
{/* Category header */} {/* Category header */}
<div className="flex items-center gap-3 bg-white/5 p-4 rounded-xl border border-white/10"> {(() => {
<div className="w-8 h-8 rounded-lg bg-primary/20 flex items-center justify-center"> const isMissingRequired = currentCategory.is_required && !sel?.productId && (!sel?.productIds || sel.productIds.length === 0)
<CategoryIcon icon={currentCategory.icon} className="w-4 h-4 text-primary" /> return (
<div className={`flex items-center gap-3 p-4 rounded-xl border transition-all ${
isMissingRequired
? 'animate-eye-catcher bg-red-500/10 border-red-500 text-red-400 font-bold'
: 'bg-white/5 border-white/10'
}`}>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
isMissingRequired ? 'bg-red-500/20 text-red-400' : 'bg-primary/20 text-primary'
}`}>
<CategoryIcon icon={currentCategory.icon} className="w-4 h-4" />
</div> </div>
<div> <div>
<h3 className="font-bold text-white text-sm">{currentCategory.name}</h3> <h3 className="font-bold text-white text-sm flex items-center gap-2">
{currentCategory.name}
{isMissingRequired && (
<span className="text-[10px] bg-red-500/20 text-red-400 border border-red-500/40 px-2 py-0.5 rounded-full font-extrabold animate-pulse tracking-wider">
PFLICHTFELD AUSWÄHLEN!
</span>
)}
</h3>
{currentCategory.description && ( {currentCategory.description && (
<p className="text-slate-400 text-xs mt-0.5">{currentCategory.description}</p> <p className="text-slate-400 text-xs mt-0.5">{currentCategory.description}</p>
)} )}
@@ -107,8 +123,8 @@ export function StepSoftware({
<Check className="w-3 h-3 mr-1" /> Ausgewählt <Check className="w-3 h-3 mr-1" /> Ausgewählt
</Badge> </Badge>
) : currentCategory.is_required ? ( ) : currentCategory.is_required ? (
<Badge variant="destructive" className="ml-auto opacity-90 text-xs rounded-full bg-red-500/20 text-red-400 border border-red-500/30"> <Badge variant="destructive" className="ml-auto opacity-90 text-xs rounded-full bg-red-500/30 text-red-400 border border-red-500/50 animate-pulse font-extrabold">
<AlertCircle className="w-3 h-3 mr-1" /> Erforderlich <AlertCircle className="w-3 h-3 mr-1 text-red-400" /> Bitte auswählen!
</Badge> </Badge>
) : ( ) : (
<Badge variant="outline" className="ml-auto border-white/10 text-slate-400 text-xs rounded-full bg-white/5"> <Badge variant="outline" className="ml-auto border-white/10 text-slate-400 text-xs rounded-full bg-white/5">
@@ -116,6 +132,8 @@ export function StepSoftware({
</Badge> </Badge>
)} )}
</div> </div>
)
})()}
{catProducts.length === 0 ? ( {catProducts.length === 0 ? (
<p className="text-slate-500 text-sm italic"> <p className="text-slate-500 text-sm italic">

View File

@@ -236,10 +236,13 @@ export function SummarySidebar({
</div> </div>
)} )}
{!allCategoriesFilled && ( {!allCategoriesFilled && (
<p className="text-xs text-amber-400 flex items-center gap-1 bg-amber-500/10 p-2 rounded-lg border border-amber-500/20"> <div className="text-xs text-red-400 flex items-start gap-2 bg-red-500/10 p-3 rounded-xl border border-red-500/40 animate-eye-catcher">
<AlertCircle className="w-3.5 h-3.5 shrink-0 text-amber-400" /> <AlertCircle className="w-4 h-4 shrink-0 text-red-400 mt-0.5" />
Bitte aus jeder Pflichtkategorie einen Artikel wählen. <div>
</p> <p className="font-extrabold uppercase tracking-wider text-[11px] text-red-300">Pflichtkategorie fehlt!</p>
<p className="text-[11px] text-slate-300 leading-relaxed">Bitte wählen Sie aus allen rot markierten Pflichtkategorien einen Artikel aus.</p>
</div>
</div>
)} )}
{productValidationErrors.map((err, errIdx) => ( {productValidationErrors.map((err, errIdx) => (
<p key={errIdx} className="text-xs text-red-400 flex items-center gap-1 bg-red-500/10 p-2 rounded-lg border border-red-500/20"> <p key={errIdx} className="text-xs text-red-400 flex items-center gap-1 bg-red-500/10 p-2 rounded-lg border border-red-500/20">
@@ -277,12 +280,7 @@ export function SummarySidebar({
<CardFooter className="flex flex-col gap-2 shrink-0 p-4 pt-2 border-t border-white/10 bg-slate-950/80"> <CardFooter className="flex flex-col gap-2 shrink-0 p-4 pt-2 border-t border-white/10 bg-slate-950/80">
<Button <Button
className="w-full h-11 text-sm font-bold bg-gradient-to-r from-blue-600 via-sky-500 to-cyan-400 hover:opacity-90 shadow-[0_0_20px_rgba(56,189,248,0.3)] transition-all" className="w-full h-11 text-sm font-bold bg-gradient-to-r from-blue-600 via-sky-500 to-cyan-400 hover:opacity-90 shadow-[0_0_20px_rgba(56,189,248,0.3)] transition-all"
onClick={async () => { onClick={nextStep}
if (hasActiveSelection || editingIdx !== null) {
await addToBasket()
}
nextStep()
}}
disabled={isNextStepDisabled && basketItems.length === 0} disabled={isNextStepDisabled && basketItems.length === 0}
> >
Weiter zu Schritt 4 <ChevronRight className="ml-1 w-4 h-4" /> Weiter zu Schritt 4 <ChevronRight className="ml-1 w-4 h-4" />