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

"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>
);
}