cre-sync/components/Quiz.tsx
BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

377 lines
12 KiB
TypeScript

import React, { useState } from 'react';
import { ClayCard } from './ClayCard';
import { Button } from './Button';
import { ArrowRight, ArrowLeft, TrendingUp, Users, BookOpen } from 'lucide-react';
export interface QuizResult {
yearsInBusiness: string;
dealsLast12Months: number;
leadsPerMonth: number;
hoursProspecting: number;
performanceLevel: 'below' | 'at' | 'above';
recommendedAction: 'coaching' | 'team' | 'none';
}
interface QuizProps {
onComplete: (result: QuizResult) => void;
calendlyCoachingLink?: string;
calendlyTeamLink?: string;
}
interface Question {
id: keyof Pick<QuizResult, 'yearsInBusiness' | 'dealsLast12Months' | 'leadsPerMonth' | 'hoursProspecting'>;
question: string;
type: 'select' | 'number';
options?: { value: string; label: string }[];
placeholder?: string;
}
const questions: Question[] = [
{
id: 'yearsInBusiness',
question: 'How long have you been in real estate?',
type: 'select',
options: [
{ value: '0-1', label: 'Less than 1 year' },
{ value: '1-3', label: '1-3 years' },
{ value: '3-5', label: '3-5 years' },
{ value: '5-10', label: '5-10 years' },
{ value: '10+', label: '10+ years' },
],
},
{
id: 'dealsLast12Months',
question: 'How many deals did you close in the last 12 months?',
type: 'number',
placeholder: 'Enter number of deals',
},
{
id: 'leadsPerMonth',
question: 'How many leads do you generate per month?',
type: 'number',
placeholder: 'Enter number of leads',
},
{
id: 'hoursProspecting',
question: 'How many hours per week do you spend prospecting?',
type: 'number',
placeholder: 'Enter hours per week',
},
];
// Peer benchmarks based on years of experience
const getPeerBenchmarks = (years: string) => {
const benchmarks: Record<string, { deals: number; leads: number; hours: number }> = {
'0-1': { deals: 3, leads: 10, hours: 15 },
'1-3': { deals: 8, leads: 20, hours: 12 },
'3-5': { deals: 12, leads: 30, hours: 10 },
'5-10': { deals: 18, leads: 40, hours: 10 },
'10+': { deals: 24, leads: 50, hours: 8 },
};
return benchmarks[years] || benchmarks['1-3'];
};
const calculatePerformanceLevel = (
answers: Partial<QuizResult>
): { level: 'below' | 'at' | 'above'; action: 'coaching' | 'team' | 'none' } => {
const benchmarks = getPeerBenchmarks(answers.yearsInBusiness || '1-3');
let score = 0;
// Compare deals (weight: 40%)
const dealsRatio = (answers.dealsLast12Months || 0) / benchmarks.deals;
if (dealsRatio >= 1.2) score += 40;
else if (dealsRatio >= 0.8) score += 25;
else score += 10;
// Compare leads (weight: 35%)
const leadsRatio = (answers.leadsPerMonth || 0) / benchmarks.leads;
if (leadsRatio >= 1.2) score += 35;
else if (leadsRatio >= 0.8) score += 22;
else score += 8;
// Compare prospecting hours (weight: 25%)
const hoursRatio = (answers.hoursProspecting || 0) / benchmarks.hours;
if (hoursRatio >= 1.2) score += 25;
else if (hoursRatio >= 0.8) score += 16;
else score += 6;
// Determine performance level
let level: 'below' | 'at' | 'above';
let action: 'coaching' | 'team' | 'none';
if (score >= 80) {
level = 'above';
action = 'none';
} else if (score >= 55) {
level = 'at';
action = 'coaching';
} else {
level = 'below';
action = 'team';
}
return { level, action };
};
export const Quiz: React.FC<QuizProps> = ({
onComplete,
calendlyCoachingLink = 'https://calendly.com/coaching',
calendlyTeamLink = 'https://calendly.com/team',
}) => {
const [currentStep, setCurrentStep] = useState(0);
const [answers, setAnswers] = useState<Partial<QuizResult>>({});
const [showResults, setShowResults] = useState(false);
const [result, setResult] = useState<QuizResult | null>(null);
const currentQuestion = questions[currentStep];
const isLastQuestion = currentStep === questions.length - 1;
const progress = ((currentStep + 1) / questions.length) * 100;
const handleAnswer = (value: string | number) => {
setAnswers((prev) => ({
...prev,
[currentQuestion.id]: value,
}));
};
const getCurrentAnswer = () => {
return answers[currentQuestion.id];
};
const canProceed = () => {
const answer = getCurrentAnswer();
return answer !== undefined && answer !== '';
};
const handleNext = () => {
if (!canProceed()) return;
if (isLastQuestion) {
const { level, action } = calculatePerformanceLevel(answers);
const finalResult: QuizResult = {
yearsInBusiness: answers.yearsInBusiness || '',
dealsLast12Months: Number(answers.dealsLast12Months) || 0,
leadsPerMonth: Number(answers.leadsPerMonth) || 0,
hoursProspecting: Number(answers.hoursProspecting) || 0,
performanceLevel: level,
recommendedAction: action,
};
setResult(finalResult);
setShowResults(true);
onComplete(finalResult);
} else {
setCurrentStep((prev) => prev + 1);
}
};
const handleBack = () => {
if (currentStep > 0) {
setCurrentStep((prev) => prev - 1);
}
};
const handleCalendlyClick = (link: string) => {
window.open(link, '_blank', 'noopener,noreferrer');
};
if (showResults && result) {
return (
<div className="max-w-xl mx-auto py-10 px-4 animate-fadeIn">
<ClayCard>
<div className="text-center">
<div
className={`w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 shadow-inner ${
result.performanceLevel === 'above'
? 'bg-green-100 text-green-600'
: result.performanceLevel === 'at'
? 'bg-yellow-100 text-yellow-600'
: 'bg-indigo-100 text-indigo-600'
}`}
>
<TrendingUp size={40} />
</div>
<h2 className="text-2xl font-bold text-gray-800 mb-4">Your Results</h2>
{result.performanceLevel === 'above' && (
<div className="mb-6">
<p className="text-gray-600 leading-relaxed">
Great work! Based on your responses, you're performing above your peers.
Keep up the excellent momentum!
</p>
</div>
)}
{result.performanceLevel === 'at' && (
<div className="mb-6">
<p className="text-gray-600 leading-relaxed">
You're performing at peer level. With some additional coaching,
you could take your business to the next level.
</p>
</div>
)}
{result.performanceLevel === 'below' && (
<div className="mb-6">
<p className="text-gray-600 leading-relaxed">
Based on your peers, you may benefit from additional support.
Would you like to learn more about coaching or joining our team?
</p>
</div>
)}
{result.performanceLevel !== 'above' && (
<div className="space-y-3 mt-8">
<Button
fullWidth
variant="primary"
onClick={() => handleCalendlyClick(calendlyCoachingLink)}
className="gap-3"
>
<BookOpen size={20} />
Learn about coaching
</Button>
<Button
fullWidth
variant="secondary"
onClick={() => handleCalendlyClick(calendlyTeamLink)}
className="gap-3"
>
<Users size={20} />
Learn about joining the team
</Button>
</div>
)}
<div className="mt-8 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">
Your Responses
</h3>
<div className="grid grid-cols-2 gap-4 text-left">
<div className="bg-gray-50 rounded-xl p-3">
<p className="text-xs text-gray-500">Experience</p>
<p className="font-semibold text-gray-800">{result.yearsInBusiness} years</p>
</div>
<div className="bg-gray-50 rounded-xl p-3">
<p className="text-xs text-gray-500">Deals (12 mo)</p>
<p className="font-semibold text-gray-800">{result.dealsLast12Months}</p>
</div>
<div className="bg-gray-50 rounded-xl p-3">
<p className="text-xs text-gray-500">Leads/Month</p>
<p className="font-semibold text-gray-800">{result.leadsPerMonth}</p>
</div>
<div className="bg-gray-50 rounded-xl p-3">
<p className="text-xs text-gray-500">Prospecting Hrs/Wk</p>
<p className="font-semibold text-gray-800">{result.hoursProspecting}</p>
</div>
</div>
</div>
</div>
</ClayCard>
</div>
);
}
return (
<div className="max-w-xl mx-auto py-10 px-4">
{/* Progress Bar */}
<div className="mb-8">
<div className="flex justify-between text-sm text-gray-500 mb-2">
<span>Question {currentStep + 1} of {questions.length}</span>
<span>{Math.round(progress)}% complete</span>
</div>
<div className="h-2 bg-gray-200 rounded-full overflow-hidden shadow-inner">
<div
className="h-full bg-indigo-600 rounded-full transition-all duration-500 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
<ClayCard>
<div className="mb-6">
<h2 className="text-xl font-bold text-gray-800 mb-2">
{currentQuestion.question}
</h2>
</div>
<div className="space-y-4 mb-8">
{currentQuestion.type === 'select' && currentQuestion.options && (
<div className="space-y-3">
{currentQuestion.options.map((option) => (
<ClayCard
key={option.value}
onClick={() => handleAnswer(option.value)}
selected={getCurrentAnswer() === option.value}
className="!p-4 !rounded-xl"
>
<span className="text-gray-700 font-medium">{option.label}</span>
</ClayCard>
))}
</div>
)}
{currentQuestion.type === 'number' && (
<input
type="number"
min="0"
className="input-field"
placeholder={currentQuestion.placeholder}
value={getCurrentAnswer() ?? ''}
onChange={(e) => handleAnswer(e.target.value ? Number(e.target.value) : '')}
/>
)}
</div>
<div className="flex gap-3">
{currentStep > 0 && (
<Button variant="secondary" onClick={handleBack}>
<ArrowLeft size={18} />
Back
</Button>
)}
<Button
fullWidth
variant="primary"
onClick={handleNext}
disabled={!canProceed()}
>
{isLastQuestion ? 'See Results' : 'Continue'}
<ArrowRight size={18} />
</Button>
</div>
</ClayCard>
{/* Input field styles */}
<style>{`
.input-field {
width: 100%;
padding: 16px 20px;
border-radius: 16px;
background-color: #F9FAFB;
border: 1px solid transparent;
box-shadow: inset 2px 2px 5px rgba(0,0,0,0.05);
outline: none;
transition: all 0.2s;
font-size: 16px;
}
.input-field:focus {
background-color: #fff;
box-shadow: 0 0 0 2px #6366f1;
}
.input-field::placeholder {
color: #9CA3AF;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fadeIn {
animation: fadeIn 0.4s ease-out;
}
`}</style>
</div>
);
};