Files
mintel.me/apps/web/src/components/ContactForm/steps/DesignStep.tsx
Marc Mintel 103d71851c
Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
chore: overhaul infrastructure and integrate @mintel packages
- Restructure to pnpm monorepo (site moved to apps/web)
- Integrate @mintel/tsconfig, @mintel/eslint-config, @mintel/husky-config
- Implement Docker service architecture (Varnish, Directus, Gatekeeper)
- Setup environment-aware Gitea Actions deployment
2026-02-05 14:18:51 +01:00

243 lines
10 KiB
TypeScript

'use client';
import * as React from 'react';
import { FormState } from '../types';
import { DESIGN_VIBES } from '../constants';
import { motion, AnimatePresence } from 'framer-motion';
import { Plus, X, Palette, Pipette, RefreshCw } from 'lucide-react';
import { Reveal } from '../../Reveal';
import { Input } from '../components/Input';
import { RepeatableList } from '../components/RepeatableList';
interface DesignStepProps {
state: FormState;
updateState: (updates: Partial<FormState>) => void;
}
export function DesignStep({ state, updateState }: DesignStepProps) {
const addColor = () => {
if (state.colorScheme.length < 5) {
updateState({ colorScheme: [...state.colorScheme, '#000000'] });
}
};
const removeColor = (index: number) => {
if (state.colorScheme.length > 1) {
const newScheme = [...state.colorScheme];
newScheme.splice(index, 1);
updateState({ colorScheme: newScheme });
}
};
const updateColor = (index: number, value: string) => {
const newScheme = [...state.colorScheme];
newScheme[index] = value;
updateState({ colorScheme: newScheme });
};
const toggleDontKnow = (id: string) => {
const current = state.dontKnows || [];
if (current.includes(id)) {
updateState({ dontKnows: current.filter(i => i !== id) });
} else {
updateState({ dontKnows: [...current, id] });
}
};
const generateHarmonicPalette = () => {
const hue = Math.floor(Math.random() * 360);
const saturation = 40 + Math.floor(Math.random() * 40);
const lightness = 40 + Math.floor(Math.random() * 40);
const hslToHex = (h: number, s: number, l: number) => {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}`;
};
const count = state.colorScheme.length;
const palette = [];
for (let i = 0; i < count; i++) {
const h = (hue + (i * (360 / count))) % 360;
const l = i === 0 ? 95 : i === count - 1 ? 20 : lightness;
palette.push(hslToHex(h, saturation, l));
}
updateState({ colorScheme: palette });
};
return (
<div className="space-y-16">
{/* Design Vibe */}
<Reveal width="100%" delay={0.1}>
<div className="space-y-8">
<div className="flex justify-between items-center">
<div className="space-y-1">
<h4 className="text-2xl font-bold text-slate-900">Design-Richtung</h4>
<p className="text-slate-500">Welche Ästhetik passt zu Ihrer Marke?</p>
</div>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('design_vibe')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('design_vibe') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{DESIGN_VIBES.map((vibe, index) => (
<motion.button
key={vibe.id}
whileHover={{ y: -5 }}
type="button"
onClick={() => updateState({ designVibe: vibe.id })}
className={`p-8 rounded-[2.5rem] border-2 text-left transition-all duration-500 focus:outline-none overflow-hidden relative group ${
state.designVibe === vibe.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-300'
}`}
>
<div className={`w-16 h-10 mb-6 transition-transform duration-500 group-hover:scale-110 ${state.designVibe === vibe.id ? 'text-white' : 'text-black'}`}>{vibe.illustration}</div>
<h4 className={`text-2xl font-bold mb-3 ${state.designVibe === vibe.id ? 'text-white' : 'text-slate-900'}`}>{vibe.label}</h4>
<p className={`text-lg leading-relaxed ${state.designVibe === vibe.id ? 'text-slate-200' : 'text-slate-500'}`}>{vibe.desc}</p>
{state.designVibe === vibe.id && (
<motion.div layoutId="activeVibe" className="absolute top-4 right-4 w-3 h-3 bg-white rounded-full" />
)}
</motion.button>
))}
</div>
</div>
</Reveal>
{/* Color Scheme */}
<Reveal width="100%" delay={0.2}>
<div className="space-y-12">
<div className="flex justify-between items-center">
<div className="space-y-1">
<h4 className="text-2xl font-bold text-slate-900">Farbschema</h4>
<p className="text-slate-500">Definieren Sie Ihre Markenfarben oder lassen Sie sich inspirieren.</p>
</div>
<div className="flex items-center gap-4">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={generateHarmonicPalette}
className="flex items-center gap-2 px-6 py-3 rounded-full text-sm font-bold bg-white border border-slate-200 text-slate-900 hover:border-slate-900 transition-all"
>
<RefreshCw size={16} />
Zufall
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={() => toggleDontKnow('color_scheme')}
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
state.dontKnows?.includes('color_scheme') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
}`}
>
Ich weiß es nicht
</motion.button>
</div>
</div>
{/* Custom Picker */}
<div className="space-y-8 p-10 bg-slate-50 rounded-[3rem] border border-slate-100">
<div className="flex items-center gap-3 text-slate-400 font-bold uppercase tracking-widest text-xs">
<Pipette size={16} />
Individuelle Farben
</div>
<div className="flex flex-wrap gap-6">
<AnimatePresence mode="popLayout">
{state.colorScheme.map((color, i) => (
<motion.div
key={`${i}-${color}`}
layout
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="relative group"
>
<div className="relative w-24 h-24 rounded-3xl overflow-hidden border-2 border-white group-hover:scale-105 transition-transform duration-300">
<input
type="color"
value={color}
onChange={(e) => updateColor(i, e.target.value)}
className="absolute inset-[-100%] w-[300%] h-[300%] cursor-pointer outline-none border-none appearance-none bg-transparent"
/>
<div className="absolute inset-0 pointer-events-none border border-black/5 rounded-3xl" />
</div>
<div className="mt-2 text-center font-mono text-[10px] text-slate-400 uppercase">{color}</div>
{state.colorScheme.length > 1 && (
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
type="button"
onClick={() => removeColor(i)}
className="absolute -top-3 -right-3 w-8 h-8 bg-white text-red-500 rounded-full flex items-center justify-center border border-slate-100 opacity-0 group-hover:opacity-100 transition-all duration-300 z-10"
>
<X size={16} strokeWidth={3} />
</motion.button>
)}
</motion.div>
))}
</AnimatePresence>
{state.colorScheme.length < 5 && (
<motion.button
layout
whileHover={{ scale: 1.05, borderColor: '#0f172a', color: '#0f172a' }}
whileTap={{ scale: 0.95 }}
type="button"
onClick={addColor}
className="w-24 h-24 rounded-3xl border-2 border-dashed border-slate-300 flex flex-col items-center justify-center text-slate-400 transition-all duration-300 bg-white/50 hover:bg-white"
>
<Plus size={32} />
<span className="text-[10px] font-bold uppercase mt-1">Add</span>
</motion.button>
)}
</div>
<p className="text-sm text-slate-400 font-medium">Klicken Sie auf eine Farbe, um sie anzupassen. Sie können bis zu 5 Farben definieren.</p>
</div>
</div>
</Reveal>
{/* References */}
<Reveal width="100%" delay={0.3}>
<div className="space-y-8">
<div className="space-y-1">
<h4 className="text-2xl font-bold text-slate-900">Referenz-Websites</h4>
<p className="text-slate-500">Gibt es Websites, die Ihnen besonders gut gefallen?</p>
</div>
<div className="p-10 bg-white border border-slate-100 rounded-[3rem]">
<RepeatableList
items={state.references || []}
onAdd={(v) => updateState({ references: [...(state.references || []), v] })}
onRemove={(i) => updateState({ references: (state.references || []).filter((_, idx) => idx !== i) })}
placeholder="https://beispiel.de"
/>
</div>
</div>
</Reveal>
{/* Wishes */}
<Reveal width="100%" delay={0.4}>
<Input
label="Individuelle Wünsche"
isTextArea
rows={4}
placeholder="Haben Sie weitere konkrete Vorstellungen?"
value={state.designWishes}
onChange={(e) => updateState({ designWishes: e.target.value })}
/>
</Reveal>
</div>
);
}