All files / src/features/onboarding WalkthroughShell.tsx

95% Statements 38/40
96% Branches 24/25
100% Functions 11/11
94.44% Lines 34/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 1052x                                               23x 23x 23x 23x 23x     23x 23x 23x     23x 9x 9x     23x 23x   23x 9x 9x 9x 6x 6x     3x     23x 3x       3x 3x 3x 2x   1x       23x 1x       1x     23x     4x           1x                                    
"use client";
 
import { useEffect, useRef, useState } from "react";
import { dismissWelcome } from "./dismissal";
import {
  buildTriageStanding,
  comparePairsFor,
  TRIAGE_ITEMS,
  type CompareOutcome,
  type CompareRoundResult,
  type TriageOutcome,
} from "./sampleData";
import { WelcomePurpose } from "./WelcomePurpose";
import { WelcomeTriage } from "./WelcomeTriage";
import { WelcomeCompare } from "./WelcomeCompare";
import { WelcomeStanding } from "./WelcomeStanding";
import { WelcomeHandoff } from "./WelcomeHandoff";
 
type Beat = "purpose" | "feelings" | "pairs" | "standing" | "handoff";
 
// The walkthrough is client-only state (decision 25): a beat cursor plus the typed
// outcomes the person taps. It teaches the real sort's two phases (spec §4):
// feelings for every piece, then pairs over Love + Like, then the ranking.
export function WalkthroughShell() {
  const [beat, setBeat] = useState<Beat>("purpose");
  const [triageIndex, setTriageIndex] = useState(0);
  const [triageOutcomes, setTriageOutcomes] = useState<Record<string, TriageOutcome>>({});
  const [compareIndex, setCompareIndex] = useState(0);
  const [compareResults, setCompareResults] = useState<CompareRoundResult[]>([]);
 
  // Focus the beat wrapper on every advance so a screen reader re-announces the beat.
  const wrapperRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    wrapperRef.current?.focus();
  }, [beat, triageIndex, compareIndex]);
 
  const goTo = (next: Beat) => {
    if (next === "handoff") dismissWelcome();
    setBeat(next);
  };
 
  const comparePairs = comparePairsFor(buildTriageStanding(triageOutcomes));
  const currentPair = comparePairs[compareIndex] ?? null;
 
  const handleTriageOutcome = (itemId: string, outcome: TriageOutcome) => {
    const next = { ...triageOutcomes, [itemId]: outcome };
    setTriageOutcomes(next);
    if (triageIndex + 1 < TRIAGE_ITEMS.length) {
      setTriageIndex((i) => i + 1);
      return;
    }
    // Every piece has passed through: pairs if two are loved, else straight to the ranking.
    goTo(comparePairsFor(buildTriageStanding(next)).length > 0 ? "pairs" : "standing");
  };
 
  const handleCompareOutcome = (outcome: CompareOutcome) => {
    Iif (!currentPair) {
      goTo("standing");
      return;
    }
    const [left, right] = currentPair;
    setCompareResults((prev) => [...prev, { itemAId: left.id, itemBId: right.id, outcome }]);
    if (compareIndex + 1 < comparePairs.length) {
      setCompareIndex((i) => i + 1);
    } else {
      goTo("standing");
    }
  };
 
  const exit = () => {
    dismissWelcome();
    // Full-document nav, NOT router.push: the server must re-run the /collections →
    // /welcome gate with the fresh dismissal cookie (purses-etv0).
    // eslint-disable-next-line @next/next/no-location-assign-relative-destination -- deliberate full navigation so the server re-runs the /welcome gate (purses-etv0)
    window.location.assign("/collections");
  };
 
  return (
    <div className="min-h-[100svh] flex flex-col items-center justify-center gap-8 px-4 py-8 sm:gap-10 sm:px-6 sm:py-12">
      <div ref={wrapperRef} tabIndex={-1} className="w-full max-w-md focus:outline-hidden sm:max-w-2xl">
        {beat === "purpose" && <WelcomePurpose onNext={() => goTo("feelings")} />}
        {beat === "feelings" && <WelcomeTriage index={triageIndex} onOutcome={handleTriageOutcome} />}
        {beat === "pairs" && currentPair && (
          <WelcomeCompare left={currentPair[0]} right={currentPair[1]} onOutcome={handleCompareOutcome} />
        )}
        {beat === "standing" && (
          <WelcomeStanding comparisons={compareResults} triage={triageOutcomes} onNext={() => goTo("handoff")} />
        )}
        {beat === "handoff" && <WelcomeHandoff onExplore={exit} />}
      </div>
 
      {/* Quiet, persistent escape on every beat but the last. */}
      {beat !== "handoff" && (
        <button
          type="button"
          onClick={exit}
          className="h-11 w-full max-w-xs rounded-control px-4 text-sm text-ink-soft hover:text-ink focus:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary sm:w-auto"
        >
          Leave tutorial
        </button>
      )}
    </div>
  );
}