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
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;
|