You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

281 lines
9.1 KiB

"use client";
import { useLayoutEffect, useRef, useState } from "react";
import type { QuestionField } from "@/lib/schema-adapter";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
type QuestionSliderProps = {
question: QuestionField;
disabled?: boolean;
};
export function QuestionSlider({
question,
disabled,
}: QuestionSliderProps) {
const { dictionary: t } = useI18n();
const [min, max] = question.extras.range;
const initialValue = Math.round((min + max) / 2);
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const storedValue = getAnswerValue(question);
const isDesiredAgeRange = false;
const thumbWidth = 18;
const bubbleHalfWidth = 18;
const steps = (() => {
const range = max - min;
const interval = range <= 15 ? 1 : range <= 50 ? 5 : range <= 150 ? 10 : 20;
const arr: number[] = [];
for (let val = min; val <= max; val += interval) {
arr.push(val);
}
if (arr[arr.length - 1] !== max) {
const lastVal = arr[arr.length - 1];
if (max - lastVal < interval * 0.5) {
arr.pop();
}
arr.push(max);
}
return arr;
})();
// Multi-state logic for Age Range Slider
let fromVal = 25;
let toVal = 30;
if (isDesiredAgeRange) {
if (typeof storedValue === "string" && storedValue.includes("-")) {
const parts = storedValue.split("-").map(Number);
if (
parts.length === 2 &&
!Number.isNaN(parts[0]) &&
!Number.isNaN(parts[1])
) {
fromVal = parts[0];
toVal = parts[1];
}
} else if (typeof storedValue === "number") {
fromVal = storedValue;
toVal = Math.max(storedValue, max);
}
fromVal = Math.max(min, Math.min(max, fromVal));
toVal = Math.max(min, Math.min(max, toVal));
if (toVal < fromVal) {
toVal = fromVal;
}
}
// Single-slider states (only used if isDesiredAgeRange is false)
const value = typeof storedValue === "number" ? storedValue : initialValue;
const progress = max === min ? 0 : ((value - min) / (max - min)) * 100;
const sliderWrapperRef = useRef<HTMLDivElement>(null);
const sliderRef = useRef<HTMLInputElement>(null);
const [bubblePosition, setBubblePosition] = useState(bubbleHalfWidth);
useLayoutEffect(() => {
if (isDesiredAgeRange) return;
const wrapper = sliderWrapperRef.current;
const slider = sliderRef.current;
if (!wrapper || !slider) {
return;
}
const updateBubblePosition = () => {
const wrapperRect = wrapper.getBoundingClientRect();
const sliderRect = slider.getBoundingClientRect();
const sliderLeft = sliderRect.left - wrapperRect.left;
const thumbCenter =
sliderLeft +
thumbWidth / 2 +
(progress / 100) * (sliderRect.width - thumbWidth);
setBubblePosition(
Math.min(
Math.max(thumbCenter, bubbleHalfWidth),
wrapperRect.width - bubbleHalfWidth,
),
);
};
updateBubblePosition();
const resizeObserver = new ResizeObserver(updateBubblePosition);
resizeObserver.observe(wrapper);
resizeObserver.observe(slider);
return () => resizeObserver.disconnect();
}, [progress]);
if (isDesiredAgeRange) {
const handleFromChange = (newFrom: number) => {
const val = Math.min(newFrom, toVal);
setAnswerValue(question, `${val}-${toVal}`);
};
const handleToChange = (newTo: number) => {
const val = Math.max(newTo, fromVal);
setAnswerValue(question, `${fromVal}-${val}`);
};
const progressFrom =
max === min ? 0 : ((fromVal - min) / (max - min)) * 100;
const progressTo = max === min ? 0 : ((toVal - min) / (max - min)) * 100;
return (
<div
className={[
"flex w-full flex-col gap-6 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
{/* First Slider (From Age) */}
<div className="flex flex-col gap-2 pt-2">
<div className="flex justify-between items-center text-[14px] font-bold text-[#181818]">
<span>{t.From ?? "From"}</span>
<span className="rounded-[8px] bg-[#FCE7EA] text-[#F2465F] px-2.5 py-1 text-[13px] font-bold">
{fromVal}
</span>
</div>
<div className="relative">
<input
type="range"
min={min}
max={max}
step={1}
value={fromVal}
onChange={(e) => handleFromChange(Number(e.target.value))}
disabled={disabled}
className="question-slider-range w-full"
style={{
background: `linear-gradient(to right, #F2465F 0%, #F2465F ${progressFrom}%, #FCE7EA ${progressFrom}%, #FCE7EA 100%)`,
}}
/>
</div>
<div className="relative h-4 text-[#B7B7B7] text-[10px] font-medium w-full">
{steps.map((step) => {
const leftPct = ((step - min) / (max - min)) * 100;
return (
<span
key={`from-${step}`}
className="absolute -translate-x-1/2"
style={{
left: `calc(${leftPct}% - (${leftPct / 100} * ${thumbWidth}px) + ${thumbWidth / 2}px)`,
}}
>
{step}
</span>
);
})}
</div>
</div>
{/* Second Slider (To Age) */}
<div className="flex flex-col gap-2 pt-2">
<div className="flex justify-between items-center text-[14px] font-bold text-[#181818]">
<span>{t.To ?? "To"}</span>
<span className="rounded-[8px] bg-[#FCE7EA] text-[#F2465F] px-2.5 py-1 text-[13px] font-bold">
{toVal}
</span>
</div>
<div className="relative">
<input
type="range"
min={min}
max={max}
step={1}
value={toVal}
onChange={(e) => handleToChange(Number(e.target.value))}
disabled={disabled}
className="question-slider-range w-full"
style={{
background: `linear-gradient(to right, #F2465F 0%, #F2465F ${progressTo}%, #FCE7EA ${progressTo}%, #FCE7EA 100%)`,
}}
/>
</div>
<div className="relative h-4 text-[#B7B7B7] text-[10px] font-medium w-full">
{steps.map((step) => {
const leftPct = ((step - min) / (max - min)) * 100;
return (
<span
key={`to-${step}`}
className="absolute -translate-x-1/2"
style={{
left: `calc(${leftPct}% - (${leftPct / 100} * ${thumbWidth}px) + ${thumbWidth / 2}px)`,
}}
>
{step}
</span>
);
})}
</div>
</div>
</div>
);
}
return (
<div
className={[
"flex w-full flex-col gap-3.5 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
<div className="pt-7">
<div ref={sliderWrapperRef} className="relative">
<div
className="-translate-x-1/2 absolute -top-7.5 z-10 flex items-center justify-center rounded-[8px] bg-[#F2465F] px-3 py-1 text-[14px] font-bold text-white shadow-md after:absolute after:-bottom-[4px] after:left-1/2 after:h-[9px] after:w-2.5 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-[#F2465F] after:content-['']"
style={{ left: `${bubblePosition}px` }}
>
{value}
</div>
<input
ref={sliderRef}
type="range"
min={min}
max={max}
step={1}
value={value}
onChange={(event) =>
setAnswerValue(
question, Number(event.target.value),
)
}
disabled={disabled}
className="question-slider-range w-full"
style={{
background: `linear-gradient(to right, #F2465F 0%, #F2465F ${progress}%, #FCE7EA ${progress}%, #FCE7EA 100%)`,
}}
/>
</div>
<div
className="relative mt-2 h-5 text-[#B7B7B7] text-[10px] font-medium"
style={{ width: "100%" }}
>
{steps.map((step) => {
const leftPct = ((step - min) / (max - min)) * 100;
return (
<span
key={step}
className="absolute -translate-x-1/2"
style={{
left: `calc(${leftPct}% - (${leftPct / 100} * ${thumbWidth}px) + ${thumbWidth / 2}px)`,
}}
>
{step}
</span>
);
})}
</div>
</div>
</div>
);
}
export default QuestionSlider;