Some checks failed
🧪 CI (QA) / 🧪 Quality Assurance (push) Failing after 1m3s
- 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
243 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|