diff --git a/public/assets/images/diamond-black.png b/public/assets/images/diamond-black.png new file mode 100644 index 0000000..d10d63a Binary files /dev/null and b/public/assets/images/diamond-black.png differ diff --git a/public/assets/images/diamond-color.png b/public/assets/images/diamond-color.png new file mode 100644 index 0000000..8dff14d Binary files /dev/null and b/public/assets/images/diamond-color.png differ diff --git a/public/assets/images/diamond-gradient.png b/public/assets/images/diamond-gradient.png new file mode 100644 index 0000000..8dff14d Binary files /dev/null and b/public/assets/images/diamond-gradient.png differ diff --git a/public/assets/images/diamond-outline.png b/public/assets/images/diamond-outline.png new file mode 100644 index 0000000..d10d63a Binary files /dev/null and b/public/assets/images/diamond-outline.png differ diff --git a/src/app/globals.css b/src/app/globals.css index 00f8adb..d3a1473 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -161,29 +161,29 @@ body[data-page-background="custom"] .app-shell { 0 0 0 1px rgb(0 0 0 / 8%); } -/* 3 Dots Pulsing Wave Loader for Buttons & App */ -@keyframes button-dot-slide { - 0%, 100% { - transform: scale(0.35); - opacity: 0.35; +/* 3 Dots Bouncing Line Circle Loader for Buttons */ +@keyframes button-dot-bounce { + 0%, 80%, 100% { + transform: translateY(0) scale(0.7); + opacity: 0.4; } 40% { - transform: scale(1.25); + transform: translateY(-4px) scale(1.15); opacity: 1; } } .animate-dots-slide-1 { - animation: button-dot-slide 0.9s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; + animation: button-dot-bounce 0.9s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation-delay: 0s; } .animate-dots-slide-2 { - animation: button-dot-slide 0.9s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; - animation-delay: 0.18s; + animation: button-dot-bounce 0.9s cubic-bezier(0.4, 0, 0.6, 1) infinite; + animation-delay: 0.15s; } .animate-dots-slide-3 { - animation: button-dot-slide 0.9s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite; - animation-delay: 0.36s; + animation: button-dot-bounce 0.9s cubic-bezier(0.4, 0, 0.6, 1) infinite; + animation-delay: 0.3s; } diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx index cc687c3..37abd4d 100644 --- a/src/app/new-match/profile/page.tsx +++ b/src/app/new-match/profile/page.tsx @@ -472,7 +472,7 @@ export default function NewMatchProfilePage() { type="button" disabled={!caseId || isSubmitting || isMaleAccepted} onClick={() => setIsRejectSheetOpen(true)} - className="inline-flex w-1/3 items-center justify-center rounded-[12px] border border-[#BFBFBF] bg-white px-4 py-[13px] text-[16px] font-semibold text-[#9A9A9A]" + className="inline-flex w-1/3 h-[52px] items-center justify-center rounded-[12px] border border-[#BFBFBF] bg-white px-4 text-[16px] font-semibold text-[#9A9A9A]" > Reject @@ -490,7 +490,7 @@ export default function NewMatchProfilePage() { setIsRequestSheetOpen(true); }} - className="inline-flex w-2/3 whitespace-nowrap items-center justify-center gap-1 rounded-[12px] bg-[#F0445B] px-4 py-[13px] text-[16px] font-semibold text-white shadow-[0_8px_16px_rgba(240,68,91,0.24)] disabled:cursor-not-allowed disabled:opacity-50" + className="inline-flex w-2/3 h-[52px] whitespace-nowrap items-center justify-center gap-1 rounded-[12px] bg-[#F0445B] px-4 text-[16px] font-semibold text-white shadow-[0_8px_16px_rgba(240,68,91,0.24)] disabled:cursor-not-allowed disabled:opacity-50" > ); } diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 97f1f76..c11a3d6 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -652,7 +652,7 @@ export default function QuestionDetailClient({ icon="close" iconLabel={closeLabel} /> -

+

{item.title}

@@ -709,8 +711,9 @@ export default function QuestionDetailClient({ variant="transparent" icon="close" iconLabel={closeLabel} + exitHref={questionsListHref} /> -

+

{item.title}

diff --git a/src/app/questions-list/sections-request.tsx b/src/app/questions-list/sections-request.tsx index 85d55f2..d588ae7 100644 --- a/src/app/questions-list/sections-request.tsx +++ b/src/app/questions-list/sections-request.tsx @@ -58,7 +58,7 @@ export default function SectionsRequest() { icon={null} title={({ close }) => ( - + Bookings Terms & Conditions + ) : null} ); } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 19fe982..440d615 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -120,9 +120,9 @@ export function Button({ widthClass, "items-center", "justify-center", + "h-[52px]", "rounded-[11px]", "px-4", - countdownLocked ? "py-[9px]" : "py-[14px]", "text-center", "transition-opacity", isOutlined diff --git a/src/components/ui/help-modal.tsx b/src/components/ui/help-modal.tsx new file mode 100644 index 0000000..63b7fb5 --- /dev/null +++ b/src/components/ui/help-modal.tsx @@ -0,0 +1,173 @@ +"use client"; + +import type { ReactNode } from "react"; +import { useCallback, useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import Button from "@/components/ui/button"; +import { useI18n } from "@/translations/provider"; + +const EXIT_ANIMATION_MS = 220; + +export type HelpModalProps = { + isOpen: boolean; + onClose: () => void; + title?: ReactNode; + description?: ReactNode; + buttonText?: ReactNode; +}; + +export function HelpModal({ + isOpen, + onClose, + title, + description, + buttonText, +}: HelpModalProps) { + const { dictionary: t } = useI18n(); + const [isVisible, setIsVisible] = useState(isOpen); + const [isEntering, setIsEntering] = useState(true); + const [isClosing, setIsClosing] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const resolvedTitle = "Tips"; + const resolvedDescription = + description ?? + (t.common as any)?.helpDescription ?? + "Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance."; + const resolvedButtonText = + buttonText ?? (t.common as any)?.gotIt ?? "Got it"; + + const closeSheet = useCallback(() => { + if (isClosing) return; + setIsClosing(true); + }, [isClosing]); + + // Sync with isOpen prop + useEffect(() => { + if (isOpen) { + setIsVisible(true); + setIsClosing(false); + setIsEntering(true); + } + }, [isOpen]); + + // Entry animation + useEffect(() => { + if (!isVisible) return; + const frameId = window.requestAnimationFrame(() => { + setIsEntering(false); + }); + return () => window.cancelAnimationFrame(frameId); + }, [isVisible]); + + // Lock body scroll + useEffect(() => { + if (!isVisible) return; + const previousBodyOverflow = document.body.style.overflow; + const previousHtmlOverflow = document.documentElement.style.overflow; + document.body.style.overflow = "hidden"; + document.documentElement.style.overflow = "hidden"; + return () => { + document.body.style.overflow = previousBodyOverflow; + document.documentElement.style.overflow = previousHtmlOverflow; + }; + }, [isVisible]); + + // Exit animation + useEffect(() => { + if (!isClosing) return; + const timeoutId = window.setTimeout(() => { + setIsVisible(false); + onClose(); + }, EXIT_ANIMATION_MS); + return () => window.clearTimeout(timeoutId); + }, [isClosing, onClose]); + + if (!isVisible || !mounted) return null; + + return createPortal( +
{ + if (e.target === e.currentTarget) closeSheet(); + }} + onKeyDown={(e) => { + if ( + e.target === e.currentTarget && + (e.key === "Escape" || e.key === "Enter" || e.key === " ") + ) { + e.preventDefault(); + closeSheet(); + } + }} + > +
+
+

+ {resolvedTitle} +

+ + {(() => { + const items = Array.isArray(resolvedDescription) + ? resolvedDescription + : typeof resolvedDescription === "string" + ? resolvedDescription + .split(/\.\s+/) + .map((s) => s.replace(/\.$/, "").trim()) + .filter(Boolean) + : null; + + if (items && Array.isArray(items)) { + return ( +
    + {items.map((item, i) => ( +
  • + + {item} +
  • + ))} +
+ ); + } + + return ( +
+ {resolvedDescription} +
+ ); + })()} + +
+ +
+
+
+
, + document.body, + ); +} + +export default HelpModal; diff --git a/src/components/ui/navigation-button.tsx b/src/components/ui/navigation-button.tsx index 2f9278e..4ba8c5b 100644 --- a/src/components/ui/navigation-button.tsx +++ b/src/components/ui/navigation-button.tsx @@ -2,9 +2,11 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; -import type { ButtonHTMLAttributes } from "react"; +import type { ButtonHTMLAttributes, ReactNode } from "react"; +import { useState } from "react"; import { GoArrowLeft } from "react-icons/go"; import { useI18n } from "@/translations/provider"; +import HelpModal from "@/components/ui/help-modal"; type NavigationButtonIcon = | "back" @@ -22,6 +24,10 @@ export type NavigationButtonProps = Omit< icon: NavigationButtonIcon; iconLabel?: string; variant?: NavigationButtonVariant; + helpTitle?: ReactNode; + helpDescription?: ReactNode; + helpButtonText?: ReactNode; + disableHelpModal?: boolean; }; export function NavigationButton({ @@ -30,10 +36,16 @@ export function NavigationButton({ variant = "default", type = "button", className, + helpTitle, + helpDescription, + helpButtonText, + disableHelpModal = false, ...props }: NavigationButtonProps) { const router = useRouter(); const { dictionary: t } = useI18n(); + const [isHelpOpen, setIsHelpOpen] = useState(false); + const iconNode = (() => { switch (icon) { case "back": @@ -93,38 +105,56 @@ export function NavigationButton({ } })(); + const isHelpTrigger = icon === "info" || helpTitle !== undefined || helpDescription !== undefined; + return ( - + }} + className={[ + "inline-flex items-center justify-center rounded-[15px] p-2 cursor-pointer transition-transform active:scale-95", + variant === "transparent" ? "bg-white/20" : "bg-[#FFFFFF]", + className, + ] + .filter(Boolean) + .join(" ")} + > + {iconNode} + + + {isHelpTrigger && !disableHelpModal ? ( + setIsHelpOpen(false)} + title={helpTitle} + description={helpDescription} + buttonText={helpButtonText} + /> + ) : null} + ); } diff --git a/src/components/ui/token-switcher.tsx b/src/components/ui/token-switcher.tsx index 98203cc..51b20f6 100644 --- a/src/components/ui/token-switcher.tsx +++ b/src/components/ui/token-switcher.tsx @@ -7,6 +7,9 @@ import { setClientCookie, getClientCookie } from "@/lib/cookies"; export const FEMALE_TOKEN = "545f61bb3e061ccb9b19f84715eb1b1ed4740331"; export const MALE_TOKEN = "f3a7543b44ef0a713d1ee0d4f7866b3825cf1308"; +export const FEMALE_EMAIL = "m.a.ghorbani01@gmail.com"; +export const MALE_EMAIL = "muhammadamin.ghorbani@gmail.com"; + const TOKEN_COOKIE_NAME = "HABIB_TOKEN"; type TokenSwitcherProps = { @@ -39,11 +42,12 @@ export function TokenSwitcher({ const handleSelectToken = (newToken: string) => { if (typeof window !== "undefined") { - // Clear local storage so answers and cached state from previous user are removed + // Clear local storage and session storage so answers and cached state from previous user are removed try { window.localStorage.clear(); + window.sessionStorage.clear(); } catch (e) { - console.error("Failed to clear localStorage", e); + console.error("Failed to clear storage", e); } setClientCookie(TOKEN_COOKIE_NAME, newToken); @@ -115,8 +119,11 @@ export function TokenSwitcher({ 👨
حساب آقا
-
- f3a7543b...308 +
+ {MALE_EMAIL} +
+
+ {MALE_TOKEN}
@@ -142,8 +149,11 @@ export function TokenSwitcher({ 👩
حساب خانم
-
- 545f61bb...331 +
+ {FEMALE_EMAIL} +
+
+ {FEMALE_TOKEN}
diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index 717b6c3..ee1cdae 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -11,6 +11,9 @@ "required": "Required", "estimateTime": "Estimate time", "rangeError": "The value entered seems incorrect. Please provide a realistic value.", + "help": "Help", + "gotIt": "Got it", + "helpDescription": "Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance.", "finalMatchIntroduce": "Final Match Introduction", "confirmFinalMatch": "Confirm Final Match", "confirmFinalMatchQuestion": "Are you sure you want to officially introduce these two candidates to each other?", @@ -60,7 +63,8 @@ "By identifying your strongest needs, you can better communicate your expectations and build healthier relationships." ] }, - "startMatchFailed": "Sending the match request failed. Please check your connection and try again." + "startMatchFailed": "Sending the match request failed. Please check your connection and try again.", + "moveToEnd": "Move to the End" }, "match": { "title": "New Match", diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index 2d4c57c..ee88a6a 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -10,7 +10,10 @@ "back": "بازگشت", "required": "ضروری", "estimateTime": "زمان تقریبی", - "rangeError": "مقدار وارد شده صحیح به نظر نمی‌رسد. لطفاً یک عدد واقعی وارد کنید." + "rangeError": "مقدار وارد شده صحیح به نظر نمی‌رسد. لطفاً یک عدد واقعی وارد کنید.", + "help": "راهنما", + "gotIt": "متوجه شدم", + "helpDescription": "این بخش به شما کمک می‌کند تا مراحل ثبت‌نام و پاسخ به سوالات را به صورت دقیق و آگاهانه تکمیل نمایید. تمامی اطلاعات شما نزد ما محرمانه باقی می‌ماند." }, "intro": { "imageAlt": "ازدواج آسمانی", @@ -52,7 +55,8 @@ "با شناسایی قوی‌ترین نیازهای خود، می‌توانید انتظارات خود را بهتر بیان کنید و روابط سالم‌تری بسازید." ] }, - "startMatchFailed": "ارسال درخواست مچ انجام نشد. اتصال خود را بررسی کنید و دوباره تلاش کنید." + "startMatchFailed": "ارسال درخواست مچ انجام نشد. اتصال خود را بررسی کنید و دوباره تلاش کنید.", + "moveToEnd": "Move to the End" }, "match": { "title": "گزینه جدید",