← back to ClawCoder

src/components/onboarding/question-screen.tsx

187 lines

'use client'

import { useState, useCallback, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import type { OnboardingQuestion } from '@/lib/onboarding/questions'
import { ProgressIndicator } from '@/components/ui/progress-indicator'

interface QuestionScreenProps {
  question: OnboardingQuestion
  totalSteps: number
  selectedValue: string | undefined
  onSelect: (value: string) => void
  onBack: () => void
  onNext: () => void
  isFirst: boolean
}

/** Detect prefers-reduced-motion at render time. */
function usePrefersReducedMotion() {
  const [reduced, setReduced] = useState(false)
  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
    setReduced(mq.matches)
    const handler = (e: MediaQueryListEvent) => setReduced(e.matches)
    mq.addEventListener('change', handler)
    return () => mq.removeEventListener('change', handler)
  }, [])
  return reduced
}

const fadeVariants = {
  initial: { opacity: 0, x: 40 },
  animate: { opacity: 1, x: 0 },
  exit: { opacity: 0, x: -40 },
}

const fadeFallback = {
  initial: { opacity: 0 },
  animate: { opacity: 1 },
  exit: { opacity: 0 },
}

export function QuestionScreen({
  question,
  totalSteps,
  selectedValue,
  onSelect,
  onBack,
  onNext,
  isFirst,
}: QuestionScreenProps) {
  const [showHint, setShowHint] = useState(false)
  const reducedMotion = usePrefersReducedMotion()
  const variants = reducedMotion ? fadeFallback : fadeVariants

  const handleSelect = useCallback(
    (value: string) => {
      onSelect(value)
      // Auto-advance after a short delay so the user sees the selection
      setTimeout(() => {
        onNext()
      }, 300)
    },
    [onSelect, onNext]
  )

  return (
    <div className="flex min-h-screen flex-col">
      {/* Main content, centered vertically with padding for bottom bar */}
      <div className="flex flex-1 flex-col items-center justify-center px-4 pb-28 pt-8">
        <AnimatePresence mode="wait">
          <motion.div
            key={question.id}
            variants={variants}
            initial="initial"
            animate="animate"
            exit="exit"
            transition={{ duration: 0.25, ease: 'easeInOut' }}
            className="flex w-full max-w-2xl flex-col items-center"
          >
            {/* Step label */}
            <p className="mb-3 text-sm font-medium uppercase tracking-wider text-slate-400">
              Step {question.step} of {totalSteps}
            </p>

            {/* Question */}
            <h1 className="mb-2 text-center text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl">
              {question.question}
            </h1>

            {/* Why we ask (collapsible) */}
            <button
              type="button"
              onClick={() => setShowHint((v) => !v)}
              className="mb-8 text-sm text-blue-500 transition-colors hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
              aria-expanded={showHint}
            >
              {showHint ? 'Hide' : 'Why do we ask?'}
            </button>

            <AnimatePresence>
              {showHint && (
                <motion.p
                  initial={{ opacity: 0, height: 0 }}
                  animate={{ opacity: 1, height: 'auto' }}
                  exit={{ opacity: 0, height: 0 }}
                  transition={{ duration: 0.2 }}
                  className="-mt-4 mb-8 max-w-md text-center text-sm text-slate-500"
                >
                  {question.whyWeAsk}
                </motion.p>
              )}
            </AnimatePresence>

            {/* Pill options grid */}
            <div
              className={`grid w-full gap-4 ${
                question.options.length <= 3
                  ? 'grid-cols-1 sm:grid-cols-3'
                  : question.options.length === 4
                    ? 'grid-cols-1 sm:grid-cols-2'
                    : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
              }`}
            >
              {question.options.map((opt) => (
                <button
                  key={opt.value}
                  type="button"
                  onClick={() => handleSelect(opt.value)}
                  className={`group flex min-h-[72px] flex-col items-center justify-center rounded-2xl border-2 px-6 py-4 text-center transition-all focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 motion-reduce:transform-none ${
                    selectedValue === opt.value
                      ? 'border-blue-500 bg-blue-50 shadow-md'
                      : 'border-slate-200 bg-white hover:border-blue-300 hover:shadow-sm'
                  }`}
                >
                  <span
                    className={`text-base font-semibold ${
                      selectedValue === opt.value
                        ? 'text-blue-700'
                        : 'text-slate-900 group-hover:text-blue-600'
                    }`}
                  >
                    {opt.label}
                  </span>
                  {opt.description && (
                    <span className="mt-1 text-sm text-slate-500">
                      {opt.description}
                    </span>
                  )}
                </button>
              ))}
            </div>
          </motion.div>
        </AnimatePresence>
      </div>

      {/* Fixed bottom bar - progress + navigation */}
      <div className="fixed bottom-0 left-0 right-0 z-50 border-t border-slate-200 bg-white/80 backdrop-blur">
        <div className="mx-auto max-w-2xl px-4 pb-4 pt-3">
          <ProgressIndicator current={question.step} total={totalSteps} />
          <div className="mt-3 flex justify-between">
            {!isFirst ? (
              <button
                type="button"
                onClick={onBack}
                className="min-h-[48px] rounded-xl border border-slate-300 px-6 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
              >
                Back
              </button>
            ) : (
              <div />
            )}
            <button
              type="button"
              onClick={onNext}
              disabled={!selectedValue}
              className="min-h-[48px] rounded-xl bg-blue-600 px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 disabled:cursor-not-allowed disabled:opacity-50"
            >
              {question.step === totalSteps ? 'Finish' : 'Next'}
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}