From 31fd5b8f50873a5b18ef6b1b468f86c0483e9162 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Wed, 19 Aug 2026 22:06:01 +0330 Subject: [PATCH 01/39] feat(ui): improve slider touch gestures and add i18n support - Enhance touch swipe logic in SliderPage by tracking vertical movement to prevent accidental swipes during vertical scrolling - Implement internationalization (i18n) in SliderSlideTwo using the `useI18n` hook - Refactor SliderSlideTwo layout and styling for better responsiveness and content alignment - Update aspect ratios and spacing for video thumbnail components --- src/components/Componentes/slider-page.tsx | 13 ++++++-- .../Componentes/slider-slide-two.tsx | 32 ++++++++++++------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/components/Componentes/slider-page.tsx b/src/components/Componentes/slider-page.tsx index 7cc1677..0a46f21 100644 --- a/src/components/Componentes/slider-page.tsx +++ b/src/components/Componentes/slider-page.tsx @@ -33,6 +33,7 @@ export default function SliderPage() { useState("self"); const [hasReadRules, setHasReadRules] = useState(false); const [touchStartX, setTouchStartX] = useState(null); + const [touchStartY, setTouchStartY] = useState(null); const updateProfileBasicMutation = useUpdateMarriageProfileBasicMutation(); const queryClient = useQueryClient(); const [submitError, setSubmitError] = useState(null); @@ -120,20 +121,27 @@ export default function SliderPage() { const handleTouchStart = (event: TouchEvent) => { setTouchStartX(event.touches[0]?.clientX ?? null); + setTouchStartY(event.touches[0]?.clientY ?? null); }; const handleTouchEnd = (event: TouchEvent) => { - if (touchStartX === null) { + if (touchStartX === null || touchStartY === null) { return; } const touchEndX = event.changedTouches[0]?.clientX ?? touchStartX; + const touchEndY = event.changedTouches[0]?.clientY ?? touchStartY; const deltaX = touchStartX - touchEndX; + const deltaY = touchStartY - touchEndY; - if (Math.abs(deltaX) >= SWIPE_THRESHOLD) { + if ( + Math.abs(deltaX) >= SWIPE_THRESHOLD && + Math.abs(deltaX) > Math.abs(deltaY) * 1.5 + ) { if (deltaX > 0) { if (activeSlide === 0 && !hasReadRules) { setTouchStartX(null); + setTouchStartY(null); return; } goToNextSlide(); @@ -143,6 +151,7 @@ export default function SliderPage() { } setTouchStartX(null); + setTouchStartY(null); }; return ( diff --git a/src/components/Componentes/slider-slide-two.tsx b/src/components/Componentes/slider-slide-two.tsx index 5b117f5..9d08344 100644 --- a/src/components/Componentes/slider-slide-two.tsx +++ b/src/components/Componentes/slider-slide-two.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import { useState } from "react"; import { useMarriageConfigQuery } from "@/hooks/marriage/use-marriage-config"; +import { useI18n } from "@/translations/provider"; import { SliderHeader } from "./slider-header"; import type { SliderSlideProps } from "./slider-slide"; import VideoPlayer from "./video-player"; @@ -11,6 +12,7 @@ import NetworkImage from "./network-image"; export function SliderSlideTwo({ index }: SliderSlideProps) { const [isPlayerOpen, setIsPlayerOpen] = useState(false); const { data: config } = useMarriageConfigQuery(); + const { dictionary: t } = useI18n(); const thumbnailSrc = config?.video_step_2_thumbnail_url || @@ -24,13 +26,16 @@ export function SliderSlideTwo({ index }: SliderSlideProps) { aria-label={`Slide ${index + 1}`} className="flex h-full min-h-0 w-full shrink-0 flex-col" > -
- +
+
+ +
+
setIsPlayerOpen(true)} > -
+
-
+ +
Dr. Hasti Masoudi

- Dr. Hasti Masoudi + {t["Dr. Hasti Masoudi"] || "Dr. Hasti Masoudi"}

-

- We have come together with the goal of creating a secure and - confidential path for "permanent marriage" among Muslims +

+ {t[ + 'We have come together with the goal of creating a secure and confidential path for "permanent marriage" among Muslims' + ] || + 'We have come together with the goal of creating a secure and confidential path for "permanent marriage" among Muslims'}

From 58100b685f0d91de284dfe03a6f17a77f8e14f28 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Wed, 19 Aug 2026 23:17:58 +0330 Subject: [PATCH 02/39] feat: integrate SliderPage as an overlay in intro-client with hardware back button support --- src/app/intro/intro-client.tsx | 282 ++++++++++++--------- src/components/Componentes/slider-page.tsx | 12 +- 2 files changed, 179 insertions(+), 115 deletions(-) diff --git a/src/app/intro/intro-client.tsx b/src/app/intro/intro-client.tsx index bcf6d05..2344081 100644 --- a/src/app/intro/intro-client.tsx +++ b/src/app/intro/intro-client.tsx @@ -2,15 +2,18 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import Button from "@/components/Componentes/button"; import NetworkImage from "@/components/Componentes/network-image"; import PageHeader from "@/components/Componentes/page-header"; import ReportActionsSheet from "@/components/Componentes/report-actions-sheet"; +import SectionOverlayHost from "@/components/Componentes/section-overlay-host"; +import SliderPage from "@/components/Componentes/slider-page"; import VideoPlayer from "@/components/Componentes/video-player"; import { useMarriageConfigQuery } from "@/hooks/marriage/use-marriage-config"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useHabibWebReady } from "@/hooks/use-habib-web-ready"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { authBridge } from "@/lib/auth-bridge"; import { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; @@ -26,6 +29,7 @@ export default function IntroClient() { const [isReportSheetOpen, setIsReportSheetOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isPlayerOpen, setIsPlayerOpen] = useState(false); + const [isStepsOpen, setIsStepsOpen] = useState(false); const { data: config } = useMarriageConfigQuery(); @@ -33,6 +37,47 @@ export default function IntroClient() { // image so we don't need to wait for it — announce immediately on mount. useHabibWebReady(true); + useEffect(() => { + const readStepsFromUrl = () => { + if (typeof window === "undefined") return; + const params = new URLSearchParams(window.location.search); + setIsStepsOpen(params.get("steps") === "open"); + }; + + readStepsFromUrl(); + window.addEventListener("popstate", readStepsFromUrl); + return () => window.removeEventListener("popstate", readStepsFromUrl); + }, []); + + const handleOpenSteps = useCallback(() => { + setIsStepsOpen(true); + if (typeof window !== "undefined") { + const url = new URL(window.location.href); + url.searchParams.set("steps", "open"); + window.history.pushState({ steps: "open" }, "", url.toString()); + } + }, []); + + const handleCloseSteps = useCallback(() => { + if (typeof window !== "undefined") { + const params = new URLSearchParams(window.location.search); + if (params.get("steps") === "open") { + setIsStepsOpen(false); + window.history.back(); + return; + } + } + setIsStepsOpen(false); + }, []); + + useHardwareBackHandler(() => { + if (isStepsOpen) { + handleCloseSteps(); + return true; // handled: keep WebView open, just close the overlay + } + return false; // Root: tell Flutter to close WebView screen + }, isStepsOpen); + const handleSubmit = async () => { if (isSubmitting) { return; @@ -61,137 +106,148 @@ export default function IntroClient() { } const submitPath = getSubmitPath(profileResponse); - const nextPath = localizePath( - submitPath === "/intro" ? "/terms" : submitPath, - locale, - ); - router.push(nextPath); + if (submitPath === "/intro" || submitPath === "/terms") { + handleOpenSteps(); + } else { + router.push(localizePath(submitPath, locale)); + } } catch (error) { console.error("Submission/redirect failed", error); - router.push(localizePath("/terms", locale)); + handleOpenSteps(); } finally { setIsSubmitting(false); } }; return ( -
- {isReportSheetOpen && ( - setIsReportSheetOpen(false)} /> - )} - setIsReportSheetOpen(true), - }} - /> -
-
- {t["heavenly -

- {t["A Path to Heavenly Marriage"]} -

-

- { - t[ - 'We have come together with the goal of creating a secure and confidential path for "permanent marriage" among Muslims' - ] - } -

-
-
-
+ <> +
+ {isReportSheetOpen && ( + setIsReportSheetOpen(false)} /> + )} + setIsReportSheetOpen(true), + }} + /> +
+
{t["user -
-

120

-

- {t["user profiles"]} -

+

+ {t["A Path to Heavenly Marriage"]} +

+

+ { + t[ + 'We have come together with the goal of creating a secure and confidential path for "permanent marriage" among Muslims' + ] + } +

+
+
+
+ {t["user +
+

120

+

+ {t["user profiles"]} +

+
+
+
+ {t["matches"]} +
+

14

+

+ {t["matches"]} +

+
+
+
+ {t["marriages"]} +
+

14

+

+ {t["marriages"]} +

+
-
- {t["matches"]} setIsPlayerOpen(true)} + > + -
-

14

-

- {t["matches"]} -

-
-
-
+
{t["marriages"]} -
-

14

-

- {t["marriages"]} -

-
-
-
setIsPlayerOpen(true)} - > - -
- {t["play"]} -
- setIsPlayerOpen(false)} - videoUrl={config?.intro_video_url} - /> -
-
- + +
-
-
-
+
+
+ + + {isStepsOpen && ( + + )} + + ); } diff --git a/src/components/Componentes/slider-page.tsx b/src/components/Componentes/slider-page.tsx index 0a46f21..ec2b09e 100644 --- a/src/components/Componentes/slider-page.tsx +++ b/src/components/Componentes/slider-page.tsx @@ -24,7 +24,11 @@ const FINAL_SLIDE_COUNT = 5; const SWIPE_THRESHOLD = 40; const SLIDER_ANSWERS_STORAGE_KEY = "marriage-slider-answers"; -export default function SliderPage() { +export type SliderPageProps = { + onClose?: () => void; +}; + +export default function SliderPage({ onClose }: SliderPageProps = {}) { const router = useRouter(); const { locale } = useI18n(); const [activeSlide, setActiveSlide] = useState(0); @@ -169,7 +173,11 @@ export default function SliderPage() { console.warn("sessionStorage is not accessible:", e); } } - router.back(); + if (onClose) { + onClose(); + } else { + router.back(); + } }} />