diff --git a/FAMINELA.OTF b/FAMINELA.OTF new file mode 100644 index 0000000..091ff77 Binary files /dev/null and b/FAMINELA.OTF differ diff --git a/FAMINELA.TTF b/FAMINELA.TTF new file mode 100644 index 0000000..a99d4f2 Binary files /dev/null and b/FAMINELA.TTF differ diff --git a/src/app/api/dev-reset-profile/route.ts b/src/app/api/dev-reset-profile/route.ts new file mode 100644 index 0000000..b61154b --- /dev/null +++ b/src/app/api/dev-reset-profile/route.ts @@ -0,0 +1,41 @@ +import type { NextRequest } from "next/server"; +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +export async function POST(request: NextRequest) { + if (process.env.NODE_ENV !== "development") { + return Response.json({ error: "Forbidden in production" }, { status: 403 }); + } + + try { + const { userId } = await request.json(); + if (!userId) { + return Response.json({ error: "userId is required" }, { status: 400 }); + } + + const scriptPath = "C:\\Users\\User\\.gemini\\antigravity-ide\\brain\\5015ea2d-f1fc-4e3d-9372-4bb0670449cc\\scratch\\reset_user.py"; + // Construct the command safely with numeric userId + const command = `python "${scriptPath}" ${Number(userId)}`; + const { stdout, stderr } = await execAsync(command); + + if (stderr) { + console.error("Stderr from reset script:", stderr); + } + + console.log("Stdout from reset script:", stdout); + + if (stdout.includes("SUCCESS:")) { + return Response.json({ success: true, message: stdout.trim() }); + } else { + return Response.json({ error: stdout.trim() }, { status: 500 }); + } + } catch (error: any) { + console.error("Failed to reset profile:", error); + return Response.json({ error: error.message || "Internal Server Error" }, { status: 500 }); + } +} diff --git a/src/app/finding-match/page.tsx b/src/app/finding-match/page.tsx index 8e378f6..7ee7247 100644 --- a/src/app/finding-match/page.tsx +++ b/src/app/finding-match/page.tsx @@ -37,10 +37,7 @@ export default function FindingMatchPage() { }, [profile, locale, router]); const copy = t.findingMatch; - const matchImageSrc = - profile?.gender === "female" - ? "/assets/images/Group 15978fdsa80467.svg" - : "/assets/images/Group 159788fd0467.svg"; + const matchImageSrc = "/assets/images/Group 1597880466.svg"; return ( <> @@ -52,11 +49,11 @@ export default function FindingMatchPage() { >
-

{t.common.appName}

+

{t.common.appName}

-
+
diff --git a/src/app/globals.css b/src/app/globals.css index 0a20fc3..14f00e0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -174,7 +174,7 @@ html:lang(ar) body, [dir="rtl"].font-faminela, [dir="rtl"].font-ryling, .font-faminela { - font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + font-family: var(--font-faminela-local), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } .app-shell { diff --git a/src/app/intro/page.tsx b/src/app/intro/page.tsx index bb20ef4..fa40a76 100644 --- a/src/app/intro/page.tsx +++ b/src/app/intro/page.tsx @@ -8,6 +8,7 @@ import NavigationButton from "@/components/Componentes/navigation-button"; import ReportActionsSheet from "@/components/Componentes/report-actions-sheet"; import type { MarriageProfileResponse } from "@/hooks/marriage/types"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; +import { useMarriageConfigQuery } from "@/hooks/marriage/use-marriage-config"; import { authBridge } from "@/lib/auth-bridge"; import { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; @@ -25,8 +26,11 @@ export default function Intro() { const [isReportSheetOpen, setIsReportSheetOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isCheckingRedirect, setIsCheckingRedirect] = useState(true); + const [isPlayerOpen, setIsPlayerOpen] = useState(false); const isRedirectingRef = useRef(false); + const { data: config } = useMarriageConfigQuery(); + useEffect(() => { let isCancelled = false; @@ -157,7 +161,7 @@ export default function Intro() {
User @@ -171,7 +175,7 @@ export default function Intro() {
User @@ -185,7 +189,7 @@ export default function Intro() {
User @@ -197,21 +201,64 @@ export default function Intro() {
-
+
setIsPlayerOpen(true)} + > {t.intro.videoAlt} +
{t.intro.playAlt}
+ + {isPlayerOpen && ( +
+
+ +
+
+ )}
(null); + const paymentMutation = useHabcoinPaymentMutation(); useEffect(() => { if (!profile) { @@ -237,9 +241,44 @@ export default function NewMatchPage() { ? "یک گزینه‌ی مناسب برای شما پیدا شده است. در صورت تایید، مشخصات شما جهت ادامه فرآیند معرفی ارزیابی خواهد شد." : "A matching profile has been found. Information is provided by the candidate's family or introducers. If you approve, we'll share your profile with her family."; + const isMale = profile?.gender === "male"; + const hasActiveSub = !!profile?.active_subscription; + const isMatchAvailable = !!profile?.match_summary; + const showPaymentModal = isMale && !hasActiveSub && isMatchAvailable; + + const handlePayment = async () => { + const recommendedPlanId = profile?.recommended_plan?.id; + if (!recommendedPlanId) return; + + try { + setPaymentError(null); + await paymentMutation.mutateAsync(recommendedPlanId); + } catch (err: any) { + console.error("Payment failed", err); + const msg = + err?.response?.data?.error || err?.message || "Payment failed"; + const modalT = (t as any).paymentModal || {}; + if (msg === "Not enough coins") { + setPaymentError( + modalT.insufficientCoins || + "Insufficient coin balance. Please recharge your account.", + ); + } else { + setPaymentError(msg); + } + } + }; + return ( <> + {showPaymentModal && ( + + )}
setIsRejectSheetOpen(false)} /> ) : null} + {isMaleRejectWarningOpen ? ( + +

+ {t.maleRejectionWarning.carefulReview} +

+
+ 💡 +

+ {t.maleRejectionWarning.friendlyDelay} +

+
+
+ +

+ {t.maleRejectionWarning.noPenalty} +

+
+
+ } + buttons={({ close }) => ( +
+ { + close(); + setIsDismissReasonSheetOpen(true); + }} + /> + +
+ )} + onClose={() => setIsMaleRejectWarningOpen(false)} + /> + ) : null} {isDismissReasonSheetOpen ? ( setIsDismissReasonSheetOpen(false)} @@ -477,7 +534,13 @@ export default function NewMatchProfilePage() { +
+ + ); +} + +export default PaymentSwipeModal; diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx index fb78be7..dc402fc 100644 --- a/src/components/Componentes/question-answer-storage.tsx +++ b/src/components/Componentes/question-answer-storage.tsx @@ -360,6 +360,16 @@ export function QuestionAnswersProvider({ ); }, [slug, storageKey, serverSectionData, questions, canEdit]); + const syncTimeoutRef = useRef(null); + + useEffect(() => { + return () => { + if (syncTimeoutRef.current !== null) { + clearTimeout(syncTimeoutRef.current); + } + }; + }, []); + const getAnswerValue = useCallback( (question: QuestionField, questionIndex: number) => answers[getQuestionFieldKey(question, questionIndex)]?.value, @@ -393,10 +403,20 @@ export function QuestionAnswersProvider({ true, ); - // Sync with the backend immediately when value is updated - setTimeout(() => { + if (syncTimeoutRef.current !== null) { + clearTimeout(syncTimeoutRef.current); + } + + const isTextLike = + question.type === "text" || + question.type === "textarea" || + question.type === "number"; + + const delay = isTextLike ? 1000 : 0; + + syncTimeoutRef.current = setTimeout(() => { void flushAnswersRef.current(); - }, 0); + }, delay); return nextAnswers; }); diff --git a/src/components/Componentes/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx index e9de1f0..1674ceb 100644 --- a/src/components/Componentes/question-section-flow.tsx +++ b/src/components/Componentes/question-section-flow.tsx @@ -3,8 +3,7 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; import type { ReactNode } from "react"; -import { useCallback, useState } from "react"; -import Button from "./button"; +import { useCallback, useEffect, useState } from "react"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; @@ -77,7 +76,80 @@ function SectionFlowContent({ } }, [exitHref, flushAnswers, isCompleted, locale, router]); - const isDisabled = !isCompleted || isLeaving; + useEffect(() => { + if (!isCompleted || isLeaving) return; + if (activeQuestionIndex !== (questions?.length ?? 0) - 1) return; + + let isScheduled = false; + let timerId: NodeJS.Timeout | null = null; + + const checkAndSubmit = () => { + const activeEl = document.activeElement; + const isTyping = + activeEl && + (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA"); + + if (!isTyping) { + if (!isScheduled) { + isScheduled = true; + timerId = setTimeout(() => { + void handleContinue(); + }, 800); + } + } + }; + + checkAndSubmit(); + + const handleBlur = () => { + setTimeout(() => { + const activeEl = document.activeElement; + const stillTyping = + activeEl && + (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA"); + if (!stillTyping && !isScheduled) { + isScheduled = true; + timerId = setTimeout(() => { + void handleContinue(); + }, 400); + } + }, 100); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter") { + const target = e.target as HTMLElement; + if ( + target && + (target.tagName === "INPUT" || target.tagName === "TEXTAREA") + ) { + target.blur(); + if (!isScheduled) { + isScheduled = true; + timerId = setTimeout(() => { + void handleContinue(); + }, 400); + } + } + } + }; + + const activeEl = document.activeElement; + if (activeEl) { + activeEl.addEventListener("blur", handleBlur); + } + window.addEventListener("keydown", handleKeyDown); + + return () => { + if (activeEl) { + activeEl.removeEventListener("blur", handleBlur); + } + window.removeEventListener("keydown", handleKeyDown); + if (timerId) { + clearTimeout(timerId); + } + }; + }, [isCompleted, isLeaving, activeQuestionIndex, questions?.length, handleContinue]); const age = getStoredAge(); const activeQuestion = questions?.[activeQuestionIndex]; @@ -107,20 +179,6 @@ function SectionFlowContent({ height={31} /> } - footer={ - - } onQuestionExit={handleQuestionExit} onQuestionTransition={markOptionalQuestionsPassed} onActiveIndexChange={setActiveQuestionIndex} diff --git a/src/components/Componentes/question-snap-list.tsx b/src/components/Componentes/question-snap-list.tsx index bd95f8d..c255073 100644 --- a/src/components/Componentes/question-snap-list.tsx +++ b/src/components/Componentes/question-snap-list.tsx @@ -9,8 +9,6 @@ import { useRef, useState, } from "react"; - -import { GoArrowDown } from "react-icons/go"; import { useI18n } from "@/translations/provider"; import { useQuestionProgress } from "./question-progress-tracker"; @@ -412,20 +410,7 @@ export function QuestionSnapList({ {firstQuestionHint} ) : null} - {justCompleted && activeIndex < questions.length - 1 ? ( - - ) : null} + ); } diff --git a/src/components/Componentes/swipe-button.tsx b/src/components/Componentes/swipe-button.tsx new file mode 100644 index 0000000..6505881 --- /dev/null +++ b/src/components/Componentes/swipe-button.tsx @@ -0,0 +1,162 @@ +"use client"; + +import type React from "react"; +import { useEffect, useRef, useState } from "react"; +import { GoChevronLeft, GoChevronRight } from "react-icons/go"; +import { useI18n } from "@/translations/provider"; + +type SwipeButtonProps = { + onSuccess: () => void; + text: string; + disabled?: boolean; +}; + +export function SwipeButton({ + onSuccess, + text, + disabled = false, +}: SwipeButtonProps) { + const { locale } = useI18n(); + const isRtl = + locale === "fa" || + locale === "ar" || + locale === "ur" || + locale === "he" || + locale === "ks"; + + const [dragX, setDragX] = useState(0); + const [isDragging, setIsDragging] = useState(false); + const [swiped, setSwiped] = useState(false); + + const containerRef = useRef(null); + const startX = useRef(0); + + const getContainerWidth = () => { + return containerRef.current ? containerRef.current.clientWidth : 0; + }; + + const handleStart = (clientX: number) => { + if (disabled || swiped) return; + setIsDragging(true); + startX.current = clientX; + }; + + const handleMove = (clientX: number) => { + if (!isDragging || swiped) return; + const delta = isRtl ? startX.current - clientX : clientX - startX.current; + const containerWidth = getContainerWidth(); + const handleWidth = 48; // width of the handle button (w-12 = 48px) + const padding = 8; // padding around the handle (p-1 = 4px on each side -> 8px total) + const maxDrag = containerWidth - handleWidth - padding; + const newDragX = Math.max(0, Math.min(delta, maxDrag)); + setDragX(newDragX); + }; + + const handleEnd = () => { + if (!isDragging) return; + setIsDragging(false); + + const containerWidth = getContainerWidth(); + const handleWidth = 48; + const padding = 8; + const maxDrag = containerWidth - handleWidth - padding; + + if (dragX >= maxDrag * 0.85) { + const finalX = maxDrag; + setDragX(finalX); + setSwiped(true); + onSuccess(); + } else { + setDragX(0); + } + }; + + // Touch handlers + const onTouchStart = (e: React.TouchEvent) => { + handleStart(e.touches[0].clientX); + }; + + const onTouchMove = (e: React.TouchEvent) => { + handleMove(e.touches[0].clientX); + }; + + // Mouse handlers + const onMouseDown = (e: React.MouseEvent) => { + handleStart(e.clientX); + }; + + useEffect(() => { + if (!isDragging) return; + + const onGlobalMouseMove = (e: MouseEvent) => { + handleMove(e.clientX); + }; + + const onGlobalMouseUp = () => { + handleEnd(); + }; + + window.addEventListener("mousemove", onGlobalMouseMove); + window.addEventListener("mouseup", onGlobalMouseUp); + + return () => { + window.removeEventListener("mousemove", onGlobalMouseMove); + window.removeEventListener("mouseup", onGlobalMouseUp); + }; + }, [isDragging, handleEnd, handleMove]); + + const handleStyle = isRtl + ? { + transform: `translateX(${-dragX}px)`, + transition: isDragging ? "none" : "transform 0.2s ease-out", + right: "4px", + } + : { + transform: `translateX(${dragX}px)`, + transition: isDragging ? "none" : "transform 0.2s ease-out", + left: "4px", + }; + + return ( +
+ {/* Background sliding fill */} +
+ + {/* Slide text */} + + {swiped ? "..." : text} + + + {/* Slide handle */} +
+ {isRtl ? ( + + ) : ( + + )} +
+
+ ); +} + +export default SwipeButton; diff --git a/src/components/Componentes/token-switcher.tsx b/src/components/Componentes/token-switcher.tsx index cc37dc9..8375a78 100644 --- a/src/components/Componentes/token-switcher.tsx +++ b/src/components/Componentes/token-switcher.tsx @@ -39,6 +39,7 @@ export function TokenSwitcher({ const isMale = currentToken === MALE_TOKEN; const isFemale = currentToken === FEMALE_TOKEN; + const isNoToken = currentToken === "NO_TOKEN" || !currentToken; const handleSelectToken = (newToken: string) => { if (typeof window !== "undefined") { @@ -62,6 +63,59 @@ export function TokenSwitcher({ } }; + const handleResetUser = async (userId: number | null, label: string) => { + const message = + userId !== null + ? `آیا از پاک کردن تمامی اطلاعات هویتی، پاسخ‌ها و پیشرفت این کاربر (${label}) و شروع مجدد از مرحله اول آنبوردینگ مطمئن هستید؟` + : `آیا از پاک کردن تمامی اطلاعات محلی و کوکی‌های مربوط به این حالت (بدون توکن) و شروع مجدد مطمئن هستید؟`; + + if (!window.confirm(message)) { + return; + } + + try { + // 1. Clear local/session storage + if (typeof window !== "undefined") { + window.localStorage.clear(); + window.sessionStorage.clear(); + } + + // 2. Call backend reset script via API route if it is a real DB user + if (userId !== null) { + const response = await fetch("/api/dev-reset-profile", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userId }), + }); + + if (!response.ok) { + const errData = await response.json(); + alert(`خطا در ریست پروفایل: ${errData.error || response.statusText}`); + return; + } + } + + // 3. Set the target token in the cookies and redirect to / + const targetToken = userId === 17119 ? MALE_TOKEN : userId === 147714 ? FEMALE_TOKEN : "NO_TOKEN"; + + setClientCookie(TOKEN_COOKIE_NAME, targetToken); + setClientCookie("habib_token", targetToken); + if (typeof window !== "undefined") { + sessionStorage.setItem(TOKEN_COOKIE_NAME, targetToken); + (window as any).HABIB_TOKEN = targetToken; + setCurrentToken(targetToken); + } + + setIsOpen(false); + window.location.href = "/"; + } catch (error) { + console.error("Failed to reset user:", error); + alert("خطایی در انجام عملیات رخ داد."); + } + }; + return ( <> {isOpen && ( @@ -104,18 +158,20 @@ export function TokenSwitcher({
- {/* Male option */} -
+ +
+ {isMale && ( + + فعال + + )} +
- {isMale && ( - - فعال - - )} - + - {/* Female option */} - +
+ {isFemale && ( + + فعال + + )} +
- {isFemale && ( - - فعال - - )} - + + + {/* No token option card */} +
+ +
+ {isNoToken && ( + + فعال + + )} + +
+
-
+