Browse Source

feat: implement multi-language infrastructure and dynamic question components for marriage matching system

front-test-2
ghorbani 3 weeks ago
parent
commit
ae1c830517
  1. 8
      src/app/api/dev-reset-profile/route.ts
  2. 10
      src/app/api/proxy/route.ts
  3. 4
      src/app/finding-match/page.tsx
  4. 93
      src/app/globals.css
  5. 51
      src/app/intro/page.tsx
  6. 4
      src/app/page.tsx
  7. 4
      src/app/questions-list/page.tsx
  8. 22
      src/components/Componentes/notice-box.tsx
  9. 11
      src/components/Componentes/question-dropdown.tsx
  10. 8
      src/components/Componentes/question-radio.tsx
  11. 8
      src/components/Componentes/question-section-flow.tsx
  12. 1
      src/components/Componentes/question-snap-list.tsx
  13. 5
      src/components/Componentes/required-steps-card.tsx
  14. 26
      src/components/Componentes/slider-header.tsx
  15. 94
      src/components/Componentes/slider-page.tsx
  16. 6
      src/components/Componentes/slider-slide-five.tsx
  17. 20
      src/components/Componentes/slider-slide-four.tsx
  18. 390
      src/components/Componentes/slider-slide-one.tsx
  19. 19
      src/components/Componentes/slider-slide-three.tsx
  20. 37
      src/components/Componentes/slider-slide-two.tsx
  21. 17
      src/components/Componentes/token-switcher.tsx
  22. 90
      src/components/Componentes/video-player.tsx
  23. 586
      src/data/questions/en.json
  24. 586
      src/data/questions/fa.json
  25. 1
      src/hooks/marriage/use-marriage-config.ts
  26. 22
      src/hooks/marriage/use-section-data.ts
  27. 16
      src/lib/auth-bridge.ts
  28. 22
      src/lib/http.ts

8
src/app/api/dev-reset-profile/route.ts

@ -18,7 +18,8 @@ export async function POST(request: NextRequest) {
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";
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);
@ -36,6 +37,9 @@ export async function POST(request: NextRequest) {
}
} catch (error: any) {
console.error("Failed to reset profile:", error);
return Response.json({ error: error.message || "Internal Server Error" }, { status: 500 });
return Response.json(
{ error: error.message || "Internal Server Error" },
{ status: 500 },
);
}
}

10
src/app/api/proxy/route.ts

@ -131,7 +131,7 @@ function getRequestHeaders(request: NextRequest, targetUrl: URL) {
getCookieValue(cookieHeader, "token") ??
getCookieValue(cookieHeader, "auth_token");
if (cookieToken) {
if (cookieToken && cookieToken !== "NO_TOKEN") {
headers.set("authorization", `Token ${cookieToken}`);
}
}
@ -141,6 +141,14 @@ function getRequestHeaders(request: NextRequest, targetUrl: URL) {
headers.set("authorization", `Token ${authKey}`);
}
// Fall back to the development default token when nothing else provided auth.
if (!headers.has("authorization")) {
const defaultToken = process.env.NEXT_PUBLIC_DEFAULT_TOKEN;
if (defaultToken && defaultToken !== "NO_TOKEN") {
headers.set("authorization", `Token ${defaultToken}`);
}
}
// Dynamically set language headers
let lang =
getCookieValue(cookieHeader, "HABIB_LANGUAGE") ??

4
src/app/finding-match/page.tsx

@ -89,7 +89,9 @@ export default function FindingMatchPage() {
role="status"
>
<FaLock aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span className="group-16 leading-none font-semibold">{t.requestSent.profileLocked}</span>
<span className="group-16 leading-none font-semibold">
{t.requestSent.profileLocked}
</span>
</div>
) : (
<Link

93
src/app/globals.css

@ -8,67 +8,67 @@
--safe-right: 0px;
/* ─── Semantic Tokens (Light) ─── */
--semantic-neutral-bg: #F1F5F9;
--semantic-neutral-bg: #f1f5f9;
--semantic-neutral-text: #475569;
--semantic-neutral-border: #CBD5E1;
--semantic-neutral-icon: #64748B;
--semantic-neutral-border: #cbd5e1;
--semantic-neutral-icon: #64748b;
--semantic-info-bg: #EFF6FF;
--semantic-info-text: #1D4ED8;
--semantic-info-border: #BFDBFE;
--semantic-info-icon: #2563EB;
--semantic-info-bg: #eff6ff;
--semantic-info-text: #1d4ed8;
--semantic-info-border: #bfdbfe;
--semantic-info-icon: #2563eb;
--semantic-success-bg: #ECFDF5;
--semantic-success-bg: #ecfdf5;
--semantic-success-text: #047857;
--semantic-success-border: #A7F3D0;
--semantic-success-border: #a7f3d0;
--semantic-success-icon: #059669;
--semantic-warning-bg: #FFFBEB;
--semantic-warning-text: #B45309;
--semantic-warning-border: #FDE68A;
--semantic-warning-icon: #D97706;
--semantic-warning-bg: #fffbeb;
--semantic-warning-text: #b45309;
--semantic-warning-border: #fde68a;
--semantic-warning-icon: #d97706;
--semantic-danger-bg: #FEF2F2;
--semantic-danger-text: #B91C1C;
--semantic-danger-border: #FECACA;
--semantic-danger-icon: #DC2626;
--semantic-danger-bg: #fef2f2;
--semantic-danger-text: #b91c1c;
--semantic-danger-border: #fecaca;
--semantic-danger-icon: #dc2626;
--semantic-ai-bg: #F5F3FF;
--semantic-ai-text: #6D28D9;
--semantic-ai-border: #DDD6FE;
--semantic-ai-icon: #7C3AED;
--semantic-ai-bg: #f5f3ff;
--semantic-ai-text: #6d28d9;
--semantic-ai-border: #ddd6fe;
--semantic-ai-icon: #7c3aed;
}
.dark {
--semantic-neutral-bg: #1E293B;
--semantic-neutral-text: #E2E8F0;
--semantic-neutral-bg: #1e293b;
--semantic-neutral-text: #e2e8f0;
--semantic-neutral-border: #475569;
--semantic-neutral-icon: #CBD5E1;
--semantic-neutral-icon: #cbd5e1;
--semantic-info-bg: #172554;
--semantic-info-text: #BFDBFE;
--semantic-info-border: #1D4ED8;
--semantic-info-icon: #93C5FD;
--semantic-info-text: #bfdbfe;
--semantic-info-border: #1d4ed8;
--semantic-info-icon: #93c5fd;
--semantic-success-bg: #052E2B;
--semantic-success-text: #A7F3D0;
--semantic-success-bg: #052e2b;
--semantic-success-text: #a7f3d0;
--semantic-success-border: #047857;
--semantic-success-icon: #6EE7B7;
--semantic-warning-bg: #451A03;
--semantic-warning-text: #FDE68A;
--semantic-warning-border: #B45309;
--semantic-warning-icon: #FCD34D;
--semantic-danger-bg: #450A0A;
--semantic-danger-text: #FECACA;
--semantic-danger-border: #B91C1C;
--semantic-danger-icon: #FCA5A5;
--semantic-ai-bg: #2E1065;
--semantic-ai-text: #DDD6FE;
--semantic-ai-border: #7C3AED;
--semantic-ai-icon: #C4B5FD;
--semantic-success-icon: #6ee7b7;
--semantic-warning-bg: #451a03;
--semantic-warning-text: #fde68a;
--semantic-warning-border: #b45309;
--semantic-warning-icon: #fcd34d;
--semantic-danger-bg: #450a0a;
--semantic-danger-text: #fecaca;
--semantic-danger-border: #b91c1c;
--semantic-danger-icon: #fca5a5;
--semantic-ai-bg: #2e1065;
--semantic-ai-text: #ddd6fe;
--semantic-ai-border: #7c3aed;
--semantic-ai-icon: #c4b5fd;
}
@theme inline {
@ -174,7 +174,8 @@ html:lang(ar) body,
[dir="rtl"].font-faminela,
[dir="rtl"].font-ryling,
.font-faminela {
font-family: var(--font-faminela-local), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
font-family:
var(--font-faminela-local), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.app-shell {

51
src/app/intro/page.tsx

@ -13,6 +13,7 @@ import { authBridge } from "@/lib/auth-bridge";
import { getSubmitPath } from "@/lib/get-submit-path";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
import VideoPlayer from "@/components/Componentes/video-player";
const REDIRECT_SESSION_KEY = "redirect";
@ -114,7 +115,11 @@ export default function Intro() {
);
}
const nextPath = localizePath(getSubmitPath(profileResponse), locale);
const submitPath = getSubmitPath(profileResponse);
const nextPath = localizePath(
submitPath === "/intro" ? "/terms" : submitPath,
locale,
);
router.push(nextPath);
} catch (error) {
console.error("Submission/redirect failed", error);
@ -206,7 +211,10 @@ export default function Intro() {
onClick={() => setIsPlayerOpen(true)}
>
<Image
src={config?.intro_video_thumbnail_url || "/assets/images/Frame 2095586523.png"}
src={
config?.intro_video_thumbnail_url ||
"/assets/images/Frame 2095586523.png"
}
alt={t.intro.videoAlt}
fill
sizes="344px"
@ -223,42 +231,11 @@ export default function Intro() {
/>
</div>
{isPlayerOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm p-4">
<div className="relative w-full max-w-[640px] aspect-video rounded-2xl overflow-hidden bg-black shadow-2xl">
<button
onClick={(e) => {
e.stopPropagation();
setIsPlayerOpen(false);
}}
className="absolute top-4 right-4 z-10 flex items-center justify-center w-10 h-10 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors active:scale-95 cursor-pointer"
aria-label="Close video player"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
<VideoPlayer
isOpen={isPlayerOpen}
onClose={() => setIsPlayerOpen(false)}
videoUrl={config?.intro_video_url}
/>
</svg>
</button>
<video
src={config?.intro_video_url || "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"}
controls
autoPlay
playsInline
className="w-full h-full object-contain"
/>
</div>
</div>
)}
<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(20px + var(--safe-bottom))" }}

4
src/app/page.tsx

@ -6,7 +6,9 @@ export const dynamic = "force-dynamic";
export default async function RootPage() {
const cookieStore = await cookies();
const habibLanguage = cookieStore.get("HABIB_LANGUAGE")?.value ?? cookieStore.get("habib_language")?.value;
const habibLanguage =
cookieStore.get("HABIB_LANGUAGE")?.value ??
cookieStore.get("habib_language")?.value;
let targetLocale = defaultLocale;

4
src/app/questions-list/page.tsx

@ -45,7 +45,6 @@ export default function QuestionsListPage() {
refetchOnMount: "always",
});
const startMatchMutation = useStartMarriageMatchMutation({
onSuccess: () => {
markMatchStarted();
@ -134,8 +133,7 @@ export default function QuestionsListPage() {
const profileStatus = profile?.status;
const isProfileSuspended = profileStatus === "suspended";
const isProfileBlocked =
profileStatus === "in_case" ||
profileStatus === "matched";
profileStatus === "in_case" || profileStatus === "matched";
const canStartMatch =
!isProfileSuspended && !isProfileBlocked && allRequiredSectionsCompleted;
const [isSyncError, setIsSyncError] = useState(false);

22
src/components/Componentes/notice-box.tsx

@ -4,19 +4,35 @@ type NoticeBoxProps = {
children?: ReactNode;
text?: string;
className?: string;
align?: "center" | "justify" | "left" | "right";
};
export function NoticeBox({ children, text, className = "" }: NoticeBoxProps) {
export function NoticeBox({
children,
text,
className = "",
align = "center",
}: NoticeBoxProps) {
const alignClass =
align === "justify"
? "text-justify"
: align === "left"
? "text-left"
: align === "right"
? "text-right"
: "text-center";
return (
<div
className={[
"mt-3 bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl text-center",
"mt-3 bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl",
alignClass,
className,
]
.filter(Boolean)
.join(" ")}
>
<p className="text-[#F14B46] group-12 font-semibold text-center leading-[1.6]">
<p className={`text-[#F14B46] group-12 font-semibold leading-[1.6] ${alignClass}`}>
{text || children}
</p>
</div>

11
src/components/Componentes/question-dropdown.tsx

@ -216,8 +216,7 @@ export function QuestionDropdown({
);
if (hasOther) {
nextValue = selectedList.filter(
(v) =>
v !== otherOptionName && !v.startsWith(`${otherOptionName}: `),
(v) => v !== otherOptionName && !v.startsWith(`${otherOptionName}: `),
);
} else {
nextValue = [...selectedList, otherOptionName];
@ -311,8 +310,12 @@ export function QuestionDropdown({
isPersonalityTraits
? (t.questions.writeOtherTraits ?? "Write other options...")
: isSmokingSubstances
? (locale === "fa" ? "خط قرمزهای دیگر را بنویسید..." : "Write other red lines...")
: (locale === "fa" ? "سرگرمی‌ها یا علایق دیگر را بنویسید..." : "Write other hobbies or interests...")
? locale === "fa"
? "خط قرمزهای دیگر را بنویسید..."
: "Write other red lines..."
: locale === "fa"
? "سرگرمی‌ها یا علایق دیگر را بنویسید..."
: "Write other hobbies or interests..."
}
className="h-[48px] w-full rounded-[14px] border border-[#D0D5DD] bg-white px-4 text-[14px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F]"
/>

8
src/components/Componentes/question-radio.tsx

@ -86,8 +86,8 @@ export function QuestionRadio({
)}
</div>
)}
{option.includes(" - ")
? (() => {
{option.includes(" - ") ? (
(() => {
const parts = option.split(" - ");
const title = parts[0];
const description = parts.slice(1).join(" - ");
@ -104,7 +104,9 @@ export function QuestionRadio({
</span>
);
})()
: <span className="flex-1">{option}</span>}
) : (
<span className="flex-1">{option}</span>
)}
</label>
);
})}

8
src/components/Componentes/question-section-flow.tsx

@ -149,7 +149,13 @@ function SectionFlowContent({
clearTimeout(timerId);
}
};
}, [isCompleted, isLeaving, activeQuestionIndex, questions?.length, handleContinue]);
}, [
isCompleted,
isLeaving,
activeQuestionIndex,
questions?.length,
handleContinue,
]);
const age = getStoredAge();
const activeQuestion = questions?.[activeQuestionIndex];

1
src/components/Componentes/question-snap-list.tsx

@ -410,7 +410,6 @@ export function QuestionSnapList({
{firstQuestionHint}
</div>
) : null}
</section>
);
}

5
src/components/Componentes/required-steps-card.tsx

@ -2,10 +2,7 @@
import { useMemo } from "react";
import { IoAlert, IoCheckmark } from "react-icons/io5";
import {
getLocalSectionProgress,
getStoredAge,
} from "./progress-helper";
import { getLocalSectionProgress, getStoredAge } from "./progress-helper";
import {
getQuestionListItems,
isQuestionListItemVisibleForProfile,

26
src/components/Componentes/slider-header.tsx

@ -0,0 +1,26 @@
import type { ReactNode } from "react";
type SliderHeaderProps = {
index: number;
title: ReactNode;
totalSteps?: number;
stepLabel?: string;
};
export function SliderHeader({
index,
title,
totalSteps = 5,
stepLabel,
}: SliderHeaderProps) {
return (
<div className="text-center">
<p className="text-[#747474] group-10 font-semibold">
{stepLabel || `Step ${index + 1} of ${totalSteps}`}
</p>
<p className="text-[#111111] group-16 font-bold">{title}</p>
</div>
);
}
export default SliderHeader;

94
src/components/Componentes/slider-page.tsx

@ -3,58 +3,35 @@
import { useRouter } from "next/navigation";
import type { TouchEvent } from "react";
import { useEffect, useState } from "react";
import type {
GenderAnswer,
RegistrationAnswer,
} from "./slider-slide";
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 { SliderSlideOne, SliderSlideOneActions } from "./slider-slide-one";
import { SliderSlideThree } from "./slider-slide-three";
import { SliderSlideTwo } from "./slider-slide-two";
import Button from "./button";
import NavigationButton from "./navigation-button";
import { useUpdateMarriageProfileBasicMutation } from "@/hooks/marriage/use-profile-basic";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
const BASE_SLIDE_COUNT = 4;
const FINAL_SLIDE_COUNT = 5;
const SWIPE_THRESHOLD = 40;
const SLIDER_ANSWERS_STORAGE_KEY = "marriage-slider-answers";
type SavedSliderAnswers = {
gender: GenderAnswer;
registration: RegistrationAnswer;
};
const isGenderAnswer = (value: unknown): value is GenderAnswer =>
value === "man" || value === "woman";
const isRegistrationAnswer = (value: unknown): value is RegistrationAnswer =>
value === "self" || value === "other";
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>("other");
useState<RegistrationAnswer>("self");
const [hasReadRules, setHasReadRules] = useState(false);
const [touchStartX, setTouchStartX] = useState<number | null>(null);
const updateProfileBasicMutation = useUpdateMarriageProfileBasicMutation();
const hasFinalNotice = selectedGender === "woman";
const maxSlideIndex = hasFinalNotice
? FINAL_SLIDE_COUNT - 1
: BASE_SLIDE_COUNT - 1;
const displaySlideCount =
hasFinalNotice && activeSlide === FINAL_SLIDE_COUNT - 1
? FINAL_SLIDE_COUNT
: BASE_SLIDE_COUNT;
const hasFinalNotice = true;
const maxSlideIndex = FINAL_SLIDE_COUNT - 1;
const displaySlideCount = FINAL_SLIDE_COUNT;
const navDotCount = displaySlideCount;
useEffect(() => {
@ -64,12 +41,6 @@ export default function SliderPage() {
}
}, []);
useEffect(() => {
if (!hasFinalNotice && activeSlide >= FINAL_SLIDE_COUNT - 1) {
setActiveSlide(BASE_SLIDE_COUNT - 1);
}
}, [activeSlide, hasFinalNotice]);
const goToSlide = (index: number) => {
setActiveSlide(Math.max(0, Math.min(index, maxSlideIndex)));
};
@ -83,6 +54,26 @@ export default function SliderPage() {
};
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",
@ -91,11 +82,8 @@ export default function SliderPage() {
} catch (error) {
console.warn("Failed to update profile basic details:", error);
} finally {
if (typeof window !== "undefined") {
window.localStorage.removeItem(SLIDER_ANSWERS_STORAGE_KEY);
}
// Action Item 3: Immediately direct user into Card 1 (personal_info) after onboarding
router.push(localizePath("/questions-list/personal_info", locale));
clearTimeout(timeout);
navigateAway();
}
};
@ -165,7 +153,10 @@ export default function SliderPage() {
className="flex h-full w-full transition-transform duration-300 ease-out"
style={{ transform: `translateX(-${activeSlide * 100}%)` }}
>
<SliderSlideOne index={0} />
<SliderSlideOne
index={0}
onScrollToEnd={() => setHasReadRules(true)}
/>
<SliderSlideTwo index={1} />
<SliderSlideThree
index={2}
@ -188,12 +179,9 @@ export default function SliderPage() {
>
<div className="pointer-events-auto">
{activeSlide === 0 ? (
<SliderSlideOneActions onAccept={goToNextSlide} />
) : activeSlide === BASE_SLIDE_COUNT - 1 ? (
<SliderFinalActions
onBack={goToPreviousSlide}
isFinishing={updateProfileBasicMutation.isPending}
onFinish={hasFinalNotice ? goToNextSlide : completeSlider}
<SliderSlideOneActions
onAccept={goToNextSlide}
disabled={!hasReadRules}
/>
) : activeSlide === maxSlideIndex ? (
<SliderFinalActions

6
src/components/Componentes/slider-slide-five.tsx

@ -1,3 +1,4 @@
import { SliderHeader } from "./slider-header";
import type { SliderSlideProps } from "./slider-slide";
const NOTICE_ITEMS = [
@ -16,10 +17,7 @@ export function SliderSlideFive({ index }: SliderSlideProps) {
className="flex h-full min-h-0 w-full shrink-0 flex-col"
>
<div className="min-h-0 flex-1 overflow-y-auto pb-28">
<div className="text-center">
<p className="text-[#747474] text-xs font-semibold">Submit Process</p>
<p className="text-[#111111] font-bold">5/5 Final Notice</p>
</div>
<SliderHeader index={index} totalSteps={5} title="Final Notice" />
<div className="mt-6 space-y-5">
<p className="text-[#36363C] text-sm font-semibold leading-5">

20
src/components/Componentes/slider-slide-four.tsx

@ -1,10 +1,8 @@
"use client";
import type {
RegistrationAnswer,
SliderSlideProps,
} from "./slider-slide";
import NoticeBox from "./notice-box";
import { SliderHeader } from "./slider-header";
import type { RegistrationAnswer, SliderSlideProps } from "./slider-slide";
type SliderSlideFourProps = SliderSlideProps & {
selectedRegistration: RegistrationAnswer;
@ -27,17 +25,9 @@ export function SliderSlideFour({
className="flex h-full min-h-0 w-full shrink-0 flex-col"
>
<div className="min-h-0 flex-1 overflow-y-auto pb-28">
<div className="text-center">
<p className="text-[#747474] group-10 font-semibold">
Submit Process
</p>
<p className="text-[#111111] group-16 font-bold">
{index + 1}/4 Registration Type
</p>
</div>
<NoticeBox className="mt-8">
Read the terms and conditions carefully so that you don't run into any
problems in the next process - reading this section is mandatory.
<SliderHeader index={index} title="Registration Type" />
<NoticeBox className="mt-8" align="justify">
You can register both for yourself and for your loved ones, but obtaining the explicit consent and permission of the person you are creating the profile for is mandatory. To ensure accuracy, it is best to let the individual complete the subsequent steps themselves. Remember that all matches are introduced with absolute privacy and strictly based on the information you provide.
</NoticeBox>
<div className="flex min-h-[calc(100dvh-520px)] items-center justify-center pt-6">
<div className="w-full max-w-[440px] space-y-4">

390
src/components/Componentes/slider-slide-one.tsx

@ -1,16 +1,57 @@
import type { SliderSlideProps } from "./slider-slide";
"use client";
import { useEffect, useRef } from "react";
import Button from "./button";
import { SliderHeader } from "./slider-header";
import type { SliderSlideProps } from "./slider-slide";
const ACTION_AREA_HEIGHT = 80;
const CONTENT_MASK = `linear-gradient(to bottom, black 0, black calc(100% - ${ACTION_AREA_HEIGHT}px), transparent calc(100% - ${ACTION_AREA_HEIGHT}px), transparent 100%)`;
export function SliderSlideOne({ index }: SliderSlideProps) {
type SliderSlideOneProps = SliderSlideProps & {
onScrollToEnd?: () => void;
};
export function SliderSlideOne({ index, onScrollToEnd }: SliderSlideOneProps) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const checkScroll = () => {
const tolerance = 10;
const isBottom =
container.scrollHeight - container.scrollTop <=
container.clientHeight + tolerance;
if (isBottom) {
onScrollToEnd?.();
}
};
// Check initially
checkScroll();
container.addEventListener("scroll", checkScroll);
window.addEventListener("resize", checkScroll);
// Check after short delay for dynamically rendered container layout
const timer = setTimeout(checkScroll, 150);
return () => {
container.removeEventListener("scroll", checkScroll);
window.removeEventListener("resize", checkScroll);
clearTimeout(timer);
};
}, [onScrollToEnd]);
return (
<section
aria-label={`Slide ${index + 1}`}
className="flex h-full min-h-0 w-full shrink-0 flex-col"
>
<div
ref={containerRef}
className="min-h-0 flex-1 overflow-y-auto"
style={{
paddingBottom: ACTION_AREA_HEIGHT,
@ -18,98 +59,370 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
maskImage: CONTENT_MASK,
}}
>
<div className="text-center">
<p className="text-[#747474] group-10 font-semibold">
Submit Process
</p>
<p className="text-[#111111] group-16 font-bold">
{index + 1}/4 terms & conditions
</p>
</div>
<SliderHeader index={index} title="Terms & Conditions" />
<div className="bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl mt-8">
<p className="text-[#F14B46] group-12 font-semibold text-center">
Dear user, while welcoming you to the Habib Marij application,
please read the following rules and regulations carefully before
using the services of this platform. Your registration and use of
this application constitutes full acceptance of these rules.
Welcome to Habib Marriage. Please review our terms of service before
proceeding. By registering and using this platform, you agree to
these terms
</p>
</div>
<div className="bg-white rounded-2xl mt-4 overflow-y-auto shadow-xs">
<div className="space-y-6 px-5 py-5">
<div className="space-y-3.5">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
1. Eligibility and Membership Requirements
1. Acceptance of Rules and Platform Goal
</h3>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Full Acceptance of Agreement:
</span>{" "}
<span className="font-normal text-[#525252]">
Registration and use of any part of the application means
reading and fully accepting all rules. If you do not agree,
you must refrain from further use.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Changes and Updates:
</span>{" "}
<span className="font-normal text-[#525252]">
The application reserves the right to edit the rules when
necessary, and your continued use will constitute acceptance
of the new changes.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Exclusive Goal of the Application:
</span>{" "}
<span className="font-normal text-[#525252]">
This platform is designed solely for matchmaking and permanent
marriage. Use with the purpose of dating, temporary
relationships, or entertainment is prohibited.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Age and Legal Capacity Requirement:
</span>{" "}
<span className="font-normal text-[#525252]">
Use of services is allowed only for individuals over 18 years
of age who have the legal and religious capacity for marriage.
</span>
</p>
</div>
<div className="space-y-3.5 pt-1">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
2. User Account, Security, and Authentication
</h3>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Accuracy of Personal Information:
</span>{" "}
<span className="font-normal text-[#525252]">
The user is obliged to enter all identity, occupational,
educational, and marital status information completely
accurately and honestly.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Prohibition of Fake Accounts:
</span>{" "}
<span className="font-normal text-[#525252]">
Creating an account using the identity, name, mobile number,
or photos of others, as well as creating multiple user
accounts, is a serious violation.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Maintaining Account Security:
</span>{" "}
<span className="font-normal text-[#525252]">
The responsibility for maintaining the confidentiality of the
password and login code lies with the user, and transferring
or renting the account to another person is prohibited.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Introducing a Trusted Person:
</span>{" "}
<span className="font-normal text-[#525252]">
To enhance security, users can introduce a trusted family
member. This information is used strictly for security and
urgent cases.
</span>
</p>
</div>
<div className="space-y-3.5 pt-1">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
3. Communications, Ethics, and User Behavior
</h3>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Mutual Respect:
</span>{" "}
<span className="font-normal text-[#525252]">
Conversations must be ethical and purposeful. Sending
offensive, sexual, obscene, threatening, or harassing messages
is strictly prohibited.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Respecting Others&apos; Privacy:
</span>{" "}
<span className="font-normal text-[#525252]">
Saving, sending, or publishing screenshots of conversations
and photos of other users outside the application is a
violation of privacy.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Absolute Prohibition of Financial Requests:
</span>{" "}
<span className="font-normal text-[#525252]">
Requesting money, loans, gift cards, cryptocurrency, or any
financial benefit from other users under any pretext is
prohibited.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Prohibition of Commercial Activity:
</span>{" "}
<span className="font-normal text-[#525252]">
Any advertising, marketing, selling goods, or raising capital
within the application environment is considered a violation.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Respectful End of Conversation:
</span>{" "}
<span className="font-normal text-[#525252]">
If the other party does not wish to continue the
communication, the user is obliged to respect their decision
and refrain from pursuing or causing harassment.
</span>
</p>
</div>
<div className="space-y-3.5 pt-1">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
4. Privacy, Data, and Intellectual Property
</h3>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Personal Information Protection:
</span>{" "}
<span className="font-normal text-[#525252]">
The application is committed to protecting your sensitive
information in accordance with the privacy policy and will not
sell it to third parties without permission.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Public Profile Display:
</span>{" "}
<span className="font-normal text-[#525252]">
By completing the profile, the user consents to their public
information being displayed to other qualified users for
service delivery.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Account Deletion Rules:
</span>{" "}
<span className="font-normal text-[#525252]">
Upon requesting account deletion, your information is removed
from public view, but necessary data for legal and security
requirements will remain archived in the system.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Intellectual Property Rights:
</span>{" "}
<span className="font-normal text-[#525252]">
All rights to software codes, design, database, and brand name
belong to the platform, and any copying or scraping is
prohibited.
</span>
</p>
</div>
<div className="space-y-3.5 pt-1">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
5. Financial Terms and Subscriptions
</h3>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Legal Age:</span>{" "}
<span className="font-bold text-[#262626]">
Nature of Paid Services:
</span>{" "}
<span className="font-normal text-[#525252]">
Users must meet the minimum legal age for independent
registration.
Payment is solely for using premium features and software
infrastructure and does not provide any guarantee for marriage
or finding a spouse.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Refund Policy:</span>{" "}
<span className="font-normal text-[#525252]">
Paid fees are non-refundable, except in definitive cases of
system technical errors.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Identity Verification:
Disclaimer for External Transactions:
</span>{" "}
<span className="font-normal text-[#525252]">
Mandatory submission of valid government-issued ID upon
registration.
The application holds no responsibility for financial
transactions between users or outside the application&apos;s
official payment gateway.
</span>
</p>
</div>
<div className="space-y-3.5 pt-1">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
6. Monitoring, Violations, and Legal Prosecution
</h3>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Single Status Commitment:
Confidential Reporting:
</span>{" "}
<span className="font-normal text-[#525252]">
Users must provide proof of being single, or documents
confirming divorce or spouse&apos;s death upon request.
Users must report violations, suspicious behavior, or
financial requests through the report violation option. The
identity of the reporter remains confidential.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Intent:</span>{" "}
<span className="font-bold text-[#262626]">
Authorities of Application Moderators:
</span>{" "}
<span className="font-normal text-[#525252]">
Commitment to monogamy and intention for permanent marriage
only (no simultaneous or temporary relationships).
The platform reserves the right to block, restrict, or suspend
the user account without prior notice if any violation or
breach of rules is detected.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
General Health:
Cooperation with Law Enforcement:
</span>{" "}
<span className="font-normal text-[#525252]">
Self-declaration regarding mental health, absence of
addiction, and no criminal record.
In the event of cybercrimes, fraud, or threats, the
application will refer the necessary information to judicial
and law enforcement authorities.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Dispute Resolution:
</span>{" "}
<span className="font-normal text-[#525252]">
Any legal dispute must first be pursued through application
support. If no agreement is reached, the competent authority
will be the courts of the city where the application is
registered.
</span>
</p>
</div>
<div className="space-y-3.5 pt-1">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
2. Privacy and Data Management
7. Disclaimer and In-Person Meetings
</h3>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Content Security:
Role of the Application:
</span>{" "}
<span className="font-normal text-[#525252]">
This platform is only a facilitator for initial communication
and does not replace family background research, counseling,
and pre-marital investigations.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Responsibility for Accuracy of Claims:
</span>{" "}
<span className="font-normal text-[#525252]">
Accurate verification of users&apos; claims regarding their
financial, occupational, marital, and health status is the
responsibility of the user and their family.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Responsibility for In-Person Meetings:
</span>{" "}
<span className="font-normal text-[#525252]">
Planning and deciding to meet in person is entirely the
responsibility of the users, and the application assumes no
liability for any incidents or potential damages.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Safety Recommendations in Meetings:
</span>{" "}
<span className="font-normal text-[#525252]">
Technical prevention of screenshots from profiles and chat
environments.
It is strongly recommended that initial meetings be held in
crowded public places, and that a family member or a trusted
person be kept informed.
</span>
</p>
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">
Progressive Disclosure:
Force Majeure Conditions:
</span>{" "}
<span className="font-normal text-[#525252]">
Sensitive information (face, contact details) revealed
step-by-step only with mutual consent.
In the event of occurrences beyond control, such as a
nationwide internet outage, natural disasters, or sanctions,
the application will have no liability for temporary service
outages.
</span>
</p>
</div>
@ -122,10 +435,12 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
type SliderSlideOneActionsProps = {
onAccept: () => void;
disabled?: boolean;
};
export function SliderSlideOneActions({
onAccept,
disabled,
}: SliderSlideOneActionsProps) {
return (
<div className="flex items-center gap-3 [&>a]:flex-1 [&>button]:flex-1">
@ -137,6 +452,7 @@ export function SliderSlideOneActions({
countdownSeconds={0}
onClick={onAccept}
className="flex-1"
disabled={disabled}
>
Accept
</Button>

19
src/components/Componentes/slider-slide-three.tsx

@ -1,10 +1,8 @@
"use client";
import Image from "next/image";
import type {
GenderAnswer,
SliderSlideProps,
} from "./slider-slide";
import { SliderHeader } from "./slider-header";
import type { GenderAnswer, SliderSlideProps } from "./slider-slide";
type GenderOption = {
id: GenderAnswer;
@ -17,14 +15,14 @@ type GenderOption = {
const GENDER_OPTIONS: GenderOption[] = [
{
id: "man",
label: "Submit Man",
label: "Man",
alt: "man",
enabledSrc: "/assets/images/Group 27033.svg",
disabledSrc: "/assets/images/disabled Group 27033.svg",
},
{
id: "woman",
label: "Submit Woman",
label: "Woman",
alt: "woman",
enabledSrc: "/assets/images/enabled Group 27032.svg",
disabledSrc: "/assets/images/Group 27032.svg",
@ -46,14 +44,9 @@ export function SliderSlideThree({
aria-label={`Slide ${index + 1}`}
className="flex h-full min-h-0 w-full shrink-0 flex-col justify-between"
>
<div className="text-center">
<p className="text-[#747474] group-10 font-semibold">Submit Process</p>
<p className="text-[#111111] group-16 font-bold">
{index + 1}/4 Select Gender
</p>
</div>
<SliderHeader index={index} title="Select your gender" />
<div className="flex items-center justify-evenly">
<div className="flex w-full items-center justify-evenly">
{GENDER_OPTIONS.map((option) => {
const isSelected = option.id === selectedGender;

37
src/components/Componentes/slider-slide-two.tsx

@ -1,36 +1,44 @@
"use client";
import Image from "next/image";
import { useState } from "react";
import { useMarriageConfigQuery } from "@/hooks/marriage/use-marriage-config";
import { SliderHeader } from "./slider-header";
import type { SliderSlideProps } from "./slider-slide";
import VideoPlayer from "./video-player";
export function SliderSlideTwo({ index }: SliderSlideProps) {
const [isPlayerOpen, setIsPlayerOpen] = useState(false);
const { data: config } = useMarriageConfigQuery();
return (
<section
aria-label={`Slide ${index + 1}`}
className="flex h-full min-h-0 w-full shrink-0 flex-col"
>
<div className="min-h-0 flex-1 overflow-y-auto pb-[86px]">
<div className="text-center">
<p className="text-[#747474] group-10 font-semibold">
Submit Process
</p>
<p className="text-[#111111] group-16 font-bold">
{index + 1}/4 Watch Video
</p>
</div>
<div className="mt-8 flex min-h-0 justify-center">
<SliderHeader index={index} title="Watch Video" />
<div
className="mt-8 flex min-h-0 justify-center cursor-pointer group"
onClick={() => setIsPlayerOpen(true)}
>
<div className="relative w-full max-w-[344px]">
<Image
src={"/assets/images/Frame 20953586523.png"}
src={
config?.intro_video_thumbnail_url ||
"/assets/images/Frame 20953586523.png"
}
alt="video"
width={344}
height={488}
className="h-auto w-full"
className="h-auto w-full transition-transform duration-300 group-hover:scale-102 rounded-2xl"
/>
<Image
src={"/assets/images/Frame 1116607280.svg"}
alt="play"
width={68}
height={68}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transition-transform duration-300 group-hover:scale-110 active:scale-95"
/>
</div>
</div>
@ -53,6 +61,11 @@ export function SliderSlideTwo({ index }: SliderSlideProps) {
</p>
</div>
</div>
<VideoPlayer
isOpen={isPlayerOpen}
onClose={() => setIsPlayerOpen(false)}
videoUrl={config?.video_step_2_url}
/>
</section>
);
}

17
src/components/Componentes/token-switcher.tsx

@ -98,7 +98,12 @@ export function TokenSwitcher({
}
// 3. Set the target token in the cookies and redirect to /
const targetToken = userId === 17119 ? MALE_TOKEN : userId === 147714 ? FEMALE_TOKEN : "NO_TOKEN";
const targetToken =
userId === 17119
? MALE_TOKEN
: userId === 147714
? FEMALE_TOKEN
: "NO_TOKEN";
setClientCookie(TOKEN_COOKIE_NAME, targetToken);
setClientCookie("habib_token", targetToken);
@ -134,7 +139,15 @@ export function TokenSwitcher({
.join(" ")}
>
<MdOutlineSwitchAccount className="size-5 text-rose-500" />
<span>{isMale ? "آقا 👨" : isFemale ? "خانم 👩" : isNoToken ? "بدون توکن 👤" : "توکن 🔑"}</span>
<span>
{isMale
? "آقا 👨"
: isFemale
? "خانم 👩"
: isNoToken
? "بدون توکن 👤"
: "توکن 🔑"}
</span>
</button>
{isOpen && (

90
src/components/Componentes/video-player.tsx

@ -0,0 +1,90 @@
"use client";
import { useEffect, useState, type FC } from "react";
import { createPortal } from "react-dom";
type VideoPlayerProps = {
isOpen: boolean;
onClose: () => void;
videoUrl?: string;
};
export const VideoPlayer: FC<VideoPlayerProps> = ({
isOpen,
onClose,
videoUrl,
}) => {
const [mounted, setMounted] = useState(false);
// Default to a responsive landscape layout that adapts nicely
const [containerStyle, setContainerStyle] = useState(
"relative w-full max-w-[640px] aspect-video rounded-2xl overflow-hidden bg-black shadow-2xl transition-all duration-300 ease-out",
);
useEffect(() => {
setMounted(true);
}, []);
const handleLoadedMetadata = (e: React.SyntheticEvent<HTMLVideoElement>) => {
const video = e.currentTarget;
if (video.videoWidth && video.videoHeight) {
const ratio = video.videoWidth / video.videoHeight;
if (ratio < 0.8) {
// Vertical/portrait video (mobile format like TikTok/selfie)
setContainerStyle(
"relative w-full max-w-[340px] sm:max-w-[380px] aspect-[9/16] rounded-2xl overflow-hidden bg-black shadow-2xl max-h-[82vh] transition-all duration-300 ease-out",
);
} else {
// Horizontal/landscape video (standard format)
setContainerStyle(
"relative w-full max-w-[640px] sm:max-w-[720px] md:max-w-[800px] aspect-video rounded-2xl overflow-hidden bg-black shadow-2xl max-h-[85vh] transition-all duration-300 ease-out",
);
}
}
};
if (!isOpen || !mounted) return null;
return createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/85 backdrop-blur-xs p-4"
onClick={onClose}
>
<div className={containerStyle} onClick={(e) => e.stopPropagation()}>
<button
onClick={onClose}
className="absolute top-3 right-3 sm:top-4 sm:right-4 z-10 flex items-center justify-center w-11 h-11 rounded-full bg-black/40 hover:bg-black/60 text-white transition-all active:scale-95 cursor-pointer backdrop-blur-xs"
aria-label="Close video player"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<video
src={
videoUrl ||
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
}
controls
autoPlay
playsInline
onLoadedMetadata={handleLoadedMetadata}
className="w-full h-full object-contain"
/>
</div>
</div>,
document.body,
);
};
export default VideoPlayer;

586
src/data/questions/en.json
File diff suppressed because it is too large
View File

586
src/data/questions/fa.json
File diff suppressed because it is too large
View File

1
src/hooks/marriage/use-marriage-config.ts

@ -6,6 +6,7 @@ import { http } from "@/lib/http";
export type MarriageConfig = {
intro_video_url: string;
intro_video_thumbnail_url: string;
video_step_2_url?: string;
};
export async function getMarriageConfig() {

22
src/hooks/marriage/use-section-data.ts

@ -80,19 +80,29 @@ export async function updateMarriageSectionData(
payload: UpdateMarriageSectionDataPayload,
) {
if (slug === "family_marital_history") {
const mergedItemEn = getQuestionListItemBySlug("family_marital_history", "en");
const mergedItemFa = getQuestionListItemBySlug("family_marital_history", "fa");
const mergedItemEn = getQuestionListItemBySlug(
"family_marital_history",
"en",
);
const mergedItemFa = getQuestionListItemBySlug(
"family_marital_history",
"fa",
);
const fbQuestionsEn = mergedItemEn?.questions.filter(
const fbQuestionsEn =
mergedItemEn?.questions.filter(
(q) => q.originalSlug === "family_background",
) || [];
const mhQuestionsEn = mergedItemEn?.questions.filter(
const mhQuestionsEn =
mergedItemEn?.questions.filter(
(q) => q.originalSlug === "marital_history_children",
) || [];
const fbQuestionsFa = mergedItemFa?.questions.filter(
const fbQuestionsFa =
mergedItemFa?.questions.filter(
(q) => q.originalSlug === "family_background",
) || [];
const mhQuestionsFa = mergedItemFa?.questions.filter(
const mhQuestionsFa =
mergedItemFa?.questions.filter(
(q) => q.originalSlug === "marital_history_children",
) || [];

16
src/lib/auth-bridge.ts

@ -217,18 +217,21 @@ class AuthBridge {
const cookieToken =
getClientCookie(TOKEN_COOKIE_NAME) ??
getClientCookie("habib_token") ??
(typeof window !== "undefined" ? sessionStorage.getItem(TOKEN_COOKIE_NAME) : null);
(typeof window !== "undefined"
? sessionStorage.getItem(TOKEN_COOKIE_NAME)
: null);
if (cookieToken === "NO_TOKEN") {
return null;
}
// Treat "NO_TOKEN" the same as having no cookie at all — fall through
// to the default token instead of returning null.
const effectiveCookie =
cookieToken && cookieToken !== "NO_TOKEN" ? cookieToken : null;
if (this.token) {
return this.token;
}
if (cookieToken) {
return cookieToken;
if (effectiveCookie) {
return effectiveCookie;
}
return (
@ -248,4 +251,3 @@ class AuthBridge {
}
export const authBridge = new AuthBridge();

22
src/lib/http.ts

@ -87,22 +87,34 @@ http.interceptors.request.use((config) => {
config.headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
config.headers["Pragma"] = "no-cache";
const stripNoToken = (v: string | null) =>
v && v !== "NO_TOKEN" ? v : null;
const token =
authBridge.getToken() ??
getClientCookie("HABIB_TOKEN") ??
getClientCookie("habib_token");
stripNoToken(getClientCookie("HABIB_TOKEN")) ??
stripNoToken(getClientCookie("habib_token"));
if (token) {
config.headers.Authorization = `Token ${token}`;
}
let lang: string | undefined = (getClientCookie("HABIB_LANGUAGE") ?? getClientCookie("habib_language")) ?? undefined;
let lang: string | undefined =
getClientCookie("HABIB_LANGUAGE") ??
getClientCookie("habib_language") ??
undefined;
if (!isLocale(lang)) {
const pathSegment = typeof window !== "undefined" ? window.location.pathname.split("/")[1] : undefined;
const pathSegment =
typeof window !== "undefined"
? window.location.pathname.split("/")[1]
: undefined;
if (isLocale(pathSegment)) {
lang = pathSegment;
} else {
const docLang = typeof document !== "undefined" ? document.documentElement.lang : undefined;
const docLang =
typeof document !== "undefined"
? document.documentElement.lang
: undefined;
lang = isLocale(docLang) ? docLang : "fa";
}
}

Loading…
Cancel
Save