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.
249 lines
8.0 KiB
249 lines
8.0 KiB
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import type { TouchEvent } from "react";
|
|
import { useEffect, useState } from "react";
|
|
import { useUpdateMarriageProfileBasicMutation } from "@/hooks/marriage/use-profile-basic";
|
|
import { localizePath } from "@/translations/config";
|
|
import { useI18n } from "@/translations/provider";
|
|
import Button from "./button";
|
|
import NavigationButton from "./navigation-button";
|
|
import type { GenderAnswer, RegistrationAnswer } from "./slider-slide";
|
|
import { SliderSlideFive } from "./slider-slide-five";
|
|
import { SliderSlideFour } from "./slider-slide-four";
|
|
import { SliderSlideOne, SliderSlideOneActions } from "./slider-slide-one";
|
|
import { SliderSlideThree } from "./slider-slide-three";
|
|
import { SliderSlideTwo } from "./slider-slide-two";
|
|
|
|
const FINAL_SLIDE_COUNT = 5;
|
|
const SWIPE_THRESHOLD = 40;
|
|
const SLIDER_ANSWERS_STORAGE_KEY = "marriage-slider-answers";
|
|
|
|
export default function SliderPage() {
|
|
const router = useRouter();
|
|
const { locale } = useI18n();
|
|
const [activeSlide, setActiveSlide] = useState(0);
|
|
const [selectedGender, setSelectedGender] = useState<GenderAnswer>("woman");
|
|
const [selectedRegistration, setSelectedRegistration] =
|
|
useState<RegistrationAnswer>("self");
|
|
const [hasReadRules, setHasReadRules] = useState(false);
|
|
const [touchStartX, setTouchStartX] = useState<number | null>(null);
|
|
const updateProfileBasicMutation = useUpdateMarriageProfileBasicMutation();
|
|
const hasFinalNotice = true;
|
|
const maxSlideIndex = FINAL_SLIDE_COUNT - 1;
|
|
const displaySlideCount = FINAL_SLIDE_COUNT;
|
|
const navDotCount = displaySlideCount;
|
|
|
|
useEffect(() => {
|
|
// Clear any previous slider answers on mount so abandoned onboarding starts clean
|
|
if (typeof window !== "undefined") {
|
|
window.localStorage.removeItem(SLIDER_ANSWERS_STORAGE_KEY);
|
|
}
|
|
}, []);
|
|
|
|
const goToSlide = (index: number) => {
|
|
setActiveSlide(Math.max(0, Math.min(index, maxSlideIndex)));
|
|
};
|
|
|
|
const goToPreviousSlide = () => {
|
|
goToSlide(activeSlide - 1);
|
|
};
|
|
|
|
const goToNextSlide = () => {
|
|
goToSlide(activeSlide + 1);
|
|
};
|
|
|
|
const completeSlider = async () => {
|
|
const navigateAway = () => {
|
|
if (typeof window !== "undefined") {
|
|
window.localStorage.removeItem(SLIDER_ANSWERS_STORAGE_KEY);
|
|
// Use hard navigation instead of router.push() to ensure the
|
|
// destination page loads with a fresh auth state. SPA (soft)
|
|
// navigation sometimes leaves stale query-cache / token state
|
|
// that causes the profile query to return 401 and the page to
|
|
// stay stuck on the loading spinner.
|
|
window.location.href = localizePath(
|
|
"/questions-list/personal_info",
|
|
locale,
|
|
);
|
|
return;
|
|
}
|
|
router.push(localizePath("/questions-list/personal_info", locale));
|
|
};
|
|
|
|
// Safety timeout — navigate after 5 seconds even if the request hangs
|
|
const timeout = setTimeout(navigateAway, 5000);
|
|
|
|
try {
|
|
await updateProfileBasicMutation.mutateAsync({
|
|
gender: selectedGender === "man" ? "male" : "female",
|
|
is_registering_for_self: selectedRegistration === "self",
|
|
});
|
|
} catch (error) {
|
|
console.warn("Failed to update profile basic details:", error);
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
navigateAway();
|
|
}
|
|
};
|
|
|
|
const handleTouchStart = (event: TouchEvent<HTMLDivElement>) => {
|
|
setTouchStartX(event.touches[0]?.clientX ?? null);
|
|
};
|
|
|
|
const handleTouchEnd = (event: TouchEvent<HTMLDivElement>) => {
|
|
if (touchStartX === null) {
|
|
return;
|
|
}
|
|
|
|
const touchEndX = event.changedTouches[0]?.clientX ?? touchStartX;
|
|
const deltaX = touchStartX - touchEndX;
|
|
|
|
if (Math.abs(deltaX) >= SWIPE_THRESHOLD) {
|
|
if (deltaX > 0) {
|
|
goToNextSlide();
|
|
} else {
|
|
goToPreviousSlide();
|
|
}
|
|
}
|
|
|
|
setTouchStartX(null);
|
|
};
|
|
|
|
return (
|
|
<div className="relative flex h-[100dvh] max-h-[100dvh] flex-col overflow-hidden">
|
|
<header className="relative -mx-[17px] rounded-b-[32px] px-[17px] pt-[max(16px,calc(var(--safe-top)+8px))] pb-3.5">
|
|
<div className="relative flex items-center justify-between">
|
|
<NavigationButton
|
|
icon="back"
|
|
iconLabel="Close slider"
|
|
onClick={() => router.back()}
|
|
/>
|
|
<nav
|
|
aria-label={`Slide ${activeSlide + 1} of ${displaySlideCount}`}
|
|
className="absolute left-1/2 flex -translate-x-1/2 items-center gap-2"
|
|
>
|
|
{Array.from({ length: navDotCount }, (_, index) => {
|
|
const isActive = index === activeSlide;
|
|
|
|
return (
|
|
<button
|
|
key={`slide-${index + 1}`}
|
|
type="button"
|
|
aria-label={`Go to slide ${index + 1}`}
|
|
aria-pressed={isActive}
|
|
className={[
|
|
"h-2.5 rounded-full border-1 border-[#F14B46] transition-all duration-300 ease-out",
|
|
isActive ? "w-6 bg-[#F14B46]" : "w-2.5 bg-white/80",
|
|
].join(" ")}
|
|
onClick={() => goToSlide(index)}
|
|
/>
|
|
);
|
|
})}
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main
|
|
className="flex min-h-0 flex-1 touch-pan-y overflow-hidden pt-6"
|
|
onTouchEnd={handleTouchEnd}
|
|
onTouchStart={handleTouchStart}
|
|
>
|
|
<div
|
|
className="flex h-full w-full transition-transform duration-300 ease-out"
|
|
style={{ transform: `translateX(-${activeSlide * 100}%)` }}
|
|
>
|
|
<SliderSlideOne
|
|
index={0}
|
|
onScrollToEnd={() => setHasReadRules(true)}
|
|
/>
|
|
<SliderSlideTwo index={1} />
|
|
<SliderSlideThree
|
|
index={2}
|
|
selectedGender={selectedGender}
|
|
onGenderChange={setSelectedGender}
|
|
/>
|
|
<SliderSlideFour
|
|
index={3}
|
|
selectedRegistration={selectedRegistration}
|
|
onRegistrationChange={setSelectedRegistration}
|
|
/>
|
|
{hasFinalNotice ? <SliderSlideFive index={4} /> : null}
|
|
</div>
|
|
</main>
|
|
|
|
<div className="pointer-events-none fixed bottom-0 left-1/2 z-20 w-full sm:max-w-[375px] -translate-x-1/2 bg-[#F5F5F5]">
|
|
<div
|
|
style={{ paddingBottom: `calc(1rem + var(--safe-bottom))` }}
|
|
className="mx-auto w-full sm:max-w-[375px] px-[17px] pt-4"
|
|
>
|
|
<div className="pointer-events-auto">
|
|
{activeSlide === 0 ? (
|
|
<SliderSlideOneActions
|
|
onAccept={goToNextSlide}
|
|
disabled={!hasReadRules}
|
|
/>
|
|
) : activeSlide === maxSlideIndex ? (
|
|
<SliderFinalActions
|
|
onBack={goToPreviousSlide}
|
|
isFinishing={updateProfileBasicMutation.isPending}
|
|
onFinish={completeSlider}
|
|
/>
|
|
) : (
|
|
<SliderStepActions
|
|
onBack={goToPreviousSlide}
|
|
onNext={goToNextSlide}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type SliderStepActionsProps = {
|
|
onBack: () => void;
|
|
onNext: () => void;
|
|
};
|
|
|
|
function SliderStepActions({ onBack, onNext }: SliderStepActionsProps) {
|
|
return (
|
|
<div className="flex items-center gap-3 [&>button]:flex-1">
|
|
<Button variant="outlined" arrowDirection="left" onClick={onBack}>
|
|
Back
|
|
</Button>
|
|
<Button arrowDirection="right" onClick={onNext}>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type SliderFinalActionsProps = {
|
|
isFinishing?: boolean;
|
|
onBack: () => void;
|
|
onFinish: () => void | Promise<void>;
|
|
};
|
|
|
|
function SliderFinalActions({
|
|
isFinishing = false,
|
|
onBack,
|
|
onFinish,
|
|
}: SliderFinalActionsProps) {
|
|
return (
|
|
<div className="flex items-center gap-3 [&>button]:flex-1">
|
|
<Button
|
|
variant="outlined"
|
|
arrowDirection="left"
|
|
disabled={isFinishing}
|
|
onClick={onBack}
|
|
>
|
|
Back
|
|
</Button>
|
|
<Button disabled={isFinishing} isLoading={isFinishing} onClick={onFinish}>
|
|
Finish
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|