"use client"; import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, CornerDownLeftIcon, RotateCcwIcon, SparklesIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type KeyboardEvent, useEffect, useId, useLayoutEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Kbd } from "@/components/ui/kbd"; import { cn } from "@/lib/utils"; export type SurveyQuestion = | { id: string; kind: "single"; title: string; hint?: string; options: string[] } | { id: string; kind: "multi"; title: string; hint?: string; options: string[]; max?: number } | { id: string; kind: "text"; title: string; hint?: string; placeholder?: string; optional?: boolean }; export type SurveyAnswers = Record; export interface IntakeSurveyProps { questions: SurveyQuestion[]; name?: string; doneTitle?: string; doneText?: string; onComplete?: (answers: SurveyAnswers) => void; className?: string; } const ease = [0.23, 1, 0.32, 1] as const; const press = "transition-[box-shadow,scale,background-color] duration-150 active:scale-[0.97]"; const LETTERS = "ABCDEFGHI"; function useMeasuredHeight() { const ref = useRef(null); const [height, setHeight] = useState("auto"); useLayoutEffect(() => { const el = ref.current; if (!el) return; const ro = new ResizeObserver(() => setHeight(el.offsetHeight)); ro.observe(el); return () => ro.disconnect(); }, []); return [ref, height] as const; } export function IntakeSurvey({ questions, name, doneTitle, doneText = "We’ve set up your workspace around your answers. You can change any of this later in settings.", onComplete, className, }: IntakeSurveyProps) { const reduce = useReducedMotion(); const uid = useId(); const rootRef = useRef(null); const textRef = useRef(null); const [step, setStep] = useState(0); const [dir, setDir] = useState(1); const [answers, setAnswers] = useState({}); const [flash, setFlash] = useState(null); const [limitHit, setLimitHit] = useState(false); const advanceTimer = useRef>(undefined); const [contentRef, height] = useMeasuredHeight(); const answersRef = useRef(answers); answersRef.current = answers; const done = step >= questions.length; const q = questions[step]; const progress = done ? 1 : (step + 1) / (questions.length + 1); useEffect(() => () => clearTimeout(advanceTimer.current), []); useEffect(() => { if (!limitHit) return; const t = setTimeout(() => setLimitHit(false), 1600); return () => clearTimeout(t); }, [limitHit]); useEffect(() => { if (!rootRef.current?.contains(document.activeElement)) return; const t = setTimeout(() => { if (q?.kind === "text") textRef.current?.focus(); else rootRef.current?.querySelector("[data-survey-stage]")?.focus(); }, 230); return () => clearTimeout(t); }, [step, q?.kind]); const go = (next: number) => { clearTimeout(advanceTimer.current); setFlash(null); setDir(next > step ? 1 : -1); setStep(Math.max(0, Math.min(questions.length, next))); if (next >= questions.length) setTimeout(() => onComplete?.(answersRef.current), 0); }; const pick = (option: string) => { if (!q || done) return; if (q.kind === "single") { setAnswers((a) => ({ ...a, [q.id]: option })); setFlash(option); clearTimeout(advanceTimer.current); advanceTimer.current = setTimeout(() => go(step + 1), reduce ? 200 : 480); } else if (q.kind === "multi") { const current = (answers[q.id] as string[] | undefined) ?? []; if (current.includes(option)) { setAnswers((a) => ({ ...a, [q.id]: current.filter((o) => o !== option) })); } else if (q.max && current.length >= q.max) { setLimitHit(true); } else { setAnswers((a) => ({ ...a, [q.id]: [...current, option] })); } } }; const canContinue = !q || (q.kind === "single" && Boolean(answers[q.id])) || (q.kind === "multi" && ((answers[q.id] as string[] | undefined)?.length ?? 0) > 0) || (q.kind === "text" && (q.optional || Boolean((answers[q.id] as string | undefined)?.trim()))); const onKeyDown = (e: KeyboardEvent) => { if (done || !q || e.metaKey || e.ctrlKey || e.altKey) return; const inText = (e.target as HTMLElement).tagName === "TEXTAREA"; if (e.key === "Enter" && !e.shiftKey) { if (inText || (e.target as HTMLElement).tagName !== "BUTTON") { e.preventDefault(); if (canContinue) go(step + 1); } return; } if (inText || q.kind === "text") return; const i = LETTERS.indexOf(e.key.toUpperCase()); if (e.key.length === 1 && i >= 0 && i < q.options.length) { e.preventDefault(); pick(q.options[i]); } }; const variants = { enter: (d: number) => (reduce ? { opacity: 0 } : { opacity: 0, y: d * 16, filter: "blur(3px)" }), center: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: (d: number) => (reduce ? { opacity: 0 } : { opacity: 0, y: d * -16, filter: "blur(3px)" }), }; const first = name?.split(" ")[0]; return (
{done ? "Done" : `${step + 1}/${questions.length}`}
{!done && q ? (

{step + 1}

{q.title}

{q.hint && (

{q.hint}

)} {q.kind === "text" ? (