Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
351 lines
13 KiB
TypeScript
351 lines
13 KiB
TypeScript
import { useState } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
Building2,
|
|
User,
|
|
Monitor,
|
|
Headphones,
|
|
ArrowRight,
|
|
Shield,
|
|
Clock,
|
|
Sparkles,
|
|
} from 'lucide-react';
|
|
import type {
|
|
ClientType,
|
|
CompanyInfo,
|
|
ContactInfo,
|
|
Industry,
|
|
} from '@/types/quote';
|
|
|
|
export interface StepWelcomeProps {
|
|
clientType: ClientType;
|
|
companyInfo: CompanyInfo;
|
|
contactInfo: ContactInfo;
|
|
onSetClientType: (type: ClientType) => void;
|
|
onUpdateCompany: (data: Partial<CompanyInfo>) => void;
|
|
onUpdateContact: (data: Partial<ContactInfo>) => void;
|
|
onSetEndpointCount: (count: number) => void;
|
|
onSetIndustry: (industry: Industry | '') => void;
|
|
}
|
|
|
|
const industries: Industry[] = [
|
|
'Healthcare',
|
|
'Legal',
|
|
'Finance',
|
|
'Manufacturing',
|
|
'Retail',
|
|
'Professional Services',
|
|
'Other',
|
|
];
|
|
|
|
const journeySteps = [
|
|
{
|
|
icon: Sparkles,
|
|
title: 'Tell us about yourself',
|
|
desc: 'Basic info so we can personalize your experience',
|
|
},
|
|
{
|
|
icon: Monitor,
|
|
title: 'Choose your services',
|
|
desc: 'Toggle the IT services that interest you',
|
|
},
|
|
{
|
|
icon: Headphones,
|
|
title: 'Configure each service',
|
|
desc: "We'll walk through your selections one by one",
|
|
},
|
|
{
|
|
icon: ArrowRight,
|
|
title: 'Review & submit',
|
|
desc: 'Get your custom quote delivered instantly',
|
|
},
|
|
];
|
|
|
|
const stagger = {
|
|
hidden: {},
|
|
visible: { transition: { staggerChildren: 0.06 } },
|
|
};
|
|
|
|
const fadeUp = {
|
|
hidden: { opacity: 0, y: 12 },
|
|
visible: { opacity: 1, y: 0, transition: { duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] as const } },
|
|
};
|
|
|
|
export function StepWelcome({
|
|
clientType,
|
|
companyInfo,
|
|
contactInfo,
|
|
onSetClientType,
|
|
onUpdateCompany,
|
|
onUpdateContact,
|
|
onSetEndpointCount,
|
|
onSetIndustry,
|
|
}: StepWelcomeProps) {
|
|
const [endpointInput, setEndpointInput] = useState(String(companyInfo.endpointCount));
|
|
|
|
const handleEndpointChange = (val: string) => {
|
|
setEndpointInput(val);
|
|
const num = parseInt(val, 10);
|
|
if (!isNaN(num) && num >= 1) {
|
|
onSetEndpointCount(num);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
variants={stagger}
|
|
initial="hidden"
|
|
animate="visible"
|
|
className="space-y-10"
|
|
>
|
|
{/* Hero welcome */}
|
|
<motion.div variants={fadeUp} className="text-center max-w-2xl mx-auto">
|
|
<h2
|
|
className="text-3xl sm:text-4xl font-bold text-[#333d49] mb-3 leading-tight"
|
|
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
>
|
|
Let’s Build Your
|
|
<span className="text-[#fe7400]"> IT Solution</span>
|
|
</h2>
|
|
<p className="text-gray-400 text-base sm:text-lg leading-relaxed max-w-lg mx-auto">
|
|
In just a few minutes, we’ll create a custom technology package
|
|
tailored to your needs. No commitment required.
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* What to expect */}
|
|
<motion.div variants={fadeUp}>
|
|
<div className="bg-gradient-to-br from-[#f8f9fb] to-[#f1f3f5] rounded-2xl p-5 sm:p-6">
|
|
<p
|
|
className="text-[11px] uppercase tracking-widest font-semibold text-gray-400 mb-4"
|
|
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
>
|
|
What to expect
|
|
</p>
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
|
|
{journeySteps.map((step, i) => (
|
|
<div key={i} className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className="flex items-center justify-center w-6 h-6 rounded-full bg-[#fe7400]/10 text-[#fe7400] text-[10px] font-bold flex-shrink-0"
|
|
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
>
|
|
{i + 1}
|
|
</span>
|
|
<step.icon className="w-3.5 h-3.5 text-gray-400" />
|
|
</div>
|
|
<p
|
|
className="text-sm font-semibold text-[#333d49] leading-snug"
|
|
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
>
|
|
{step.title}
|
|
</p>
|
|
<p className="text-xs text-gray-400 leading-relaxed">{step.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Client type toggle */}
|
|
<motion.div variants={fadeUp}>
|
|
<label
|
|
className="block text-sm font-semibold text-[#333d49] mb-3"
|
|
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
>
|
|
I’m looking for IT services for…
|
|
</label>
|
|
<div className="inline-flex bg-[#f1f3f5] rounded-xl p-1 gap-1">
|
|
{(['company', 'individual'] as const).map((type) => (
|
|
<button
|
|
key={type}
|
|
type="button"
|
|
onClick={() => onSetClientType(type)}
|
|
className={`
|
|
relative flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-semibold transition-all duration-200
|
|
${clientType === type
|
|
? 'bg-white text-[#333d49] shadow-sm'
|
|
: 'text-gray-400 hover:text-gray-500'
|
|
}
|
|
`}
|
|
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
>
|
|
{type === 'company' ? (
|
|
<Building2 className="w-4 h-4" />
|
|
) : (
|
|
<User className="w-4 h-4" />
|
|
)}
|
|
{type === 'company' ? 'A Business' : 'Myself'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Contact & company info form */}
|
|
<motion.div variants={fadeUp} className="space-y-6">
|
|
{/* Contact info */}
|
|
<div>
|
|
<p
|
|
className="text-[11px] uppercase tracking-widest font-semibold text-gray-400 mb-4"
|
|
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
>
|
|
Your contact information
|
|
</p>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[#333d49] mb-1.5">
|
|
Your Name <span className="text-[#fe7400]">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={contactInfo.name}
|
|
onChange={(e) => onUpdateContact({ name: e.target.value })}
|
|
placeholder="First and last name"
|
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 bg-white text-[#333d49] text-sm
|
|
placeholder:text-gray-300 focus:border-[#fe7400] focus:ring-2 focus:ring-[#fe7400]/10
|
|
transition-all duration-200 outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-[#333d49] mb-1.5">
|
|
Email <span className="text-[#fe7400]">*</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={contactInfo.email}
|
|
onChange={(e) => onUpdateContact({ email: e.target.value })}
|
|
placeholder="you@company.com"
|
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 bg-white text-[#333d49] text-sm
|
|
placeholder:text-gray-300 focus:border-[#fe7400] focus:ring-2 focus:ring-[#fe7400]/10
|
|
transition-all duration-200 outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-[#333d49] mb-1.5">
|
|
Phone <span className="text-gray-300 text-xs font-normal">(recommended)</span>
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={contactInfo.phone}
|
|
onChange={(e) => onUpdateContact({ phone: e.target.value })}
|
|
placeholder="(480) 555-0100"
|
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 bg-white text-[#333d49] text-sm
|
|
placeholder:text-gray-300 focus:border-[#fe7400] focus:ring-2 focus:ring-[#fe7400]/10
|
|
transition-all duration-200 outline-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Company name — only for business clients */}
|
|
<AnimatePresence mode="wait">
|
|
{clientType === 'company' && (
|
|
<motion.div
|
|
key="company-name"
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<label className="block text-sm font-medium text-[#333d49] mb-1.5">
|
|
Company Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={companyInfo.name}
|
|
onChange={(e) => onUpdateCompany({ name: e.target.value })}
|
|
placeholder="Acme Corp"
|
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 bg-white text-[#333d49] text-sm
|
|
placeholder:text-gray-300 focus:border-[#fe7400] focus:ring-2 focus:ring-[#fe7400]/10
|
|
transition-all duration-200 outline-none"
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Business details */}
|
|
<div>
|
|
<p
|
|
className="text-[11px] uppercase tracking-widest font-semibold text-gray-400 mb-4"
|
|
style={{ fontFamily: "'Plus Jakarta Sans', sans-serif" }}
|
|
>
|
|
About your environment
|
|
</p>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-[#333d49] mb-1.5">
|
|
Devices / Endpoints <span className="text-[#fe7400]">*</span>
|
|
</label>
|
|
<div className="flex items-center gap-3">
|
|
<input
|
|
type="number"
|
|
min={1}
|
|
value={endpointInput}
|
|
onChange={(e) => handleEndpointChange(e.target.value)}
|
|
onBlur={() => {
|
|
const num = parseInt(endpointInput, 10);
|
|
if (isNaN(num) || num < 1) {
|
|
setEndpointInput('1');
|
|
onSetEndpointCount(1);
|
|
}
|
|
}}
|
|
className="w-24 px-4 py-2.5 rounded-xl border border-gray-200 bg-white text-[#333d49] text-sm text-center
|
|
focus:border-[#fe7400] focus:ring-2 focus:ring-[#fe7400]/10
|
|
transition-all duration-200 outline-none"
|
|
/>
|
|
<span className="text-sm text-gray-400">
|
|
computers, laptops, & servers
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatePresence mode="wait">
|
|
{clientType === 'company' && (
|
|
<motion.div
|
|
key="industry"
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: 'auto' }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<label className="block text-sm font-medium text-[#333d49] mb-1.5">
|
|
Industry
|
|
</label>
|
|
<select
|
|
value={companyInfo.industry}
|
|
onChange={(e) => onSetIndustry(e.target.value as Industry | '')}
|
|
className="w-full px-4 py-2.5 rounded-xl border border-gray-200 bg-white text-[#333d49] text-sm
|
|
focus:border-[#fe7400] focus:ring-2 focus:ring-[#fe7400]/10
|
|
transition-all duration-200 outline-none appearance-none cursor-pointer"
|
|
>
|
|
<option value="">Select an industry</option>
|
|
{industries.map((ind) => (
|
|
<option key={ind} value={ind}>
|
|
{ind}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Trust signals */}
|
|
<motion.div variants={fadeUp} className="flex flex-wrap items-center justify-center gap-6 pt-2">
|
|
{[
|
|
{ icon: Shield, text: 'No obligation' },
|
|
{ icon: Clock, text: 'Takes ~2 minutes' },
|
|
{ icon: Sparkles, text: 'Instant quote' },
|
|
].map(({ icon: Icon, text }) => (
|
|
<span key={text} className="flex items-center gap-1.5 text-xs text-gray-400">
|
|
<Icon className="w-3.5 h-3.5" />
|
|
{text}
|
|
</span>
|
|
))}
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
}
|