Browse Source
refactor: implement entry route caching and improve loading skeletons for profile redirection.
Dev
refactor: implement entry route caching and improve loading skeletons for profile redirection.
Dev
38 changed files with 884 additions and 136 deletions
-
2public/assets/images/Ellipse 1210.svg
-
2public/assets/images/Group 1597880466.svg
-
2public/assets/images/Group 15978804fdasf68.svg
-
2public/assets/images/Group 159788fd0467.svg
-
2public/assets/images/Group 15978fdsa80467.svg
-
2public/assets/images/female_avatar.svg
-
2public/assets/images/home-Checkups-List.svg
-
2public/assets/images/islamic_pattern_2_2892_3864.svg
-
32src/app/[lang]/page.tsx
-
9src/app/api/proxy/route.ts
-
41src/app/globals.css
-
10src/app/intro/page.tsx
-
41src/app/layout.tsx
-
24src/app/page.tsx
-
53src/app/questions-list/page.tsx
-
17src/app/terms/page.test.tsx
-
16src/app/terms/page.tsx
-
134src/components/Componentes/entry-route-resolver.test.tsx
-
103src/components/Componentes/entry-route-resolver.tsx
-
8src/components/Componentes/female-outcome-sheet.test.tsx
-
23src/components/Componentes/loading-skeleton.tsx
-
84src/components/Componentes/network-image.tsx
-
8src/components/Componentes/question-answer.test.tsx
-
7src/components/Componentes/schema-question-flow.integration.test.tsx
-
63src/components/Componentes/slider-page.test.tsx
-
26src/components/Componentes/slider-page.tsx
-
11src/components/Componentes/slider-slide-two.tsx
-
6src/components/Componentes/token-switcher.tsx
-
2src/hooks/marriage/use-match-start.ts
-
48src/hooks/marriage/use-profile-main.test.ts
-
9src/hooks/marriage/use-profile-main.ts
-
39src/lib/auth-bridge.test.ts
-
26src/lib/auth-bridge.ts
-
46src/lib/entry-route-cache.test.ts
-
65src/lib/entry-route-cache.ts
-
20src/lib/get-submit-path.test.ts
-
19src/lib/get-submit-path.ts
-
14src/lib/utils.ts
2
public/assets/images/Ellipse 1210.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
public/assets/images/Group 1597880466.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
public/assets/images/Group 15978804fdasf68.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
public/assets/images/Group 159788fd0467.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
public/assets/images/Group 15978fdsa80467.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
public/assets/images/female_avatar.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
public/assets/images/home-Checkups-List.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
public/assets/images/islamic_pattern_2_2892_3864.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1 +1,31 @@ |
|||||
export { default } from "@/app/intro/page"; |
|
||||
|
import { cookies } from "next/headers"; |
||||
|
import { redirect } from "next/navigation"; |
||||
|
import EntryRouteResolver from "@/components/Componentes/entry-route-resolver"; |
||||
|
import { |
||||
|
getAuthenticatedCachedEntryPath, |
||||
|
MARRIAGE_ENTRY_PATH_COOKIE, |
||||
|
} from "@/lib/entry-route-cache"; |
||||
|
import { localizePath } from "@/translations/config"; |
||||
|
|
||||
|
export const dynamic = "force-dynamic"; |
||||
|
|
||||
|
export default async function LocaleEntryPage({ |
||||
|
params, |
||||
|
}: { |
||||
|
params: Promise<{ lang: string }>; |
||||
|
}) { |
||||
|
const [{ lang }, cookieStore] = await Promise.all([params, cookies()]); |
||||
|
const token = |
||||
|
cookieStore.get("HABIB_TOKEN")?.value ?? |
||||
|
cookieStore.get("habib_token")?.value; |
||||
|
const cachedEntryPath = getAuthenticatedCachedEntryPath( |
||||
|
token, |
||||
|
cookieStore.get(MARRIAGE_ENTRY_PATH_COOKIE)?.value, |
||||
|
); |
||||
|
|
||||
|
if (cachedEntryPath) { |
||||
|
redirect(localizePath(cachedEntryPath, lang)); |
||||
|
} |
||||
|
|
||||
|
return <EntryRouteResolver />; |
||||
|
} |
||||
@ -0,0 +1,134 @@ |
|||||
|
import { render, waitFor } from "@testing-library/react"; |
||||
|
import { beforeEach, describe, expect, it, vi } from "vitest"; |
||||
|
import EntryRouteResolver from "./entry-route-resolver"; |
||||
|
|
||||
|
const mocks = vi.hoisted(() => ({ |
||||
|
getCachedEntryPath: vi.fn(), |
||||
|
isAuthenticated: vi.fn(), |
||||
|
refetchProfile: vi.fn(), |
||||
|
replace: vi.fn(), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("next/navigation", () => ({ |
||||
|
useRouter: () => ({ replace: mocks.replace }), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/lib/auth-bridge", () => ({ |
||||
|
HABIB_AUTH_TOKEN_CHANGED_EVENT: "habib:auth-token-changed", |
||||
|
authBridge: { isAuthenticated: mocks.isAuthenticated }, |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/lib/entry-route-cache", () => ({ |
||||
|
getCachedMarriageEntryPath: mocks.getCachedEntryPath, |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/hooks/marriage/use-profile-main", () => ({ |
||||
|
useMarriageProfileQuery: () => ({ refetch: mocks.refetchProfile }), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/translations/provider", () => ({ |
||||
|
useI18n: () => ({ locale: "en" }), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/translations/config", () => ({ |
||||
|
localizePath: (path: string, locale: string) => `/${locale}${path}`, |
||||
|
})); |
||||
|
|
||||
|
describe("EntryRouteResolver", () => { |
||||
|
beforeEach(() => { |
||||
|
vi.clearAllMocks(); |
||||
|
mocks.getCachedEntryPath.mockReturnValue(null); |
||||
|
delete window.HabibApp; |
||||
|
}); |
||||
|
|
||||
|
it("renders no loading or page content while resolving", () => { |
||||
|
mocks.isAuthenticated.mockReturnValue(true); |
||||
|
mocks.refetchProfile.mockReturnValue(new Promise(() => {})); |
||||
|
|
||||
|
const { container } = render(<EntryRouteResolver />); |
||||
|
|
||||
|
expect(container).toBeEmptyDOMElement(); |
||||
|
}); |
||||
|
|
||||
|
it("routes a registered pending-info user directly to questions", async () => { |
||||
|
mocks.isAuthenticated.mockReturnValue(true); |
||||
|
mocks.refetchProfile.mockResolvedValue({ |
||||
|
data: { |
||||
|
status: "pending_info", |
||||
|
gender: "female", |
||||
|
is_registering_for_self: true, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
render(<EntryRouteResolver />); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mocks.replace).toHaveBeenCalledWith("/en/questions-list"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("uses the persistent entry path without refetching the profile", async () => { |
||||
|
mocks.isAuthenticated.mockReturnValue(true); |
||||
|
mocks.getCachedEntryPath.mockReturnValue("/questions-list"); |
||||
|
|
||||
|
render(<EntryRouteResolver />); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mocks.replace).toHaveBeenCalledWith("/en/questions-list"); |
||||
|
}); |
||||
|
expect(mocks.refetchProfile).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("routes a registered user with an advanced status to that status page", async () => { |
||||
|
mocks.isAuthenticated.mockReturnValue(true); |
||||
|
mocks.refetchProfile.mockResolvedValue({ |
||||
|
data: { |
||||
|
status: "waiting", |
||||
|
gender: "male", |
||||
|
is_registering_for_self: false, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
render(<EntryRouteResolver />); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mocks.replace).toHaveBeenCalledWith("/en/finding-match"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it("routes an unauthenticated browser user to the real intro route", async () => { |
||||
|
mocks.isAuthenticated.mockReturnValue(false); |
||||
|
|
||||
|
render(<EntryRouteResolver />); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mocks.replace).toHaveBeenCalledWith("/en/intro"); |
||||
|
}); |
||||
|
expect(mocks.refetchProfile).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("waits for the WebView token event without rendering a loading page", async () => { |
||||
|
let authenticated = false; |
||||
|
mocks.isAuthenticated.mockImplementation(() => authenticated); |
||||
|
mocks.refetchProfile.mockResolvedValue({ |
||||
|
data: { |
||||
|
status: "pending_info", |
||||
|
gender: "female", |
||||
|
is_registering_for_self: true, |
||||
|
}, |
||||
|
}); |
||||
|
window.HabibApp = { postMessage: vi.fn() }; |
||||
|
|
||||
|
const { container } = render(<EntryRouteResolver />); |
||||
|
|
||||
|
expect(container).toBeEmptyDOMElement(); |
||||
|
expect(mocks.replace).not.toHaveBeenCalled(); |
||||
|
|
||||
|
authenticated = true; |
||||
|
window.dispatchEvent(new Event("habib:auth-token-changed")); |
||||
|
|
||||
|
await waitFor(() => { |
||||
|
expect(mocks.replace).toHaveBeenCalledWith("/en/questions-list"); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,103 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useRouter } from "next/navigation"; |
||||
|
import { useEffect, useRef } from "react"; |
||||
|
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; |
||||
|
import { |
||||
|
authBridge, |
||||
|
HABIB_AUTH_TOKEN_CHANGED_EVENT, |
||||
|
} from "@/lib/auth-bridge"; |
||||
|
import { getCachedMarriageEntryPath } from "@/lib/entry-route-cache"; |
||||
|
import { |
||||
|
getSubmitPath, |
||||
|
hasCompletedMarriageProfileBasics, |
||||
|
} from "@/lib/get-submit-path"; |
||||
|
import { localizePath } from "@/translations/config"; |
||||
|
import { useI18n } from "@/translations/provider"; |
||||
|
|
||||
|
export default function EntryRouteResolver() { |
||||
|
const router = useRouter(); |
||||
|
const { locale } = useI18n(); |
||||
|
const { refetch } = useMarriageProfileQuery({ |
||||
|
enabled: false, |
||||
|
retry: false, |
||||
|
}); |
||||
|
const entryCheckIdRef = useRef(0); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
let isActive = true; |
||||
|
let bridgeWaitTimeout: number | undefined; |
||||
|
|
||||
|
const goToIntro = () => { |
||||
|
router.replace(localizePath("/intro", locale)); |
||||
|
}; |
||||
|
|
||||
|
const resolveEntryRoute = async () => { |
||||
|
if (!authBridge.isAuthenticated()) { |
||||
|
if (isActive) { |
||||
|
goToIntro(); |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const cachedEntryPath = getCachedMarriageEntryPath(); |
||||
|
if (cachedEntryPath) { |
||||
|
router.replace(localizePath(cachedEntryPath, locale)); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const checkId = ++entryCheckIdRef.current; |
||||
|
|
||||
|
try { |
||||
|
const { data: profile } = await refetch(); |
||||
|
|
||||
|
if (!isActive || checkId !== entryCheckIdRef.current) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!hasCompletedMarriageProfileBasics(profile)) { |
||||
|
goToIntro(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
router.replace(localizePath(getSubmitPath(profile), locale)); |
||||
|
} catch (error) { |
||||
|
console.warn("Could not resolve entry route", error); |
||||
|
if (isActive && checkId === entryCheckIdRef.current) { |
||||
|
goToIntro(); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleTokenChanged = () => { |
||||
|
if (bridgeWaitTimeout !== undefined) { |
||||
|
window.clearTimeout(bridgeWaitTimeout); |
||||
|
bridgeWaitTimeout = undefined; |
||||
|
} |
||||
|
void resolveEntryRoute(); |
||||
|
}; |
||||
|
|
||||
|
window.addEventListener(HABIB_AUTH_TOKEN_CHANGED_EVENT, handleTokenChanged); |
||||
|
|
||||
|
if (authBridge.isAuthenticated()) { |
||||
|
void resolveEntryRoute(); |
||||
|
} else if (window.HabibApp) { |
||||
|
bridgeWaitTimeout = window.setTimeout(goToIntro, 4_000); |
||||
|
} else { |
||||
|
goToIntro(); |
||||
|
} |
||||
|
|
||||
|
return () => { |
||||
|
isActive = false; |
||||
|
if (bridgeWaitTimeout !== undefined) { |
||||
|
window.clearTimeout(bridgeWaitTimeout); |
||||
|
} |
||||
|
window.removeEventListener( |
||||
|
HABIB_AUTH_TOKEN_CHANGED_EVENT, |
||||
|
handleTokenChanged, |
||||
|
); |
||||
|
}; |
||||
|
}, [locale, refetch, router]); |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import Image, { type ImageProps } from "next/image"; |
||||
|
import { useState, useEffect } from "react"; |
||||
|
import { cn } from "@/lib/utils"; |
||||
|
|
||||
|
export interface NetworkImageProps extends Omit<ImageProps, "src"> { |
||||
|
src?: string | null; |
||||
|
fallbackSrc?: string; |
||||
|
containerClassName?: string; |
||||
|
placeholderClassName?: string; |
||||
|
} |
||||
|
|
||||
|
export function NetworkImage({ |
||||
|
src, |
||||
|
fallbackSrc = "/assets/images/Frame 2095586523.png", |
||||
|
alt = "", |
||||
|
className, |
||||
|
containerClassName, |
||||
|
placeholderClassName, |
||||
|
fill, |
||||
|
width, |
||||
|
height, |
||||
|
priority, |
||||
|
...props |
||||
|
}: NetworkImageProps) { |
||||
|
const initialSrc = src || fallbackSrc; |
||||
|
const [imgSrc, setImgSrc] = useState<string>(initialSrc); |
||||
|
const [isLoaded, setIsLoaded] = useState(false); |
||||
|
const [hasError, setHasError] = useState(false); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const target = src || fallbackSrc; |
||||
|
setImgSrc(target); |
||||
|
setHasError(false); |
||||
|
setIsLoaded(false); |
||||
|
}, [src, fallbackSrc]); |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
className={cn( |
||||
|
fill |
||||
|
? "absolute inset-0 size-full overflow-hidden" |
||||
|
: "relative overflow-hidden inline-block", |
||||
|
containerClassName, |
||||
|
)} |
||||
|
style={!fill && width && height ? { width, height } : undefined} |
||||
|
> |
||||
|
{!isLoaded && !hasError && ( |
||||
|
<div |
||||
|
className={cn( |
||||
|
"absolute inset-0 animate-pulse bg-slate-200/60 dark:bg-white/[0.08] z-0", |
||||
|
placeholderClassName, |
||||
|
)} |
||||
|
aria-hidden |
||||
|
/> |
||||
|
)} |
||||
|
<Image |
||||
|
{...props} |
||||
|
src={imgSrc} |
||||
|
alt={alt} |
||||
|
fill={fill} |
||||
|
width={!fill ? width : undefined} |
||||
|
height={!fill ? height : undefined} |
||||
|
priority={priority} |
||||
|
onLoad={() => setIsLoaded(true)} |
||||
|
onError={() => { |
||||
|
if (imgSrc !== fallbackSrc) { |
||||
|
setImgSrc(fallbackSrc); |
||||
|
} else { |
||||
|
setHasError(true); |
||||
|
} |
||||
|
}} |
||||
|
className={cn( |
||||
|
"transition-opacity duration-300", |
||||
|
!isLoaded ? "opacity-0" : "opacity-100", |
||||
|
className, |
||||
|
)} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export default NetworkImage; |
||||
@ -0,0 +1,48 @@ |
|||||
|
import { beforeEach, describe, expect, it, vi } from "vitest"; |
||||
|
import { getMarriageProfile } from "./use-profile-main"; |
||||
|
|
||||
|
const mocks = vi.hoisted(() => ({ |
||||
|
get: vi.fn(), |
||||
|
setCachedEntryPath: vi.fn(), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/lib/http", () => ({ |
||||
|
http: { get: mocks.get }, |
||||
|
})); |
||||
|
|
||||
|
vi.mock("@/lib/entry-route-cache", () => ({ |
||||
|
setCachedMarriageEntryPath: mocks.setCachedEntryPath, |
||||
|
})); |
||||
|
|
||||
|
describe("getMarriageProfile", () => { |
||||
|
beforeEach(() => { |
||||
|
vi.clearAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
it("persists the resolved route for a completed profile", async () => { |
||||
|
const profile = { |
||||
|
status: "pending_info", |
||||
|
gender: "female", |
||||
|
is_registering_for_self: true, |
||||
|
}; |
||||
|
mocks.get.mockResolvedValue({ data: profile }); |
||||
|
|
||||
|
await expect(getMarriageProfile()).resolves.toBe(profile); |
||||
|
|
||||
|
expect(mocks.setCachedEntryPath).toHaveBeenCalledWith("/questions-list"); |
||||
|
}); |
||||
|
|
||||
|
it("clears the route for an incomplete onboarding profile", async () => { |
||||
|
mocks.get.mockResolvedValue({ |
||||
|
data: { |
||||
|
status: "pending_onboarding", |
||||
|
gender: null, |
||||
|
is_registering_for_self: null, |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
await getMarriageProfile(); |
||||
|
|
||||
|
expect(mocks.setCachedEntryPath).toHaveBeenCalledWith(null); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,39 @@ |
|||||
|
import { beforeEach, describe, expect, it, vi } from "vitest"; |
||||
|
|
||||
|
const mocks = vi.hoisted(() => ({ |
||||
|
getClientCookie: vi.fn(), |
||||
|
setCachedEntryPath: vi.fn(), |
||||
|
})); |
||||
|
|
||||
|
vi.mock("./cookies", () => ({ |
||||
|
getClientCookie: mocks.getClientCookie, |
||||
|
})); |
||||
|
|
||||
|
vi.mock("./entry-route-cache", () => ({ |
||||
|
setCachedMarriageEntryPath: mocks.setCachedEntryPath, |
||||
|
})); |
||||
|
|
||||
|
describe("authBridge", () => { |
||||
|
beforeEach(() => { |
||||
|
vi.resetModules(); |
||||
|
vi.clearAllMocks(); |
||||
|
mocks.getClientCookie.mockReturnValue(null); |
||||
|
window.HABIB_TOKEN = undefined; |
||||
|
window.addFlutterResponseListener = vi.fn(() => vi.fn()); |
||||
|
window.sessionStorage.clear(); |
||||
|
}); |
||||
|
|
||||
|
it("treats a missing token as anonymous and clears the entry cache", async () => { |
||||
|
const { authBridge } = await import("./auth-bridge"); |
||||
|
|
||||
|
expect(authBridge.isAuthenticated()).toBe(false); |
||||
|
expect(mocks.setCachedEntryPath).toHaveBeenCalledWith(null); |
||||
|
}); |
||||
|
|
||||
|
it("treats an empty token as anonymous", async () => { |
||||
|
window.HABIB_TOKEN = ""; |
||||
|
const { authBridge } = await import("./auth-bridge"); |
||||
|
|
||||
|
expect(authBridge.isAuthenticated()).toBe(false); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,46 @@ |
|||||
|
import { afterEach, describe, expect, it } from "vitest"; |
||||
|
import { |
||||
|
getAuthenticatedCachedEntryPath, |
||||
|
getCachedMarriageEntryPath, |
||||
|
MARRIAGE_ENTRY_PATH_COOKIE, |
||||
|
normalizeCachedMarriageEntryPath, |
||||
|
setCachedMarriageEntryPath, |
||||
|
} from "./entry-route-cache"; |
||||
|
|
||||
|
describe("entry route cache", () => { |
||||
|
afterEach(() => { |
||||
|
document.cookie = `${MARRIAGE_ENTRY_PATH_COOKIE}=; Path=/; Max-Age=0`; |
||||
|
}); |
||||
|
|
||||
|
it("accepts only known authenticated routes", () => { |
||||
|
expect(normalizeCachedMarriageEntryPath("/questions-list")).toBe( |
||||
|
"/questions-list", |
||||
|
); |
||||
|
expect(normalizeCachedMarriageEntryPath("%2Ffinding-match")).toBe( |
||||
|
"/finding-match", |
||||
|
); |
||||
|
expect(normalizeCachedMarriageEntryPath("https://example.com")).toBeNull(); |
||||
|
expect(normalizeCachedMarriageEntryPath("//example.com")).toBeNull(); |
||||
|
expect(normalizeCachedMarriageEntryPath("/intro")).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it("requires a real token before trusting the cached route", () => { |
||||
|
expect( |
||||
|
getAuthenticatedCachedEntryPath("token", "/questions-list"), |
||||
|
).toBe("/questions-list"); |
||||
|
expect( |
||||
|
getAuthenticatedCachedEntryPath("NO_TOKEN", "/questions-list"), |
||||
|
).toBeNull(); |
||||
|
expect( |
||||
|
getAuthenticatedCachedEntryPath("", "/questions-list"), |
||||
|
).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it("persists and clears a safe client entry route", () => { |
||||
|
setCachedMarriageEntryPath("/questions-list"); |
||||
|
expect(getCachedMarriageEntryPath()).toBe("/questions-list"); |
||||
|
|
||||
|
setCachedMarriageEntryPath(null); |
||||
|
expect(getCachedMarriageEntryPath()).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,65 @@ |
|||||
|
import { |
||||
|
deleteClientCookie, |
||||
|
getClientCookie, |
||||
|
setClientCookie, |
||||
|
} from "./cookies"; |
||||
|
|
||||
|
export const MARRIAGE_ENTRY_PATH_COOKIE = "HABIB_MARRIAGE_ENTRY_PATH"; |
||||
|
|
||||
|
const AUTHENTICATED_ENTRY_PATHS = new Set([ |
||||
|
"/terms", |
||||
|
"/questions-list", |
||||
|
"/finding-match", |
||||
|
"/request-accepted", |
||||
|
"/new-match", |
||||
|
"/request-sent", |
||||
|
]); |
||||
|
|
||||
|
export function isAuthenticatedToken(token: string | null | undefined) { |
||||
|
return Boolean(token && token !== "NO_TOKEN" && token.trim() !== ""); |
||||
|
} |
||||
|
|
||||
|
export function normalizeCachedMarriageEntryPath( |
||||
|
value: string | null | undefined, |
||||
|
) { |
||||
|
if (!value) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
let decodedValue: string; |
||||
|
try { |
||||
|
decodedValue = decodeURIComponent(value); |
||||
|
} catch { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return AUTHENTICATED_ENTRY_PATHS.has(decodedValue) ? decodedValue : null; |
||||
|
} |
||||
|
|
||||
|
export function getAuthenticatedCachedEntryPath( |
||||
|
token: string | null | undefined, |
||||
|
cachedPath: string | null | undefined, |
||||
|
) { |
||||
|
if (!isAuthenticatedToken(token)) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return normalizeCachedMarriageEntryPath(cachedPath); |
||||
|
} |
||||
|
|
||||
|
export function getCachedMarriageEntryPath() { |
||||
|
return normalizeCachedMarriageEntryPath( |
||||
|
getClientCookie(MARRIAGE_ENTRY_PATH_COOKIE), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
export function setCachedMarriageEntryPath(path: string | null) { |
||||
|
const safePath = normalizeCachedMarriageEntryPath(path); |
||||
|
|
||||
|
if (!safePath) { |
||||
|
deleteClientCookie(MARRIAGE_ENTRY_PATH_COOKIE); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
setClientCookie(MARRIAGE_ENTRY_PATH_COOKIE, safePath); |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
export function cn(...inputs: (string | undefined | null | false | Record<string, boolean>)[]): string { |
||||
|
const classes: string[] = []; |
||||
|
for (const input of inputs) { |
||||
|
if (!input) continue; |
||||
|
if (typeof input === "string") { |
||||
|
classes.push(input); |
||||
|
} else if (typeof input === "object") { |
||||
|
for (const [key, val] of Object.entries(input)) { |
||||
|
if (val) classes.push(key); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return classes.join(" "); |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue