chore: stabilize apps/web (lint, build, typecheck fixes)
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Failing after 1m31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 6s
Build & Deploy / 🧪 QA (push) Failing after 1m27s
Build & Deploy / 🏗️ Build (push) Failing after 1m31s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
This commit is contained in:
@@ -4,13 +4,11 @@ import * as React from "react";
|
||||
import { FormState, Totals } from "../types";
|
||||
import { PRICING } from "../constants";
|
||||
import { AnimatedNumber } from "./AnimatedNumber";
|
||||
import {
|
||||
ConceptPrice,
|
||||
ConceptAutomation,
|
||||
} from "../../Landing/ConceptIllustrations";
|
||||
import { Info, Download, Share2, RefreshCw } from "lucide-react";
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import { ConceptAutomation } from "../../Landing/ConceptIllustrations";
|
||||
import { Download, Share2, RefreshCw } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import dynamic from "next/dynamic";
|
||||
// EstimationPDF will be imported dynamically where used or inside the and client-side block
|
||||
import IconWhite from "../../../assets/logo/Icon White Transparent.png";
|
||||
import LogoBlack from "../../../assets/logo/Logo Black Transparent.png";
|
||||
@@ -21,7 +19,7 @@ interface PriceCalculationProps {
|
||||
state: FormState;
|
||||
totals: Totals;
|
||||
isClient: boolean;
|
||||
qrCodeData: string;
|
||||
_qrCodeData: string;
|
||||
onShare?: () => void;
|
||||
}
|
||||
|
||||
@@ -29,7 +27,7 @@ export function PriceCalculation({
|
||||
state,
|
||||
totals,
|
||||
isClient,
|
||||
qrCodeData,
|
||||
_qrCodeData,
|
||||
onShare,
|
||||
}: PriceCalculationProps) {
|
||||
const {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { FormState } from './types';
|
||||
import * as React from "react";
|
||||
import { type FormState as _FormState } from "./types";
|
||||
import {
|
||||
PRICING as LOGIC_PRICING,
|
||||
PAGE_SAMPLES as LOGIC_PAGE_SAMPLES,
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
API_LABELS as LOGIC_API_LABELS,
|
||||
SOCIAL_LABELS as LOGIC_SOCIAL_LABELS,
|
||||
initialState as LOGIC_INITIAL_STATE,
|
||||
DESIGN_OPTIONS
|
||||
} from '../../logic/pricing';
|
||||
DESIGN_OPTIONS,
|
||||
} from "../../logic/pricing";
|
||||
|
||||
export const PRICING = LOGIC_PRICING;
|
||||
export const PAGE_SAMPLES = LOGIC_PAGE_SAMPLES;
|
||||
@@ -40,15 +40,50 @@ export const initialState = LOGIC_INITIAL_STATE;
|
||||
const VIBE_ILLUSTRATIONS: Record<string, React.ReactNode> = {
|
||||
minimal: (
|
||||
<svg viewBox="0 0 100 60" className="w-full h-full opacity-40">
|
||||
<rect x="10" y="10" width="80" height="2" rx="1" className="fill-current" />
|
||||
<rect x="10" y="20" width="50" height="2" rx="1" className="fill-current" />
|
||||
<rect x="10" y="40" width="30" height="10" rx="1" className="fill-current" />
|
||||
<rect
|
||||
x="10"
|
||||
y="10"
|
||||
width="80"
|
||||
height="2"
|
||||
rx="1"
|
||||
className="fill-current"
|
||||
/>
|
||||
<rect
|
||||
x="10"
|
||||
y="20"
|
||||
width="50"
|
||||
height="2"
|
||||
rx="1"
|
||||
className="fill-current"
|
||||
/>
|
||||
<rect
|
||||
x="10"
|
||||
y="40"
|
||||
width="30"
|
||||
height="10"
|
||||
rx="1"
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
bold: (
|
||||
<svg viewBox="0 0 100 60" className="w-full h-full opacity-40">
|
||||
<rect x="10" y="10" width="80" height="15" rx="2" className="fill-current" />
|
||||
<rect x="10" y="35" width="80" height="15" rx="2" className="fill-current" />
|
||||
<rect
|
||||
x="10"
|
||||
y="10"
|
||||
width="80"
|
||||
height="15"
|
||||
rx="2"
|
||||
className="fill-current"
|
||||
/>
|
||||
<rect
|
||||
x="10"
|
||||
y="35"
|
||||
width="80"
|
||||
height="15"
|
||||
rx="2"
|
||||
className="fill-current"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
nature: (
|
||||
@@ -59,14 +94,29 @@ const VIBE_ILLUSTRATIONS: Record<string, React.ReactNode> = {
|
||||
),
|
||||
tech: (
|
||||
<svg viewBox="0 0 100 60" className="w-full h-full opacity-40">
|
||||
<path d="M10 10 L90 10 L90 50 L10 50 Z" fill="none" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M10 30 L90 30" stroke="currentColor" strokeWidth="1" strokeDasharray="4 2" />
|
||||
<path d="M50 10 L50 50" stroke="currentColor" strokeWidth="1" strokeDasharray="4 2" />
|
||||
<path
|
||||
d="M10 10 L90 10 L90 50 L10 50 Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M10 30 L90 30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="4 2"
|
||||
/>
|
||||
<path
|
||||
d="M50 10 L50 50"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="4 2"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
export const DESIGN_VIBES = DESIGN_OPTIONS.map(opt => ({
|
||||
export const DESIGN_VIBES = DESIGN_OPTIONS.map((opt) => ({
|
||||
...opt,
|
||||
illustration: VIBE_ILLUSTRATIONS[opt.id]
|
||||
illustration: VIBE_ILLUSTRATIONS[opt.id],
|
||||
}));
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { Checkbox } from '../components/Checkbox';
|
||||
import { RepeatableList } from '../components/RepeatableList';
|
||||
import { Share2, ListPlus } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { RepeatableList } from "../components/RepeatableList";
|
||||
import { Share2, ListPlus } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Reveal } from "../../Reveal";
|
||||
|
||||
interface ApiStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
toggleItem: (list: string[], id: string) => string[];
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
toggleItem: (_list: string[], _id: string) => string[];
|
||||
}
|
||||
|
||||
export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
|
||||
const isWebApp = state.projectType === 'web-app';
|
||||
const isWebApp = state.projectType === "web-app";
|
||||
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -37,35 +37,61 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
{isWebApp ? 'Integrationen & Datenquellen' : 'Schnittstellen (API)'}
|
||||
{isWebApp
|
||||
? "Integrationen & Datenquellen"
|
||||
: "Schnittstellen (API)"}
|
||||
</h4>
|
||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">Optional</span>
|
||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||
Optional
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('api')}
|
||||
onClick={() => toggleDontKnow("api")}
|
||||
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
|
||||
state.dontKnows?.includes('api') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
state.dontKnows?.includes("api")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
</motion.button>
|
||||
</div>
|
||||
<p className="text-lg text-slate-500 leading-relaxed ml-2">
|
||||
{isWebApp
|
||||
? 'Mit welchen Systemen soll die Web App kommunizieren?'
|
||||
: 'Datenaustausch mit Drittsystemen zur Automatisierung.'}
|
||||
{isWebApp
|
||||
? "Mit welchen Systemen soll die Web App kommunizieren?"
|
||||
: "Datenaustausch mit Drittsystemen zur Automatisierung."}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ id: 'crm_erp', label: 'CRM / ERP', desc: 'HubSpot, Salesforce, SAP, Xentral etc.' },
|
||||
{ id: 'payment', label: 'Payment', desc: 'Stripe, PayPal, Klarna Integration.' },
|
||||
{ id: 'marketing', label: 'Marketing', desc: 'Newsletter (Mailchimp), Social Media Sync.' },
|
||||
{ id: 'ecommerce', label: 'E-Commerce', desc: 'Shopify, WooCommerce, Lagerbestand-Sync.' },
|
||||
{ id: 'maps', label: 'Google Maps / Places', desc: 'Standortsuche und Kartenintegration.' },
|
||||
{
|
||||
id: "crm_erp",
|
||||
label: "CRM / ERP",
|
||||
desc: "HubSpot, Salesforce, SAP, Xentral etc.",
|
||||
},
|
||||
{
|
||||
id: "payment",
|
||||
label: "Payment",
|
||||
desc: "Stripe, PayPal, Klarna Integration.",
|
||||
},
|
||||
{
|
||||
id: "marketing",
|
||||
label: "Marketing",
|
||||
desc: "Newsletter (Mailchimp), Social Media Sync.",
|
||||
},
|
||||
{
|
||||
id: "ecommerce",
|
||||
label: "E-Commerce",
|
||||
desc: "Shopify, WooCommerce, Lagerbestand-Sync.",
|
||||
},
|
||||
{
|
||||
id: "maps",
|
||||
label: "Google Maps / Places",
|
||||
desc: "Standortsuche und Kartenintegration.",
|
||||
},
|
||||
].map((opt, index) => (
|
||||
<motion.div
|
||||
key={opt.id}
|
||||
@@ -73,10 +99,15 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<Checkbox
|
||||
label={opt.label} desc={opt.desc}
|
||||
checked={state.apiSystems.includes(opt.id)}
|
||||
onChange={() => updateState({ apiSystems: toggleItem(state.apiSystems, opt.id) })}
|
||||
<Checkbox
|
||||
label={opt.label}
|
||||
desc={opt.desc}
|
||||
checked={state.apiSystems.includes(opt.id)}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
apiSystems: toggleItem(state.apiSystems, opt.id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -91,7 +122,9 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<ListPlus size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Weitere Systeme oder eigene APIs?</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Weitere Systeme oder eigene APIs?
|
||||
</h4>
|
||||
</div>
|
||||
<RepeatableList
|
||||
items={state.otherTech}
|
||||
@@ -106,6 +139,8 @@ export function ApiStep({ state, updateState, toggleItem }: ApiStepProps) {
|
||||
);
|
||||
|
||||
function updateTech(index: number) {
|
||||
updateState({ otherTech: state.otherTech.filter((_, idx) => idx !== index) });
|
||||
updateState({
|
||||
otherTech: state.otherTech.filter((_, idx) => idx !== index),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { ASSET_OPTIONS } from '../constants';
|
||||
import { Checkbox } from '../components/Checkbox';
|
||||
import { RepeatableList } from '../components/RepeatableList';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Minus, Plus, Briefcase, ListPlus } from 'lucide-react';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { ASSET_OPTIONS } from "../constants";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { RepeatableList } from "../components/RepeatableList";
|
||||
import { motion } from "framer-motion";
|
||||
import { Briefcase, ListPlus } from "lucide-react";
|
||||
import { Reveal } from "../../Reveal";
|
||||
|
||||
interface AssetsStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
toggleItem: (list: string[], id: string) => string[];
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
toggleItem: (_list: string[], _id: string) => string[];
|
||||
}
|
||||
|
||||
export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps) {
|
||||
export function AssetsStep({
|
||||
state,
|
||||
updateState,
|
||||
toggleItem,
|
||||
}: AssetsStepProps) {
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -34,15 +38,19 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<Briefcase size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Vorhandene Assets</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Vorhandene Assets
|
||||
</h4>
|
||||
</div>
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('assets')}
|
||||
onClick={() => toggleDontKnow("assets")}
|
||||
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
|
||||
state.dontKnows?.includes('assets') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
state.dontKnows?.includes("assets")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -58,11 +66,17 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
|
||||
>
|
||||
<div className="relative">
|
||||
<Checkbox
|
||||
key={opt.id} label={opt.label} desc={opt.desc}
|
||||
key={opt.id}
|
||||
label={opt.label}
|
||||
desc={opt.desc}
|
||||
checked={state.assets.includes(opt.id)}
|
||||
onChange={() => updateState({ assets: toggleItem(state.assets, opt.id) })}
|
||||
onChange={() =>
|
||||
updateState({ assets: toggleItem(state.assets, opt.id) })
|
||||
}
|
||||
/>
|
||||
{['logo', 'styleguide', 'content_concept'].includes(opt.id) && (
|
||||
{["logo", "styleguide", "content_concept"].includes(
|
||||
opt.id,
|
||||
) && (
|
||||
<div className="absolute top-4 right-4 px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||
Empfohlen
|
||||
</div>
|
||||
@@ -81,16 +95,23 @@ export function AssetsStep({ state, updateState, toggleItem }: AssetsStepProps)
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<ListPlus size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Weitere vorhandene Unterlagen?</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Weitere vorhandene Unterlagen?
|
||||
</h4>
|
||||
</div>
|
||||
<RepeatableList
|
||||
items={state.otherAssets}
|
||||
onAdd={(v) => updateState({ otherAssets: [...state.otherAssets, v] })}
|
||||
onRemove={(i) => updateState({ otherAssets: state.otherAssets.filter((_, idx) => idx !== i) })}
|
||||
onAdd={(v) =>
|
||||
updateState({ otherAssets: [...state.otherAssets, v] })
|
||||
}
|
||||
onRemove={(i) =>
|
||||
updateState({
|
||||
otherAssets: state.otherAssets.filter((_, idx) => idx !== i),
|
||||
})
|
||||
}
|
||||
placeholder="z.B. Lastenheft, Wireframes, Bilddatenbank..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { Checkbox } from '../components/Checkbox';
|
||||
import { RepeatableList } from '../components/RepeatableList';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Minus, Plus, FileText, ListPlus, HelpCircle, ArrowRight } from 'lucide-react';
|
||||
import { Input } from '../components/Input';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { RepeatableList } from "../components/RepeatableList";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Minus,
|
||||
Plus,
|
||||
FileText,
|
||||
ListPlus,
|
||||
HelpCircle,
|
||||
ArrowRight,
|
||||
} from "lucide-react";
|
||||
import { Input } from "../components/Input";
|
||||
|
||||
interface BaseStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
toggleItem: (list: string[], id: string) => string[];
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
toggleItem: (_list: string[], _id: string) => string[];
|
||||
}
|
||||
|
||||
export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -26,7 +33,7 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-16">
|
||||
<motion.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="space-y-8"
|
||||
@@ -52,12 +59,18 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-3xl font-bold text-slate-900 tracking-tight">Die Seitenstruktur</h4>
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">Essenziell</span>
|
||||
<h4 className="text-3xl font-bold text-slate-900 tracking-tight">
|
||||
Die Seitenstruktur
|
||||
</h4>
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||
Essenziell
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-400 mt-1">
|
||||
<HelpCircle size={14} className="shrink-0" />
|
||||
<span className="text-base">Wählen Sie die Bausteine Ihrer neuen Website.</span>
|
||||
<span className="text-base">
|
||||
Wählen Sie die Bausteine Ihrer neuen Website.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,9 +78,11 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('pages')}
|
||||
onClick={() => toggleDontKnow("pages")}
|
||||
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
|
||||
state.dontKnows?.includes('pages') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
state.dontKnows?.includes("pages")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -75,12 +90,36 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ id: 'Home', label: 'Startseite', desc: 'Der erste Eindruck Ihrer Marke.' },
|
||||
{ id: 'About', label: 'Über uns', desc: 'Ihre Geschichte und Ihr Team.' },
|
||||
{ id: 'Services', label: 'Leistungen', desc: 'Übersicht Ihres Angebots.' },
|
||||
{ id: 'Contact', label: 'Kontakt', desc: 'Anlaufstelle für Ihre Kunden.' },
|
||||
{ id: 'Landing', label: 'Landingpage', desc: 'Optimiert für Marketing-Kampagnen.' },
|
||||
{ id: 'Legal', label: 'Rechtliches', desc: 'Impressum & Datenschutz.' },
|
||||
{
|
||||
id: "Home",
|
||||
label: "Startseite",
|
||||
desc: "Der erste Eindruck Ihrer Marke.",
|
||||
},
|
||||
{
|
||||
id: "About",
|
||||
label: "Über uns",
|
||||
desc: "Ihre Geschichte und Ihr Team.",
|
||||
},
|
||||
{
|
||||
id: "Services",
|
||||
label: "Leistungen",
|
||||
desc: "Übersicht Ihres Angebots.",
|
||||
},
|
||||
{
|
||||
id: "Contact",
|
||||
label: "Kontakt",
|
||||
desc: "Anlaufstelle für Ihre Kunden.",
|
||||
},
|
||||
{
|
||||
id: "Landing",
|
||||
label: "Landingpage",
|
||||
desc: "Optimiert für Marketing-Kampagnen.",
|
||||
},
|
||||
{
|
||||
id: "Legal",
|
||||
label: "Rechtliches",
|
||||
desc: "Impressum & Datenschutz.",
|
||||
},
|
||||
].map((opt, index) => (
|
||||
<motion.div
|
||||
key={opt.id}
|
||||
@@ -88,10 +127,15 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<Checkbox
|
||||
label={opt.label} desc={opt.desc}
|
||||
checked={state.selectedPages.includes(opt.id)}
|
||||
onChange={() => updateState({ selectedPages: toggleItem(state.selectedPages, opt.id) })}
|
||||
<Checkbox
|
||||
label={opt.label}
|
||||
desc={opt.desc}
|
||||
checked={state.selectedPages.includes(opt.id)}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
selectedPages: toggleItem(state.selectedPages, opt.id),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -104,17 +148,23 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<ListPlus size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Weitere individuelle Seiten?</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Weitere individuelle Seiten?
|
||||
</h4>
|
||||
</div>
|
||||
<RepeatableList
|
||||
items={state.otherPages}
|
||||
onAdd={(v) => updateState({ otherPages: [...state.otherPages, v] })}
|
||||
onRemove={(i) => updateState({ otherPages: state.otherPages.filter((_, idx) => idx !== i) })}
|
||||
onRemove={(i) =>
|
||||
updateState({
|
||||
otherPages: state.otherPages.filter((_, idx) => idx !== i),
|
||||
})
|
||||
}
|
||||
placeholder="z.B. Karriere, FAQ, Team-Detail..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
@@ -123,18 +173,27 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:opacity-20 transition-opacity">
|
||||
<ListPlus size={120} />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-between items-center gap-8 relative z-10">
|
||||
<div>
|
||||
<h4 className="text-2xl font-bold text-white">Noch mehr Seiten?</h4>
|
||||
<p className="text-lg text-slate-400 mt-1">Falls Sie die Namen noch nicht wissen, aber die Menge schätzen können.</p>
|
||||
<h4 className="text-2xl font-bold text-white">
|
||||
Noch mehr Seiten?
|
||||
</h4>
|
||||
<p className="text-lg text-slate-400 mt-1">
|
||||
Falls Sie die Namen noch nicht wissen, aber die Menge schätzen
|
||||
können.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
type="button"
|
||||
onClick={() => updateState({ otherPagesCount: Math.max(0, state.otherPagesCount - 1) })}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateState({
|
||||
otherPagesCount: Math.max(0, state.otherPagesCount - 1),
|
||||
})
|
||||
}
|
||||
className="w-16 h-16 rounded-full bg-white/10 border border-white/10 flex items-center justify-center hover:bg-white/20 transition-colors focus:outline-none"
|
||||
>
|
||||
<Minus size={28} />
|
||||
@@ -154,7 +213,9 @@ export function BaseStep({ state, updateState, toggleItem }: BaseStepProps) {
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
type="button"
|
||||
onClick={() => updateState({ otherPagesCount: state.otherPagesCount + 1 })}
|
||||
onClick={() =>
|
||||
updateState({ otherPagesCount: state.otherPagesCount + 1 })
|
||||
}
|
||||
className="w-16 h-16 rounded-full bg-white/10 border border-white/10 flex items-center justify-center hover:bg-white/20 transition-colors focus:outline-none"
|
||||
>
|
||||
<Plus size={28} />
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { EMPLOYEE_OPTIONS } from '../constants';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Building2, Users } from 'lucide-react';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import { Input } from '../components/Input';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { EMPLOYEE_OPTIONS } from "../constants";
|
||||
import { motion } from "framer-motion";
|
||||
import { Building2, Users } from "lucide-react";
|
||||
import { Reveal } from "../../Reveal";
|
||||
import { Input } from "../components/Input";
|
||||
|
||||
interface CompanyStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
}
|
||||
|
||||
export function CompanyStep({ state, updateState }: CompanyStepProps) {
|
||||
@@ -23,7 +23,9 @@ export function CompanyStep({ state, updateState }: CompanyStepProps) {
|
||||
<Building2 size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Unternehmen</h4>
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">Erforderlich</span>
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||
Erforderlich
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
label="Name des Unternehmens"
|
||||
@@ -40,18 +42,23 @@ export function CompanyStep({ state, updateState }: CompanyStepProps) {
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<Users size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Mitarbeiteranzahl</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Mitarbeiteranzahl
|
||||
</h4>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{EMPLOYEE_OPTIONS.map((option, index) => (
|
||||
{EMPLOYEE_OPTIONS.map((option) => (
|
||||
<motion.button
|
||||
key={option.id}
|
||||
whileHover={{ y: -5 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => updateState({ employeeCount: option.id })}
|
||||
className={`p-6 rounded-[2rem] border-2 transition-all duration-300 font-bold text-lg ${state.employeeCount === option.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-300 text-slate-600'
|
||||
}`}
|
||||
className={`p-6 rounded-[2rem] border-2 transition-all duration-300 font-bold text-lg ${
|
||||
state.employeeCount === option.id
|
||||
? "border-slate-900 bg-slate-900 text-white"
|
||||
: "border-slate-100 bg-white hover:border-slate-300 text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</motion.button>
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { FileText, Upload, X, User, Mail, Briefcase, MessageSquare } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import { Input } from '../components/Input';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import {
|
||||
FileText,
|
||||
Upload,
|
||||
X,
|
||||
User,
|
||||
Mail,
|
||||
Briefcase,
|
||||
MessageSquare,
|
||||
} from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Reveal } from "../../Reveal";
|
||||
import { Input } from "../components/Input";
|
||||
|
||||
interface ContactStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
}
|
||||
|
||||
export function ContactStep({ state, updateState }: ContactStepProps) {
|
||||
@@ -19,8 +27,12 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
|
||||
<div className="p-8 bg-slate-50 text-slate-900 rounded-[2.5rem] mb-8 border border-slate-100">
|
||||
<h4 className="text-2xl font-bold mb-2">Fast geschafft! 🚀</h4>
|
||||
<p className="text-slate-500 text-lg">
|
||||
Ich habe alle Details für das Projekt von <span className="text-slate-900 font-bold">{state.companyName || 'Ihrem Unternehmen'}</span> erhalten.
|
||||
Hinterlassen Sie mir noch Ihre Kontaktdaten, damit ich Ihnen ein konkretes Angebot erstellen kann.
|
||||
Ich habe alle Details für das Projekt von{" "}
|
||||
<span className="text-slate-900 font-bold">
|
||||
{state.companyName || "Ihrem Unternehmen"}
|
||||
</span>{" "}
|
||||
erhalten. Hinterlassen Sie mir noch Ihre Kontaktdaten, damit ich
|
||||
Ihnen ein konkretes Angebot erstellen kann.
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
@@ -81,31 +93,50 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
|
||||
onChange={(e) => updateState({ message: e.target.value })}
|
||||
/>
|
||||
</Reveal>
|
||||
|
||||
|
||||
<Reveal width="100%" delay={0.4}>
|
||||
<div className="space-y-6">
|
||||
<p className="text-lg font-bold text-slate-900 ml-2">Dateien hochladen (optional)</p>
|
||||
<div
|
||||
<p className="text-lg font-bold text-slate-900 ml-2">
|
||||
Dateien hochladen (optional)
|
||||
</p>
|
||||
<div
|
||||
className={`relative group border-2 border-dashed rounded-[3rem] p-12 transition-all duration-500 flex flex-col items-center justify-center gap-6 cursor-pointer min-h-[250px] ${
|
||||
state.contactFiles.length > 0 ? 'border-slate-900 bg-slate-50' : 'border-slate-200 hover:border-slate-400 bg-white hover:shadow-xl'
|
||||
state.contactFiles.length > 0
|
||||
? "border-slate-900 bg-slate-50"
|
||||
: "border-slate-200 hover:border-slate-400 bg-white hover:shadow-xl"
|
||||
}`}
|
||||
onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length > 0) updateState({ contactFiles: [...state.contactFiles, ...files] });
|
||||
if (files.length > 0)
|
||||
updateState({
|
||||
contactFiles: [...state.contactFiles, ...files],
|
||||
});
|
||||
}}
|
||||
onClick={() => document.getElementById('contact-upload')?.click()}
|
||||
onClick={() => document.getElementById("contact-upload")?.click()}
|
||||
>
|
||||
<input id="contact-upload" type="file" multiple className="hidden" onChange={(e) => {
|
||||
const files = e.target.files ? Array.from(e.target.files) : [];
|
||||
if (files.length > 0) updateState({ contactFiles: [...state.contactFiles, ...files] });
|
||||
}} />
|
||||
|
||||
<input
|
||||
id="contact-upload"
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const files = e.target.files ? Array.from(e.target.files) : [];
|
||||
if (files.length > 0)
|
||||
updateState({
|
||||
contactFiles: [...state.contactFiles, ...files],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{state.contactFiles.length > 0 ? (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="files"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
@@ -113,7 +144,7 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
{state.contactFiles.map((file, idx) => (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key={`${file.name}-${idx}`}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
@@ -125,28 +156,42 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
|
||||
<FileText size={20} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-base truncate max-w-[250px]">{file.name}</span>
|
||||
<span className="text-[10px] text-slate-400 uppercase font-bold">{(file.size / 1024 / 1024).toFixed(2)} MB</span>
|
||||
<span className="font-bold text-base truncate max-w-[250px]">
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 uppercase font-bold">
|
||||
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1, backgroundColor: '#fee2e2', color: '#ef4444' }}
|
||||
<motion.button
|
||||
whileHover={{
|
||||
scale: 1.1,
|
||||
backgroundColor: "#fee2e2",
|
||||
color: "#ef4444",
|
||||
}}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
updateState({ contactFiles: state.contactFiles.filter((_, i) => i !== idx) });
|
||||
}}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
updateState({
|
||||
contactFiles: state.contactFiles.filter(
|
||||
(_, i) => i !== idx,
|
||||
),
|
||||
});
|
||||
}}
|
||||
className="p-2 bg-slate-50 text-slate-400 rounded-full transition-colors focus:outline-none"
|
||||
>
|
||||
<X size={20} />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
))}
|
||||
<p className="text-xs text-slate-400 text-center mt-8 font-medium">Klicken oder ziehen, um weitere Dateien hinzuzufügen</p>
|
||||
<p className="text-xs text-slate-400 text-center mt-8 font-medium">
|
||||
Klicken oder ziehen, um weitere Dateien hinzuzufügen
|
||||
</p>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key="empty"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -156,8 +201,12 @@ export function ContactStep({ state, updateState }: ContactStepProps) {
|
||||
<Upload size={32} />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-bold text-slate-900">Dateien hierher ziehen</p>
|
||||
<p className="text-lg text-slate-500 mt-1">oder klicken zum Auswählen</p>
|
||||
<p className="text-xl font-bold text-slate-900">
|
||||
Dateien hierher ziehen
|
||||
</p>
|
||||
<p className="text-lg text-slate-500 mt-1">
|
||||
oder klicken zum Auswählen
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { Zap, AlertCircle, Minus, Plus, Settings2, BarChart3 } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import {
|
||||
Zap,
|
||||
AlertCircle,
|
||||
Minus,
|
||||
Plus,
|
||||
Settings2,
|
||||
BarChart3,
|
||||
} from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Reveal } from "../../Reveal";
|
||||
|
||||
interface ContentStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
}
|
||||
|
||||
export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -30,21 +37,26 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<Settings2 size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Inhalte selbst verwalten (CMS)</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Inhalte selbst verwalten (CMS)
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-lg text-slate-500 leading-relaxed">
|
||||
Ein CMS (Content Management System) ermöglicht es Ihnen, Texte, Bilder und Blogartikel selbst zu ändern, ohne programmieren zu müssen.
|
||||
Ideal, wenn Sie Ihre Website aktuell halten möchten.
|
||||
Ein CMS (Content Management System) ermöglicht es Ihnen, Texte,
|
||||
Bilder und Blogartikel selbst zu ändern, ohne programmieren zu
|
||||
müssen. Ideal, wenn Sie Ihre Website aktuell halten möchten.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center md:items-end gap-6">
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('cms')}
|
||||
onClick={() => toggleDontKnow("cms")}
|
||||
className={`px-6 py-3 rounded-full text-sm font-bold transition-all ${
|
||||
state.dontKnows?.includes('cms') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
state.dontKnows?.includes("cms")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -52,12 +64,12 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateState({ cmsSetup: !state.cmsSetup })}
|
||||
className={`w-24 h-12 rounded-full transition-all duration-500 relative focus:outline-none ${state.cmsSetup ? 'bg-slate-900' : 'bg-slate-200'}`}
|
||||
className={`w-24 h-12 rounded-full transition-all duration-500 relative focus:outline-none ${state.cmsSetup ? "bg-slate-900" : "bg-slate-200"}`}
|
||||
>
|
||||
<motion.div
|
||||
<motion.div
|
||||
animate={{ x: state.cmsSetup ? 48 : 0 }}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||
className="absolute top-1.5 left-1.5 w-9 h-9 bg-white rounded-full"
|
||||
className="absolute top-1.5 left-1.5 w-9 h-9 bg-white rounded-full"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@@ -70,14 +82,24 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
<div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-black">
|
||||
<BarChart3 size={24} />
|
||||
</div>
|
||||
<p className="text-xl font-bold text-slate-900">Wie oft ändern sich Ihre Inhalte?</p>
|
||||
<p className="text-xl font-bold text-slate-900">
|
||||
Wie oft ändern sich Ihre Inhalte?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ id: 'low', label: 'Selten', desc: 'Wenige Male im Jahr.' },
|
||||
{ id: 'medium', label: 'Regelmäßig', desc: 'Monatliche Updates.' },
|
||||
{ id: 'high', label: 'Häufig', desc: 'Wöchentlich oder täglich.' },
|
||||
{ id: "low", label: "Selten", desc: "Wenige Male im Jahr." },
|
||||
{
|
||||
id: "medium",
|
||||
label: "Regelmäßig",
|
||||
desc: "Monatliche Updates.",
|
||||
},
|
||||
{
|
||||
id: "high",
|
||||
label: "Häufig",
|
||||
desc: "Wöchentlich oder täglich.",
|
||||
},
|
||||
].map((opt, index) => (
|
||||
<motion.button
|
||||
key={opt.id}
|
||||
@@ -86,20 +108,30 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
type="button"
|
||||
onClick={() => updateState({ expectedAdjustments: opt.id })}
|
||||
className={`p-6 rounded-[2rem] border-2 text-left transition-all duration-300 focus:outline-none ${
|
||||
state.expectedAdjustments === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-200 bg-white hover:border-slate-400'
|
||||
state.expectedAdjustments === opt.id
|
||||
? "border-slate-900 bg-slate-900 text-white"
|
||||
: "border-slate-200 bg-white hover:border-slate-400"
|
||||
}`}
|
||||
>
|
||||
<p className={`font-bold text-lg ${state.expectedAdjustments === opt.id ? 'text-white' : 'text-slate-900'}`}>{opt.label}</p>
|
||||
<p className={`text-sm mt-2 leading-relaxed ${state.expectedAdjustments === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
|
||||
<p
|
||||
className={`font-bold text-lg ${state.expectedAdjustments === opt.id ? "text-white" : "text-slate-900"}`}
|
||||
>
|
||||
{opt.label}
|
||||
</p>
|
||||
<p
|
||||
className={`text-sm mt-2 leading-relaxed ${state.expectedAdjustments === opt.id ? "text-slate-200" : "text-slate-500"}`}
|
||||
>
|
||||
{opt.desc}
|
||||
</p>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{state.expectedAdjustments === 'high' && !state.cmsSetup && (
|
||||
<motion.div
|
||||
{state.expectedAdjustments === "high" && !state.cmsSetup && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0, y: 20 }}
|
||||
animate={{ opacity: 1, height: 'auto', y: 0 }}
|
||||
animate={{ opacity: 1, height: "auto", y: 0 }}
|
||||
exit={{ opacity: 0, height: 0, y: 20 }}
|
||||
className="p-8 bg-amber-50 rounded-[2.5rem] border border-amber-100 flex gap-6 items-start"
|
||||
>
|
||||
@@ -107,9 +139,13 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
<AlertCircle size={24} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-amber-900 text-xl font-bold">Empfehlung: CMS nutzen</p>
|
||||
<p className="text-amber-900 text-xl font-bold">
|
||||
Empfehlung: CMS nutzen
|
||||
</p>
|
||||
<p className="text-amber-800 text-base leading-relaxed max-w-3xl">
|
||||
Bei täglichen oder wöchentlichen Änderungen sparen Sie mit einem CMS langfristig viel Geld, da Sie keine externen Entwickler für Inhalts-Updates benötigen.
|
||||
Bei täglichen oder wöchentlichen Änderungen sparen Sie mit
|
||||
einem CMS langfristig viel Geld, da Sie keine externen
|
||||
Entwickler für Inhalts-Updates benötigen.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -122,7 +158,8 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
<Zap size={18} /> Vorteil CMS
|
||||
</div>
|
||||
<p className="text-base text-slate-500 leading-relaxed">
|
||||
Volle Kontrolle über Ihre Inhalte und keine laufenden Kosten für kleine Textänderungen oder neue Blog-Beiträge.
|
||||
Volle Kontrolle über Ihre Inhalte und keine laufenden Kosten für
|
||||
kleine Textänderungen oder neue Blog-Beiträge.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-8 bg-white rounded-[2.5rem] border border-slate-100 space-y-4">
|
||||
@@ -130,7 +167,8 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
<AlertCircle size={18} /> Fokus Design
|
||||
</div>
|
||||
<p className="text-base text-slate-500 leading-relaxed">
|
||||
Ohne CMS bleibt die technische Komplexität geringer und das Design ist maximal geschützt vor ungewollten Änderungen.
|
||||
Ohne CMS bleibt die technische Komplexität geringer und das
|
||||
Design ist maximal geschützt vor ungewollten Änderungen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,24 +178,30 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
<Reveal width="100%" delay={0.3}>
|
||||
<div className="flex flex-col gap-8 p-10 bg-white border border-slate-100 rounded-[3rem]">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-2xl font-bold text-slate-900">Inhalte einpflegen</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Inhalte einpflegen
|
||||
</h4>
|
||||
<p className="text-lg text-slate-500 leading-relaxed">
|
||||
Wer kümmert sich um die erste Befüllung? Wenn ich das übernehmen soll, geben Sie hier die Anzahl der Datensätze (z.B. fertige Blogartikel oder Produkte) an.
|
||||
Ansonsten übergeben wir Ihnen eine leere, aber einsatzbereite Struktur.
|
||||
Wer kümmert sich um die erste Befüllung? Wenn ich das übernehmen
|
||||
soll, geben Sie hier die Anzahl der Datensätze (z.B. fertige
|
||||
Blogartikel oder Produkte) an. Ansonsten übergeben wir Ihnen eine
|
||||
leere, aber einsatzbereite Struktur.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-12 py-2">
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
type="button"
|
||||
onClick={() => updateState({ newDatasets: Math.max(0, state.newDatasets - 1) })}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateState({ newDatasets: Math.max(0, state.newDatasets - 1) })
|
||||
}
|
||||
className="w-16 h-16 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none"
|
||||
>
|
||||
<Minus size={28} />
|
||||
</motion.button>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.span
|
||||
<motion.span
|
||||
key={state.newDatasets}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -167,11 +211,13 @@ export function ContentStep({ state, updateState }: ContentStepProps) {
|
||||
{state.newDatasets}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
type="button"
|
||||
onClick={() => updateState({ newDatasets: state.newDatasets + 1 })}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateState({ newDatasets: state.newDatasets + 1 })
|
||||
}
|
||||
className="w-16 h-16 rounded-full bg-slate-50 border border-slate-100 flex items-center justify-center hover:border-slate-900 transition-colors focus:outline-none"
|
||||
>
|
||||
<Plus size={28} />
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
'use client';
|
||||
"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';
|
||||
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;
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
}
|
||||
|
||||
export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
const addColor = () => {
|
||||
if (state.colorScheme.length < 5) {
|
||||
updateState({ colorScheme: [...state.colorScheme, '#000000'] });
|
||||
updateState({ colorScheme: [...state.colorScheme, "#000000"] });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -51,11 +51,13 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
|
||||
const hslToHex = (h: number, s: number, l: number) => {
|
||||
l /= 100;
|
||||
const a = s * Math.min(l, 1 - 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 Math.round(255 * color)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
};
|
||||
return `#${f(0)}${f(8)}${f(4)}`;
|
||||
};
|
||||
@@ -63,7 +65,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
const count = state.colorScheme.length;
|
||||
const palette = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const h = (hue + (i * (360 / count))) % 360;
|
||||
const h = (hue + i * (360 / count)) % 360;
|
||||
const l = i === 0 ? 95 : i === count - 1 ? 20 : lightness;
|
||||
palette.push(hslToHex(h, saturation, l));
|
||||
}
|
||||
@@ -77,16 +79,22 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
<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>
|
||||
<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
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('design_vibe')}
|
||||
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'
|
||||
state.dontKnows?.includes("design_vibe")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -100,14 +108,31 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
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'
|
||||
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>
|
||||
<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.div
|
||||
layoutId="activeVibe"
|
||||
className="absolute top-4 right-4 w-3 h-3 bg-white rounded-full"
|
||||
/>
|
||||
)}
|
||||
</motion.button>
|
||||
))}
|
||||
@@ -121,10 +146,13 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
<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>
|
||||
<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
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
@@ -134,13 +162,15 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
<RefreshCw size={16} />
|
||||
Zufall
|
||||
</motion.button>
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('color_scheme')}
|
||||
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'
|
||||
state.dontKnows?.includes("color_scheme")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -157,7 +187,7 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
<div className="flex flex-wrap gap-6">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{state.colorScheme.map((color, i) => (
|
||||
<motion.div
|
||||
<motion.div
|
||||
key={`${i}-${color}`}
|
||||
layout
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
@@ -166,17 +196,19 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
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}
|
||||
<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>
|
||||
<div className="mt-2 text-center font-mono text-[10px] text-slate-400 uppercase">
|
||||
{color}
|
||||
</div>
|
||||
{state.colorScheme.length > 1 && (
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
type="button"
|
||||
@@ -190,20 +222,29 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
))}
|
||||
</AnimatePresence>
|
||||
{state.colorScheme.length < 5 && (
|
||||
<motion.button
|
||||
<motion.button
|
||||
layout
|
||||
whileHover={{ scale: 1.05, borderColor: '#0f172a', color: '#0f172a' }}
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -212,14 +253,26 @@ export function DesignStep({ state, updateState }: DesignStepProps) {
|
||||
<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>
|
||||
<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) })}
|
||||
onAdd={(v) =>
|
||||
updateState({ references: [...(state.references || []), v] })
|
||||
}
|
||||
onRemove={(i) =>
|
||||
updateState({
|
||||
references: (state.references || []).filter(
|
||||
(_, idx) => idx !== i,
|
||||
),
|
||||
})
|
||||
}
|
||||
placeholder="https://beispiel.de"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { FEATURE_OPTIONS } from '../constants';
|
||||
import { Checkbox } from '../components/Checkbox';
|
||||
import { RepeatableList } from '../components/RepeatableList';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Minus, Plus, LayoutGrid, ListPlus, HelpCircle } from 'lucide-react';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { FEATURE_OPTIONS } from "../constants";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { RepeatableList } from "../components/RepeatableList";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Minus, Plus, LayoutGrid, ListPlus, HelpCircle } from "lucide-react";
|
||||
|
||||
interface FeaturesStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
toggleItem: (list: string[], id: string) => string[];
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
toggleItem: (_list: string[], _id: string) => string[];
|
||||
}
|
||||
|
||||
export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepProps) {
|
||||
export function FeaturesStep({
|
||||
state,
|
||||
updateState,
|
||||
toggleItem,
|
||||
}: FeaturesStepProps) {
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -34,22 +38,31 @@ export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepPro
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-2xl font-bold text-slate-900">System-Module</h4>
|
||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">Optional</span>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
System-Module
|
||||
</h4>
|
||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||
Optional
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-slate-400 mt-1">
|
||||
<HelpCircle size={14} />
|
||||
<span className="text-sm">Module sind funktionale Einheiten, die über einfache Textseiten hinausgehen.</span>
|
||||
<span className="text-sm">
|
||||
Module sind funktionale Einheiten, die über einfache
|
||||
Textseiten hinausgehen.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('features')}
|
||||
onClick={() => toggleDontKnow("features")}
|
||||
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
|
||||
state.dontKnows?.includes('features') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
state.dontKnows?.includes("features")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -63,10 +76,13 @@ export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepPro
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<Checkbox
|
||||
label={opt.label} desc={opt.desc}
|
||||
checked={state.features.includes(opt.id)}
|
||||
onChange={() => updateState({ features: toggleItem(state.features, opt.id) })}
|
||||
<Checkbox
|
||||
label={opt.label}
|
||||
desc={opt.desc}
|
||||
checked={state.features.includes(opt.id)}
|
||||
onChange={() =>
|
||||
updateState({ features: toggleItem(state.features, opt.id) })
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -79,16 +95,25 @@ export function FeaturesStep({ state, updateState, toggleItem }: FeaturesStepPro
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<ListPlus size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Weitere inhaltliche Module?</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Weitere inhaltliche Module?
|
||||
</h4>
|
||||
</div>
|
||||
<RepeatableList
|
||||
items={state.otherFeatures}
|
||||
onAdd={(v) => updateState({ otherFeatures: [...state.otherFeatures, v] })}
|
||||
onRemove={(i) => updateState({ otherFeatures: state.otherFeatures.filter((_, idx) => idx !== i) })}
|
||||
onAdd={(v) =>
|
||||
updateState({ otherFeatures: [...state.otherFeatures, v] })
|
||||
}
|
||||
onRemove={(i) =>
|
||||
updateState({
|
||||
otherFeatures: state.otherFeatures.filter(
|
||||
(_, idx) => idx !== i,
|
||||
),
|
||||
})
|
||||
}
|
||||
placeholder="z.B. Glossar, Download-Center, Partner-Bereich..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { Checkbox } from '../components/Checkbox';
|
||||
import { RepeatableList } from '../components/RepeatableList';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Minus, Plus, Cpu, ListPlus } from 'lucide-react';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import { RepeatableList } from "../components/RepeatableList";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Minus, Plus, Cpu, ListPlus } from "lucide-react";
|
||||
import { Reveal } from "../../Reveal";
|
||||
|
||||
interface FunctionsStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
toggleItem: (list: string[], id: string) => string[];
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
toggleItem: (_list: string[], _id: string) => string[];
|
||||
}
|
||||
|
||||
export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepProps) {
|
||||
const isWebApp = state.projectType === 'web-app';
|
||||
export function FunctionsStep({
|
||||
state,
|
||||
updateState,
|
||||
toggleItem,
|
||||
}: FunctionsStepProps) {
|
||||
const isWebApp = state.projectType === "web-app";
|
||||
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -36,16 +40,20 @@ export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepP
|
||||
<Cpu size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
{isWebApp ? 'Funktionale Anforderungen' : 'Erweiterte Funktionen'}
|
||||
{isWebApp
|
||||
? "Funktionale Anforderungen"
|
||||
: "Erweiterte Funktionen"}
|
||||
</h4>
|
||||
</div>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('functions')}
|
||||
onClick={() => toggleDontKnow("functions")}
|
||||
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
|
||||
state.dontKnows?.includes('functions') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
state.dontKnows?.includes("functions")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -54,63 +62,118 @@ export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepP
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{isWebApp ? (
|
||||
<>
|
||||
<Checkbox
|
||||
label="Dashboard & Analytics" desc="Visualisierung von Daten und Kennzahlen."
|
||||
checked={state.functions.includes('dashboard')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'dashboard') })}
|
||||
<Checkbox
|
||||
label="Dashboard & Analytics"
|
||||
desc="Visualisierung von Daten und Kennzahlen."
|
||||
checked={state.functions.includes("dashboard")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "dashboard"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Dateiverwaltung" desc="Upload, Download und Organisation von Dokumenten."
|
||||
checked={state.functions.includes('files')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'files') })}
|
||||
<Checkbox
|
||||
label="Dateiverwaltung"
|
||||
desc="Upload, Download und Organisation von Dokumenten."
|
||||
checked={state.functions.includes("files")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "files"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Benachrichtigungen" desc="E-Mail, Push oder In-App Alerts."
|
||||
checked={state.functions.includes('notifications')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'notifications') })}
|
||||
<Checkbox
|
||||
label="Benachrichtigungen"
|
||||
desc="E-Mail, Push oder In-App Alerts."
|
||||
checked={state.functions.includes("notifications")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "notifications"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Export-Funktionen" desc="CSV, Excel oder PDF Generierung."
|
||||
checked={state.functions.includes('export')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'export') })}
|
||||
<Checkbox
|
||||
label="Export-Funktionen"
|
||||
desc="CSV, Excel oder PDF Generierung."
|
||||
checked={state.functions.includes("export")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "export"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Checkbox
|
||||
label="Suche" desc="Volltextsuche über alle Inhalte."
|
||||
checked={state.functions.includes('search')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'search') })}
|
||||
label="Suche"
|
||||
desc="Volltextsuche über alle Inhalte."
|
||||
checked={state.functions.includes("search")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "search"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Filter-Systeme" desc="Kategorisierung und Sortierung."
|
||||
checked={state.functions.includes('filter')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'filter') })}
|
||||
label="Filter-Systeme"
|
||||
desc="Kategorisierung und Sortierung."
|
||||
checked={state.functions.includes("filter")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "filter"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="PDF-Export" desc="Automatisierte PDF-Erstellung."
|
||||
checked={state.functions.includes('pdf')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'pdf') })}
|
||||
label="PDF-Export"
|
||||
desc="Automatisierte PDF-Erstellung."
|
||||
checked={state.functions.includes("pdf")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "pdf"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Erweiterte Formulare" desc="Komplexe Abfragen & Logik."
|
||||
checked={state.functions.includes('forms')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'forms') })}
|
||||
label="Erweiterte Formulare"
|
||||
desc="Komplexe Abfragen & Logik."
|
||||
checked={state.functions.includes("forms")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "forms"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Mitgliederbereich" desc="Login-Bereich für exklusive Inhalte."
|
||||
checked={state.functions.includes('members')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'members') })}
|
||||
label="Mitgliederbereich"
|
||||
desc="Login-Bereich für exklusive Inhalte."
|
||||
checked={state.functions.includes("members")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "members"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Event-Kalender" desc="Verwaltung und Anzeige von Terminen."
|
||||
checked={state.functions.includes('calendar')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'calendar') })}
|
||||
label="Event-Kalender"
|
||||
desc="Verwaltung und Anzeige von Terminen."
|
||||
checked={state.functions.includes("calendar")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "calendar"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Echtzeit-Chat" desc="Direkte Kommunikation mit Besuchern."
|
||||
checked={state.functions.includes('chat')}
|
||||
onChange={() => updateState({ functions: toggleItem(state.functions, 'chat') })}
|
||||
label="Echtzeit-Chat"
|
||||
desc="Direkte Kommunikation mit Besuchern."
|
||||
checked={state.functions.includes("chat")}
|
||||
onChange={() =>
|
||||
updateState({
|
||||
functions: toggleItem(state.functions, "chat"),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -125,16 +188,29 @@ export function FunctionsStep({ state, updateState, toggleItem }: FunctionsStepP
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<ListPlus size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Weitere spezifische Wünsche?</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Weitere spezifische Wünsche?
|
||||
</h4>
|
||||
</div>
|
||||
<RepeatableList
|
||||
items={state.otherFunctions}
|
||||
onAdd={(v) => updateState({ otherFunctions: [...state.otherFunctions, v] })}
|
||||
onRemove={(i) => updateState({ otherFunctions: state.otherFunctions.filter((_, idx) => idx !== i) })}
|
||||
placeholder={isWebApp ? "z.B. Echtzeit-Chat, KI-Anbindung, Offline-Modus..." : "z.B. Mitgliederbereich, Event-Kalender, geschützte Downloads..."}
|
||||
onAdd={(v) =>
|
||||
updateState({ otherFunctions: [...state.otherFunctions, v] })
|
||||
}
|
||||
onRemove={(i) =>
|
||||
updateState({
|
||||
otherFunctions: state.otherFunctions.filter(
|
||||
(_, idx) => idx !== i,
|
||||
),
|
||||
})
|
||||
}
|
||||
placeholder={
|
||||
isWebApp
|
||||
? "z.B. Echtzeit-Chat, KI-Anbindung, Offline-Modus..."
|
||||
: "z.B. Mitgliederbereich, Event-Kalender, geschützte Downloads..."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { Globe, Info, Plus, X } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { Globe, Info, Plus, X } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Reveal } from "../../Reveal";
|
||||
|
||||
interface LanguageStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
}
|
||||
|
||||
const COMMON_LANGUAGES = [
|
||||
{ id: 'de', label: 'Deutsch' },
|
||||
{ id: 'en', label: 'Englisch' },
|
||||
{ id: 'fr', label: 'Französisch' },
|
||||
{ id: 'es', label: 'Spanisch' },
|
||||
{ id: 'it', label: 'Italienisch' },
|
||||
{ id: "de", label: "Deutsch" },
|
||||
{ id: "en", label: "Englisch" },
|
||||
{ id: "fr", label: "Französisch" },
|
||||
{ id: "es", label: "Spanisch" },
|
||||
{ id: "it", label: "Italienisch" },
|
||||
];
|
||||
|
||||
export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
const basePriceExplanation = "Jede zusätzliche Sprache erhöht den Gesamtaufwand für Design, Entwicklung und Qualitätssicherung um ca. 20%. Dies deckt die technische Implementierung der Übersetzungsschicht sowie die Anpassung von Layouts für unterschiedliche Textlängen ab.";
|
||||
const basePriceExplanation =
|
||||
"Jede zusätzliche Sprache erhöht den Gesamtaufwand für Design, Entwicklung und Qualitätssicherung um ca. 20%. Dies deckt die technische Implementierung der Übersetzungsschicht sowie die Anpassung von Layouts für unterschiedliche Textlängen ab.";
|
||||
|
||||
const toggleLanguage = (lang: string) => {
|
||||
const current = state.languagesList || [];
|
||||
if (current.includes(lang)) {
|
||||
updateState({ languagesList: current.filter(l => l !== lang) });
|
||||
updateState({ languagesList: current.filter((l) => l !== lang) });
|
||||
} else {
|
||||
updateState({ languagesList: [...current, lang] });
|
||||
}
|
||||
@@ -34,7 +35,7 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -52,15 +53,19 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
<Globe size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Sprachen</h4>
|
||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">Optional</span>
|
||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||
Optional
|
||||
</span>
|
||||
</div>
|
||||
<motion.button
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('languages')}
|
||||
onClick={() => toggleDontKnow("languages")}
|
||||
className={`px-6 py-3 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
|
||||
state.dontKnows?.includes('languages') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
state.dontKnows?.includes("languages")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -71,7 +76,7 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
<p className="text-lg text-slate-500 leading-relaxed ml-2">
|
||||
Welche Sprachen soll Ihre Website unterstützen?
|
||||
</p>
|
||||
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{COMMON_LANGUAGES.map((lang) => (
|
||||
<motion.button
|
||||
@@ -82,8 +87,8 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
onClick={() => toggleLanguage(lang.label)}
|
||||
className={`px-8 py-4 rounded-2xl font-bold transition-all border-2 ${
|
||||
state.languagesList.includes(lang.label)
|
||||
? 'bg-slate-900 border-slate-900 text-white'
|
||||
: 'bg-white border-slate-100 text-slate-600 hover:border-slate-300'
|
||||
? "bg-slate-900 border-slate-900 text-white"
|
||||
: "bg-white border-slate-100 text-slate-600 hover:border-slate-300"
|
||||
}`}
|
||||
>
|
||||
{lang.label}
|
||||
@@ -98,12 +103,14 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
placeholder="Weitere Sprache hinzufügen..."
|
||||
className="flex-1 p-6 bg-white border border-slate-100 rounded-2xl focus:outline-none focus:border-slate-900 transition-colors text-lg"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const val = e.currentTarget.value.trim();
|
||||
if (val && !state.languagesList.includes(val)) {
|
||||
updateState({ languagesList: [...state.languagesList, val] });
|
||||
e.currentTarget.value = '';
|
||||
updateState({
|
||||
languagesList: [...state.languagesList, val],
|
||||
});
|
||||
e.currentTarget.value = "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -113,24 +120,32 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<AnimatePresence>
|
||||
{state.languagesList.filter(l => !COMMON_LANGUAGES.find(cl => cl.label === l)).map((lang, i) => (
|
||||
<motion.div
|
||||
key={lang}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="flex items-center gap-3 px-6 py-3 bg-slate-100 rounded-full text-base font-bold text-slate-700"
|
||||
>
|
||||
<span>{lang}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateState({ languagesList: state.languagesList.filter(l => l !== lang) })}
|
||||
className="text-slate-400 hover:text-slate-900 transition-colors"
|
||||
{state.languagesList
|
||||
.filter((l) => !COMMON_LANGUAGES.find((cl) => cl.label === l))
|
||||
.map((lang, i) => (
|
||||
<motion.div
|
||||
key={lang}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="flex items-center gap-3 px-6 py-3 bg-slate-100 rounded-full text-base font-bold text-slate-700"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</motion.div>
|
||||
))}
|
||||
<span>{lang}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateState({
|
||||
languagesList: state.languagesList.filter(
|
||||
(l) => l !== lang,
|
||||
),
|
||||
})
|
||||
}
|
||||
className="text-slate-400 hover:text-slate-900 transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,7 +157,9 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-white/5 rounded-full -mr-32 -mt-32 blur-3xl" />
|
||||
<div className="flex items-center gap-4 text-slate-400 relative z-10">
|
||||
<Info size={24} />
|
||||
<span className="text-sm font-bold uppercase tracking-widest">Warum dieser Faktor?</span>
|
||||
<span className="text-sm font-bold uppercase tracking-widest">
|
||||
Warum dieser Faktor?
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-lg leading-relaxed text-slate-300 relative z-10 max-w-3xl">
|
||||
{basePriceExplanation}
|
||||
@@ -150,13 +167,15 @@ export function LanguageStep({ state, updateState }: LanguageStepProps) {
|
||||
{languagesCount > 1 && (
|
||||
<div className="pt-8 border-t border-white/10 relative z-10">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-lg font-medium text-slate-400">Aktueller Aufschlagsfaktor:</span>
|
||||
<motion.span
|
||||
<span className="text-lg font-medium text-slate-400">
|
||||
Aktueller Aufschlagsfaktor:
|
||||
</span>
|
||||
<motion.span
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{ scale: 1 }}
|
||||
className="text-4xl font-bold text-white"
|
||||
>
|
||||
+{((languagesCount - 1) * 20)}%
|
||||
+{(languagesCount - 1) * 20}%
|
||||
</motion.span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,48 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { Checkbox } from '../components/Checkbox';
|
||||
import { Link2, Globe, Share2, Instagram, Linkedin, Facebook, Twitter, Youtube } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import { Input } from '../components/Input';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { Checkbox } from "../components/Checkbox";
|
||||
import {
|
||||
Link2,
|
||||
Globe,
|
||||
Share2,
|
||||
Instagram,
|
||||
Linkedin,
|
||||
Facebook,
|
||||
Twitter,
|
||||
Youtube,
|
||||
} from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Reveal } from "../../Reveal";
|
||||
import { Input } from "../components/Input";
|
||||
|
||||
interface PresenceStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
toggleItem: (list: string[], id: string) => string[];
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
toggleItem: (_list: string[], _id: string) => string[];
|
||||
}
|
||||
|
||||
export function PresenceStep({ state, updateState, toggleItem }: PresenceStepProps) {
|
||||
export function PresenceStep({
|
||||
state,
|
||||
updateState,
|
||||
toggleItem,
|
||||
}: PresenceStepProps) {
|
||||
const updateUrl = (id: string, url: string) => {
|
||||
updateState({
|
||||
socialMediaUrls: {
|
||||
...state.socialMediaUrls,
|
||||
[id]: url
|
||||
}
|
||||
[id]: url,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'instagram', label: 'Instagram', icon: Instagram },
|
||||
{ id: 'linkedin', label: 'LinkedIn', icon: Linkedin },
|
||||
{ id: 'facebook', label: 'Facebook', icon: Facebook },
|
||||
{ id: 'twitter', label: 'Twitter / X', icon: Twitter },
|
||||
{ id: 'youtube', label: 'YouTube', icon: Youtube },
|
||||
{ id: "instagram", label: "Instagram", icon: Instagram },
|
||||
{ id: "linkedin", label: "LinkedIn", icon: Linkedin },
|
||||
{ id: "facebook", label: "Facebook", icon: Facebook },
|
||||
{ id: "twitter", label: "Twitter / X", icon: Twitter },
|
||||
{ id: "youtube", label: "YouTube", icon: Youtube },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -40,8 +53,12 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<Globe size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Bestehende Website</h4>
|
||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">Optional</span>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Bestehende Website
|
||||
</h4>
|
||||
<span className="px-2 py-1 bg-slate-50 text-slate-400 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||
Optional
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
label="URL (falls vorhanden)"
|
||||
@@ -79,9 +96,11 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
|
||||
<div className="w-12 h-12 bg-slate-50 rounded-2xl flex items-center justify-center text-black">
|
||||
<Share2 size={24} />
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900">Social Media Accounts</h4>
|
||||
<h4 className="text-2xl font-bold text-slate-900">
|
||||
Social Media Accounts
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-6">
|
||||
{SOCIAL_PLATFORMS.map((platform) => {
|
||||
const isSelected = state.socialMedia.includes(platform.id);
|
||||
@@ -92,15 +111,25 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
|
||||
whileHover={{ y: -8, scale: 1.02 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
type="button"
|
||||
onClick={() => updateState({ socialMedia: toggleItem(state.socialMedia, platform.id) })}
|
||||
onClick={() =>
|
||||
updateState({
|
||||
socialMedia: toggleItem(state.socialMedia, platform.id),
|
||||
})
|
||||
}
|
||||
className={`flex flex-col items-center gap-4 p-8 rounded-[2.5rem] border-2 transition-all duration-500 ${
|
||||
isSelected ? 'border-slate-900 bg-slate-900 text-white shadow-2xl shadow-slate-400' : 'border-slate-100 bg-white text-slate-400 hover:border-slate-300 hover:shadow-xl'
|
||||
isSelected
|
||||
? "border-slate-900 bg-slate-900 text-white shadow-2xl shadow-slate-400"
|
||||
: "border-slate-100 bg-white text-slate-400 hover:border-slate-300 hover:shadow-xl"
|
||||
}`}
|
||||
>
|
||||
<div className={`p-4 rounded-2xl transition-colors duration-500 ${isSelected ? 'bg-white/10 text-white' : 'bg-slate-50 text-slate-400'}`}>
|
||||
<div
|
||||
className={`p-4 rounded-2xl transition-colors duration-500 ${isSelected ? "bg-white/10 text-white" : "bg-slate-50 text-slate-400"}`}
|
||||
>
|
||||
<Icon size={32} />
|
||||
</div>
|
||||
<span className="font-bold text-base tracking-tight">{platform.label}</span>
|
||||
<span className="font-bold text-base tracking-tight">
|
||||
{platform.label}
|
||||
</span>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
@@ -109,7 +138,7 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
|
||||
<div className="space-y-4">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{state.socialMedia.map((id) => {
|
||||
const platform = SOCIAL_PLATFORMS.find(p => p.id === id);
|
||||
const platform = SOCIAL_PLATFORMS.find((p) => p.id === id);
|
||||
if (!platform) return null;
|
||||
return (
|
||||
<motion.div
|
||||
@@ -121,14 +150,16 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
|
||||
className="relative group"
|
||||
>
|
||||
<div className="absolute left-6 top-1/2 -translate-y-1/2 flex items-center gap-3 text-slate-400 group-focus-within:text-slate-900 transition-colors">
|
||||
<span className="font-bold text-xs uppercase tracking-widest w-20">{platform.label}</span>
|
||||
<span className="font-bold text-xs uppercase tracking-widest w-20">
|
||||
{platform.label}
|
||||
</span>
|
||||
<div className="w-[1px] h-4 bg-slate-200" />
|
||||
<Link2 size={18} />
|
||||
</div>
|
||||
<input
|
||||
type="url"
|
||||
placeholder={`https://${platform.id}.com/ihr-profil`}
|
||||
value={state.socialMediaUrls[id] || ''}
|
||||
value={state.socialMediaUrls[id] || ""}
|
||||
onChange={(e) => updateUrl(id, e.target.value)}
|
||||
className="w-full p-6 pl-40 bg-white border border-slate-100 rounded-[2rem] focus:outline-none focus:border-slate-900 transition-all duration-500 text-lg focus:shadow-xl"
|
||||
/>
|
||||
@@ -136,10 +167,12 @@ export function PresenceStep({ state, updateState, toggleItem }: PresenceStepPro
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
|
||||
{state.socialMedia.length === 0 && (
|
||||
<div className="p-12 border-2 border-dashed border-slate-100 rounded-[3rem] text-center">
|
||||
<p className="text-slate-400 font-medium">Wählen Sie oben Ihre Kanäle aus, um die Links hinzuzufügen.</p>
|
||||
<p className="text-slate-400 font-medium">
|
||||
Wählen Sie oben Ihre Kanäle aus, um die Links hinzuzufügen.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
interface TimelineStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
}
|
||||
|
||||
export function TimelineStep({ state, updateState }: TimelineStepProps) {
|
||||
const isMissingAssets = !state.assets.includes('logo') || !state.assets.includes('content_concept');
|
||||
const isMissingPages = state.selectedPages.length === 0 && state.otherPages.length === 0 && state.otherPagesCount === 0;
|
||||
const isMissingAssets =
|
||||
!state.assets.includes("logo") || !state.assets.includes("content_concept");
|
||||
const isMissingPages =
|
||||
state.selectedPages.length === 0 &&
|
||||
state.otherPages.length === 0 &&
|
||||
state.otherPagesCount === 0;
|
||||
|
||||
const toggleDontKnow = (id: string) => {
|
||||
const current = state.dontKnows || [];
|
||||
if (current.includes(id)) {
|
||||
updateState({ dontKnows: current.filter(i => i !== id) });
|
||||
updateState({ dontKnows: current.filter((i) => i !== id) });
|
||||
} else {
|
||||
updateState({ dontKnows: [...current, id] });
|
||||
}
|
||||
@@ -29,9 +33,11 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
|
||||
<h4 className="text-2xl font-bold text-slate-900">Zeitplan</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleDontKnow('timeline')}
|
||||
onClick={() => toggleDontKnow("timeline")}
|
||||
className={`px-4 py-2 rounded-full text-sm font-bold transition-all whitespace-nowrap shrink-0 ${
|
||||
state.dontKnows?.includes('timeline') ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
||||
state.dontKnows?.includes("timeline")
|
||||
? "bg-slate-900 text-white"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200"
|
||||
}`}
|
||||
>
|
||||
Ich weiß es nicht
|
||||
@@ -39,30 +45,58 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ id: 'asap', label: 'So schnell wie möglich', desc: 'Priorisierter Start gewünscht.' },
|
||||
{ id: '2-3-months', label: 'In 2-3 Monaten', desc: 'Normaler Projektvorlauf.' },
|
||||
{ id: '3-6-months', label: 'In 3-6 Monaten', desc: 'Langfristige Planung.' },
|
||||
{ id: 'flexible', label: 'Flexibel', desc: 'Kein fester Termindruck.' },
|
||||
].map(opt => (
|
||||
{
|
||||
id: "asap",
|
||||
label: "So schnell wie möglich",
|
||||
desc: "Priorisierter Start gewünscht.",
|
||||
},
|
||||
{
|
||||
id: "2-3-months",
|
||||
label: "In 2-3 Monaten",
|
||||
desc: "Normaler Projektvorlauf.",
|
||||
},
|
||||
{
|
||||
id: "3-6-months",
|
||||
label: "In 3-6 Monaten",
|
||||
desc: "Langfristige Planung.",
|
||||
},
|
||||
{
|
||||
id: "flexible",
|
||||
label: "Flexibel",
|
||||
desc: "Kein fester Termindruck.",
|
||||
},
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
onClick={() => updateState({ deadline: opt.id })}
|
||||
className={`p-10 rounded-[2.5rem] border-2 text-left transition-all focus:outline-none overflow-hidden relative ${
|
||||
state.deadline === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
|
||||
state.deadline === opt.id
|
||||
? "border-slate-900 bg-slate-900 text-white"
|
||||
: "border-slate-100 bg-white hover:border-slate-200"
|
||||
}`}
|
||||
>
|
||||
<h4 className={`text-2xl font-bold mb-2 ${state.deadline === opt.id ? 'text-white' : 'text-slate-900'}`}>{opt.label}</h4>
|
||||
<p className={`text-lg ${state.deadline === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
|
||||
<h4
|
||||
className={`text-2xl font-bold mb-2 ${state.deadline === opt.id ? "text-white" : "text-slate-900"}`}
|
||||
>
|
||||
{opt.label}
|
||||
</h4>
|
||||
<p
|
||||
className={`text-lg ${state.deadline === opt.id ? "text-slate-200" : "text-slate-500"}`}
|
||||
>
|
||||
{opt.desc}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{state.deadline === 'asap' && (
|
||||
{state.deadline === "asap" && (
|
||||
<div className="p-8 bg-slate-50 rounded-[2rem] border border-slate-100 flex gap-6 items-start">
|
||||
<AlertCircle className="text-slate-900 shrink-0 mt-1" size={28} />
|
||||
<p className="text-base text-slate-600 leading-relaxed">
|
||||
<strong>Hinweis:</strong> Bei sehr kurzfristigen Deadlines kann ein Express-Zuschlag anfallen, um die Kapazitäten entsprechend zu priorisieren.
|
||||
<strong>Hinweis:</strong> Bei sehr kurzfristigen Deadlines kann ein
|
||||
Express-Zuschlag anfallen, um die Kapazitäten entsprechend zu
|
||||
priorisieren.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -73,9 +107,15 @@ export function TimelineStep({ state, updateState }: TimelineStepProps) {
|
||||
<AlertCircle size={24} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-amber-900 text-xl font-bold">Mögliche Verzögerungen</p>
|
||||
<p className="text-amber-900 text-xl font-bold">
|
||||
Mögliche Verzögerungen
|
||||
</p>
|
||||
<p className="text-amber-800 text-base leading-relaxed">
|
||||
Für einen reibungslosen Projektstart benötigen wir noch einige Details (z.B. {isMissingAssets ? 'Logo/Inhaltskonzept' : ''} {isMissingAssets && isMissingPages ? 'und' : ''} {isMissingPages ? 'Seitenstruktur' : ''}). Ohne diese kann sich der Beginn verzögern.
|
||||
Für einen reibungslosen Projektstart benötigen wir noch einige
|
||||
Details (z.B. {isMissingAssets ? "Logo/Inhaltskonzept" : ""}{" "}
|
||||
{isMissingAssets && isMissingPages ? "und" : ""}{" "}
|
||||
{isMissingPages ? "Seitenstruktur" : ""}). Ohne diese kann sich
|
||||
der Beginn verzögern.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState, ProjectType } from '../types';
|
||||
import { ConceptWebsite, ConceptSystem } from '../../Landing/ConceptIllustrations';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Reveal } from '../../Reveal';
|
||||
import * as React from "react";
|
||||
import { FormState, ProjectType } from "../types";
|
||||
import {
|
||||
ConceptWebsite,
|
||||
ConceptSystem,
|
||||
} from "../../Landing/ConceptIllustrations";
|
||||
import { motion } from "framer-motion";
|
||||
import { Reveal } from "../../Reveal";
|
||||
|
||||
interface TypeStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
}
|
||||
|
||||
export function TypeStep({ state, updateState }: TypeStepProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{[
|
||||
{ id: 'website', label: 'Website', desc: 'Klassische Webpräsenz, Portfolio oder Blog.', illustration: <ConceptWebsite className="w-20 h-20 mb-6" /> },
|
||||
{ id: 'web-app', label: 'Web App', desc: 'Internes Tool, Dashboard oder Prozess-Logik.', illustration: <ConceptSystem className="w-20 h-20 mb-6" /> },
|
||||
{
|
||||
id: "website",
|
||||
label: "Website",
|
||||
desc: "Klassische Webpräsenz, Portfolio oder Blog.",
|
||||
illustration: <ConceptWebsite className="w-20 h-20 mb-6" />,
|
||||
},
|
||||
{
|
||||
id: "web-app",
|
||||
label: "Web App",
|
||||
desc: "Internes Tool, Dashboard oder Prozess-Logik.",
|
||||
illustration: <ConceptSystem className="w-20 h-20 mb-6" />,
|
||||
},
|
||||
].map((type, index) => (
|
||||
<Reveal key={type.id} width="100%" delay={index * 0.1}>
|
||||
<motion.button
|
||||
@@ -25,15 +38,34 @@ export function TypeStep({ state, updateState }: TypeStepProps) {
|
||||
whileTap={{ scale: 0.98 }}
|
||||
type="button"
|
||||
onClick={() => updateState({ projectType: type.id as ProjectType })}
|
||||
className={`w-full p-12 rounded-[4rem] border-2 text-left transition-all duration-700 focus:outline-none overflow-hidden relative group ${state.projectType === type.id ? 'border-slate-900 bg-slate-900 text-white shadow-2xl shadow-slate-400' : 'border-slate-100 bg-white hover:border-slate-300 hover:shadow-xl'
|
||||
}`}
|
||||
className={`w-full p-12 rounded-[4rem] border-2 text-left transition-all duration-700 focus:outline-none overflow-hidden relative group ${
|
||||
state.projectType === type.id
|
||||
? "border-slate-900 bg-slate-900 text-white shadow-2xl shadow-slate-400"
|
||||
: "border-slate-100 bg-white hover:border-slate-300 hover:shadow-xl"
|
||||
}`}
|
||||
>
|
||||
<div className={`transition-all duration-700 group-hover:scale-110 group-hover:rotate-3 ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.illustration}</div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<h4 className={`text-5xl font-bold tracking-tight ${state.projectType === type.id ? 'text-white' : 'text-slate-900'}`}>{type.label}</h4>
|
||||
<span className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest ${state.projectType === type.id ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-500'}`}>Grundlage</span>
|
||||
<div
|
||||
className={`transition-all duration-700 group-hover:scale-110 group-hover:rotate-3 ${state.projectType === type.id ? "text-white" : "text-slate-900"}`}
|
||||
>
|
||||
{type.illustration}
|
||||
</div>
|
||||
<p className={`text-2xl leading-relaxed ${state.projectType === type.id ? 'text-slate-200' : 'text-slate-500'}`}>{type.desc}</p>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<h4
|
||||
className={`text-5xl font-bold tracking-tight ${state.projectType === type.id ? "text-white" : "text-slate-900"}`}
|
||||
>
|
||||
{type.label}
|
||||
</h4>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest ${state.projectType === type.id ? "bg-white/20 text-white" : "bg-slate-100 text-slate-500"}`}
|
||||
>
|
||||
Grundlage
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className={`text-2xl leading-relaxed ${state.projectType === type.id ? "text-slate-200" : "text-slate-500"}`}
|
||||
>
|
||||
{type.desc}
|
||||
</p>
|
||||
|
||||
{state.projectType === type.id && (
|
||||
<motion.div
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { FormState } from '../types';
|
||||
import { Users, Shield, Monitor, Smartphone, Globe, Lock } from 'lucide-react';
|
||||
import * as React from "react";
|
||||
import { FormState } from "../types";
|
||||
import { Users, Shield, Monitor, Smartphone, Globe, Lock } from "lucide-react";
|
||||
|
||||
interface WebAppStepProps {
|
||||
state: FormState;
|
||||
updateState: (updates: Partial<FormState>) => void;
|
||||
updateState: (_updates: Partial<FormState>) => void;
|
||||
}
|
||||
|
||||
export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
||||
const toggleUserRole = (role: string) => {
|
||||
const current = state.userRoles || [];
|
||||
const next = current.includes(role)
|
||||
? current.filter(r => r !== role)
|
||||
const next = current.includes(role)
|
||||
? current.filter((r) => r !== role)
|
||||
: [...current, role];
|
||||
updateState({ userRoles: next });
|
||||
};
|
||||
@@ -26,23 +26,39 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
||||
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
|
||||
<Users size={24} className="text-black" /> Zielgruppe
|
||||
</h4>
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">Fokus</span>
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold uppercase tracking-wider rounded">
|
||||
Fokus
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ id: 'internal', label: 'Internes Tool', desc: 'Für Mitarbeiter & Prozesse.' },
|
||||
{ id: 'external', label: 'Kunden-Portal', desc: 'Für Ihre Endnutzer (B2B/B2C).' },
|
||||
].map(opt => (
|
||||
{
|
||||
id: "internal",
|
||||
label: "Internes Tool",
|
||||
desc: "Für Mitarbeiter & Prozesse.",
|
||||
},
|
||||
{
|
||||
id: "external",
|
||||
label: "Kunden-Portal",
|
||||
desc: "Für Ihre Endnutzer (B2B/B2C).",
|
||||
},
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
onClick={() => updateState({ targetAudience: opt.id })}
|
||||
className={`p-8 rounded-[2rem] border-2 text-left transition-all ${
|
||||
state.targetAudience === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
|
||||
state.targetAudience === opt.id
|
||||
? "border-slate-900 bg-slate-900 text-white"
|
||||
: "border-slate-100 bg-white hover:border-slate-200"
|
||||
}`}
|
||||
>
|
||||
<p className="text-xl font-bold">{opt.label}</p>
|
||||
<p className={`text-base mt-2 ${state.targetAudience === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
|
||||
<p
|
||||
className={`text-base mt-2 ${state.targetAudience === opt.id ? "text-slate-200" : "text-slate-500"}`}
|
||||
>
|
||||
{opt.desc}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -53,13 +69,21 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
||||
<h4 className="text-2xl font-bold text-slate-900">Benutzerrollen</h4>
|
||||
<p className="text-lg text-slate-500">Wer wird das System nutzen?</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{['Administratoren', 'Manager', 'Standard-Nutzer', 'Gäste', 'Read-Only'].map(role => (
|
||||
{[
|
||||
"Administratoren",
|
||||
"Manager",
|
||||
"Standard-Nutzer",
|
||||
"Gäste",
|
||||
"Read-Only",
|
||||
].map((role) => (
|
||||
<button
|
||||
key={role}
|
||||
type="button"
|
||||
onClick={() => toggleUserRole(role)}
|
||||
className={`px-8 py-4 rounded-full border-2 font-bold text-base transition-all ${
|
||||
(state.userRoles || []).includes(role) ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
|
||||
(state.userRoles || []).includes(role)
|
||||
? "border-slate-900 bg-slate-900 text-white"
|
||||
: "border-slate-100 bg-white hover:border-slate-200"
|
||||
}`}
|
||||
>
|
||||
{role}
|
||||
@@ -75,19 +99,37 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ id: 'desktop', label: 'Desktop First', icon: <Monitor size={24} /> },
|
||||
{ id: 'mobile', label: 'Mobile First', icon: <Smartphone size={24} /> },
|
||||
{ id: 'pwa', label: 'PWA (Installierbar)', icon: <Globe size={24} /> },
|
||||
].map(opt => (
|
||||
{
|
||||
id: "desktop",
|
||||
label: "Desktop First",
|
||||
icon: <Monitor size={24} />,
|
||||
},
|
||||
{
|
||||
id: "mobile",
|
||||
label: "Mobile First",
|
||||
icon: <Smartphone size={24} />,
|
||||
},
|
||||
{
|
||||
id: "pwa",
|
||||
label: "PWA (Installierbar)",
|
||||
icon: <Globe size={24} />,
|
||||
},
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
onClick={() => updateState({ platformType: opt.id })}
|
||||
className={`p-8 rounded-[2rem] border-2 flex flex-col items-center gap-4 transition-all ${
|
||||
state.platformType === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
|
||||
state.platformType === opt.id
|
||||
? "border-slate-900 bg-slate-900 text-white"
|
||||
: "border-slate-100 bg-white hover:border-slate-200"
|
||||
}`}
|
||||
>
|
||||
<div className={state.platformType === opt.id ? 'text-white' : 'text-black'}>
|
||||
<div
|
||||
className={
|
||||
state.platformType === opt.id ? "text-white" : "text-black"
|
||||
}
|
||||
>
|
||||
{opt.icon}
|
||||
</div>
|
||||
<span className="font-bold text-lg">{opt.label}</span>
|
||||
@@ -103,19 +145,33 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ id: 'standard', label: 'Standard', desc: 'Normale Nutzerdaten & Profile.' },
|
||||
{ id: 'high', label: 'Sensibel', desc: 'Finanzdaten, Gesundheitsdaten oder DSGVO-kritisch.' },
|
||||
].map(opt => (
|
||||
{
|
||||
id: "standard",
|
||||
label: "Standard",
|
||||
desc: "Normale Nutzerdaten & Profile.",
|
||||
},
|
||||
{
|
||||
id: "high",
|
||||
label: "Sensibel",
|
||||
desc: "Finanzdaten, Gesundheitsdaten oder DSGVO-kritisch.",
|
||||
},
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
type="button"
|
||||
onClick={() => updateState({ dataSensitivity: opt.id })}
|
||||
className={`p-8 rounded-[2rem] border-2 text-left transition-all ${
|
||||
state.dataSensitivity === opt.id ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-100 bg-white hover:border-slate-200'
|
||||
state.dataSensitivity === opt.id
|
||||
? "border-slate-900 bg-slate-900 text-white"
|
||||
: "border-slate-100 bg-white hover:border-slate-200"
|
||||
}`}
|
||||
>
|
||||
<p className="text-xl font-bold">{opt.label}</p>
|
||||
<p className={`text-base mt-2 ${state.dataSensitivity === opt.id ? 'text-slate-200' : 'text-slate-500'}`}>{opt.desc}</p>
|
||||
<p
|
||||
className={`text-base mt-2 ${state.dataSensitivity === opt.id ? "text-slate-200" : "text-slate-500"}`}
|
||||
>
|
||||
{opt.desc}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -126,9 +182,17 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
||||
<h4 className="text-2xl font-bold text-slate-900 flex items-center gap-3">
|
||||
<Lock size={24} className="text-black" /> Authentifizierung
|
||||
</h4>
|
||||
<p className="text-lg text-slate-500">Wie sollen sich Nutzer anmelden?</p>
|
||||
<p className="text-lg text-slate-500">
|
||||
Wie sollen sich Nutzer anmelden?
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{['E-Mail / Passwort', 'Social Login', 'SSO / SAML', '2FA / MFA', 'Magic Links'].map(method => (
|
||||
{[
|
||||
"E-Mail / Passwort",
|
||||
"Social Login",
|
||||
"SSO / SAML",
|
||||
"2FA / MFA",
|
||||
"Magic Links",
|
||||
].map((method) => (
|
||||
<div
|
||||
key={method}
|
||||
className="px-8 py-4 rounded-full border-2 border-white bg-white font-bold text-base text-slate-400"
|
||||
@@ -137,7 +201,9 @@ export function WebAppStep({ state, updateState }: WebAppStepProps) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 italic">Details zur Authentifizierung besprechen wir im Erstgespräch.</p>
|
||||
<p className="text-xs text-slate-400 italic">
|
||||
Details zur Authentifizierung besprechen wir im Erstgespräch.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user