fix(surveys): one-row rating scale, rating auto-advance, mobile spacing

Design review of the live survey popover surfaced three issues:
1. NPS 0–10 wrapped to two rows (flex-wrap + flex-1 made the 8/9/10 row
   stretch absurdly wide) — now a single equal-width grid row (grid-cols-N
   by scale; static map so Tailwind keeps the classes).
2. Single-question ratings forced a redundant "Devam" tap — tapping a score
   now auto-advances (450ms debounce to allow changing the pick) and the
   submit button is hidden on rating questions.
3. On mobile the card crowded the Chatwoot launcher — bumped to bottom-28
   (sm:bottom-24 keeps desktop unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 01:19:59 +03:00
parent c18c513139
commit 34cb361b67

View File

@@ -8,7 +8,7 @@ import {
import { Button } from "@sase/ui";
import { X } from "lucide-react";
import type { Survey } from "posthog-js";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
// Self-rendered PostHog survey popover (API-mode surveys). Question copy comes
// from the survey definition in PostHog; this component only knows how to draw
@@ -16,6 +16,16 @@ import { useEffect, useState } from "react";
const OTHER_CHOICE = "__other__";
// Rating scales render as one equal-width row. Static map so Tailwind's JIT
// keeps these classes — a template-literal `grid-cols-${n}` would get purged.
// (NPS = 010 → 11 buttons; CSAT/CES → 3/5/7.)
const RATING_COLS: Record<number, string> = {
3: "grid-cols-3",
5: "grid-cols-5",
7: "grid-cols-7",
11: "grid-cols-11",
};
export function SurveyPopover() {
const [survey, setSurvey] = useState<Survey | null>(null);
useEffect(() => subscribeActiveSurvey(setSurvey), []);
@@ -31,6 +41,7 @@ function SurveyCard({ survey }: { survey: Survey }) {
const [openText, setOpenText] = useState("");
const [rating, setRating] = useState<number | null>(null);
const [done, setDone] = useState(false);
const advanceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const questions = survey.questions;
const q = questions[step];
@@ -41,6 +52,9 @@ function SurveyCard({ survey }: { survey: Survey }) {
return () => clearTimeout(t);
}, [done]);
// Cancel a pending rating auto-advance if the card unmounts mid-debounce.
useEffect(() => () => clearTimeout(advanceTimer.current ?? undefined), []);
const qid = q?.id ?? `q${step}`;
const choices = q && "choices" in q && Array.isArray(q.choices) ? q.choices : [];
const hasOpenChoice = Boolean(q && "hasOpenChoice" in q && q.hasOpenChoice);
@@ -69,8 +83,7 @@ function SurveyCard({ survey }: { survey: Survey }) {
: { ...answers, [qid]: value };
}
function advance() {
const next = collect();
function proceed(next: SurveyResponses) {
setAnswers(next);
if (step + 1 < questions.length) {
setStep(step + 1);
@@ -84,6 +97,19 @@ function SurveyCard({ survey }: { survey: Survey }) {
setDone(true);
}
function advance() {
proceed(collect());
}
// Rating questions auto-advance — tapping a score IS the answer, so there's no
// separate submit button. A short debounce lets the user change their pick
// (and re-tapping clears the prior timer) before the card moves on.
function selectRating(v: number) {
setRating(v);
clearTimeout(advanceTimer.current ?? undefined);
advanceTimer.current = setTimeout(() => proceed({ ...answers, [qid]: v }), 450);
}
function dismiss() {
if (done) {
closeActiveSurvey();
@@ -101,7 +127,7 @@ function SurveyCard({ survey }: { survey: Survey }) {
return (
<section
aria-label={survey.name}
className="fixed bottom-24 right-4 z-[70] w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-border bg-card p-5 text-card-foreground shadow-2xl"
className="fixed bottom-28 right-4 z-[70] w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-border bg-card p-5 text-card-foreground shadow-2xl sm:bottom-24"
>
<button
type="button"
@@ -177,13 +203,13 @@ function SurveyCard({ survey }: { survey: Survey }) {
{q.type === "rating" ? (
<div className="mt-3">
<div className="flex flex-wrap gap-1">
<div className={`grid gap-1 ${RATING_COLS[ratingValues.length] ?? "grid-cols-5"}`}>
{ratingValues.map((v) => (
<button
key={v}
type="button"
onClick={() => setRating(v)}
className={`h-8 min-w-8 flex-1 rounded-md border text-sm transition-colors ${
onClick={() => selectRating(v)}
className={`flex h-9 items-center justify-center rounded-md border text-xs transition-colors ${
rating === v
? "border-brand bg-brand text-brand-foreground"
: "border-border text-muted-foreground hover:border-foreground/30 hover:text-foreground"
@@ -212,9 +238,16 @@ function SurveyCard({ survey }: { survey: Survey }) {
/>
) : null}
<Button variant="brand" className="mt-4 w-full" disabled={!canContinue} onClick={advance}>
{buttonLabel}
</Button>
{q.type !== "rating" ? (
<Button
variant="brand"
className="mt-4 w-full"
disabled={!canContinue}
onClick={advance}
>
{buttonLabel}
</Button>
) : null}
</div>
) : null}
</section>