Browse Source
feat: implement modular question components, match summary display, and profile navigation infrastructure
master
feat: implement modular question components, match summary display, and profile navigation infrastructure
master
28 changed files with 2005 additions and 199 deletions
-
18package-lock.json
-
2package.json
-
303src/app/api/proxy/route.ts
-
20src/app/new-match/page.tsx
-
32src/app/new-match/profile/page.tsx
-
20src/app/questions-list/[slug]/answer-pace-sheet.tsx
-
106src/app/questions-list/[slug]/page.tsx
-
326src/app/questions-list/[slug]/question-detail-client.tsx
-
46src/app/questions-list/page.tsx
-
23src/app/request-accepted/page.tsx
-
2src/components/dev/dev-click-to-component.tsx
-
21src/components/questions/question-answer-storage.tsx
-
2src/components/questions/question-date.tsx
-
2src/components/questions/question-dropdown.tsx
-
2src/components/questions/question-file.tsx
-
65src/components/questions/question-number.tsx
-
175src/components/questions/question-phone.tsx
-
16src/components/questions/question-progress-tracker.tsx
-
2src/components/questions/question-radio.tsx
-
39src/components/questions/question-snap-list.tsx
-
3src/components/questions/question-text.tsx
-
46src/components/questions/required-steps-card.tsx
-
95src/data/question-data.ts
-
12src/hooks/marriage/types.ts
-
4src/i18n/dictionaries.ts
-
366src/i18n/locales/en/questions.json
-
366src/i18n/locales/fa/questions.json
-
62src/lib/http.ts
@ -0,0 +1,303 @@ |
|||||
|
import type { NextRequest } from "next/server"; |
||||
|
|
||||
|
export const dynamic = "force-dynamic"; |
||||
|
export const runtime = "nodejs"; |
||||
|
|
||||
|
const PROXY_PATH_PARAM = "__proxyPath"; |
||||
|
|
||||
|
const REQUEST_HEADERS_TO_FORWARD = [ |
||||
|
"accept", |
||||
|
"accept-language", |
||||
|
"authorization", |
||||
|
"content-type", |
||||
|
"x-csrf-token", |
||||
|
"x-csrftoken", |
||||
|
"x-requested-with", |
||||
|
"x-xsrf-token", |
||||
|
]; |
||||
|
|
||||
|
const RESPONSE_HEADERS_TO_DROP = [ |
||||
|
"connection", |
||||
|
"content-encoding", |
||||
|
"content-length", |
||||
|
"keep-alive", |
||||
|
"proxy-authenticate", |
||||
|
"proxy-authorization", |
||||
|
"te", |
||||
|
"trailer", |
||||
|
"transfer-encoding", |
||||
|
"upgrade", |
||||
|
]; |
||||
|
|
||||
|
const MAX_LOG_BODY_LENGTH = 10_000; |
||||
|
const shouldLogProxy = |
||||
|
process.env.LOG_API_PROXY === "true" || |
||||
|
(process.env.LOG_API_PROXY !== "false" && |
||||
|
process.env.NODE_ENV !== "production"); |
||||
|
|
||||
|
class ProxyError extends Error { |
||||
|
constructor( |
||||
|
message: string, |
||||
|
readonly status: number, |
||||
|
) { |
||||
|
super(message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getApiBaseUrl() { |
||||
|
const apiBaseUrl = |
||||
|
process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL; |
||||
|
|
||||
|
if (!apiBaseUrl) { |
||||
|
throw new ProxyError( |
||||
|
"API_BASE_URL or NEXT_PUBLIC_API_BASE_URL is required", |
||||
|
500, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
return new URL(apiBaseUrl); |
||||
|
} catch { |
||||
|
throw new ProxyError("API base URL is invalid", 500); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getProxyPath(request: NextRequest) { |
||||
|
const proxyPath = request.nextUrl.searchParams.get(PROXY_PATH_PARAM); |
||||
|
|
||||
|
if (!proxyPath) { |
||||
|
throw new ProxyError("Proxy path is required", 400); |
||||
|
} |
||||
|
|
||||
|
if (/^[a-z][a-z\d+\-.]*:/i.test(proxyPath) || proxyPath.startsWith("//")) { |
||||
|
throw new ProxyError("Proxy path must be relative", 400); |
||||
|
} |
||||
|
|
||||
|
return proxyPath.startsWith("/") ? proxyPath : `/${proxyPath}`; |
||||
|
} |
||||
|
|
||||
|
function getTargetUrl(request: NextRequest) { |
||||
|
const targetUrl = getApiBaseUrl(); |
||||
|
const proxyUrl = new URL(getProxyPath(request), targetUrl.origin); |
||||
|
const basePath = targetUrl.pathname.replace(/\/$/, ""); |
||||
|
|
||||
|
targetUrl.pathname = `${basePath}${proxyUrl.pathname}`; |
||||
|
|
||||
|
const searchParams = new URLSearchParams(proxyUrl.search); |
||||
|
request.nextUrl.searchParams.forEach((value, key) => { |
||||
|
if (key !== PROXY_PATH_PARAM) { |
||||
|
searchParams.append(key, value); |
||||
|
} |
||||
|
}); |
||||
|
targetUrl.search = searchParams.toString(); |
||||
|
|
||||
|
return targetUrl; |
||||
|
} |
||||
|
|
||||
|
function getRequestHeaders(request: NextRequest, targetUrl: URL) { |
||||
|
const headers = new Headers(); |
||||
|
const authKey = process.env.NEXT_PUBLIC_AUTH_KEY; |
||||
|
|
||||
|
for (const header of REQUEST_HEADERS_TO_FORWARD) { |
||||
|
const value = request.headers.get(header); |
||||
|
|
||||
|
if (value) { |
||||
|
headers.set(header, value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (authKey) { |
||||
|
headers.set("authorization", `token ${authKey}`); |
||||
|
} |
||||
|
|
||||
|
headers.set("accept-encoding", "identity"); |
||||
|
headers.set("http_x_user_language", "en"); |
||||
|
headers.set("origin", targetUrl.origin); |
||||
|
headers.set("platform", "android"); |
||||
|
headers.set("referer", `${targetUrl.origin}/`); |
||||
|
headers.set("user-agent", "dart:io"); |
||||
|
|
||||
|
return headers; |
||||
|
} |
||||
|
|
||||
|
function getResponseHeaders(upstreamHeaders: Headers) { |
||||
|
const headers = new Headers(upstreamHeaders); |
||||
|
|
||||
|
for (const header of RESPONSE_HEADERS_TO_DROP) { |
||||
|
headers.delete(header); |
||||
|
} |
||||
|
|
||||
|
const setCookieHeaders = |
||||
|
( |
||||
|
upstreamHeaders as Headers & { getSetCookie?: () => string[] } |
||||
|
).getSetCookie?.() ?? []; |
||||
|
|
||||
|
if (setCookieHeaders.length > 0) { |
||||
|
headers.delete("set-cookie"); |
||||
|
|
||||
|
for (const cookie of setCookieHeaders) { |
||||
|
headers.append("set-cookie", cookie); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return headers; |
||||
|
} |
||||
|
|
||||
|
function getBodyLogValue(body: ArrayBuffer | undefined, contentType?: string) { |
||||
|
if (!body || body.byteLength === 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (contentType && !isTextContentType(contentType)) { |
||||
|
return `[${body.byteLength} bytes; ${contentType}]`; |
||||
|
} |
||||
|
|
||||
|
const text = new TextDecoder().decode(body); |
||||
|
|
||||
|
if (text.length <= MAX_LOG_BODY_LENGTH) { |
||||
|
return parseJsonForLog(text, text); |
||||
|
} |
||||
|
|
||||
|
return `${text.slice(0, MAX_LOG_BODY_LENGTH)}... [truncated ${text.length - MAX_LOG_BODY_LENGTH} chars]`; |
||||
|
} |
||||
|
|
||||
|
function isTextContentType(contentType: string) { |
||||
|
return ( |
||||
|
contentType.includes("application/json") || |
||||
|
contentType.includes("application/problem+json") || |
||||
|
contentType.startsWith("text/") || |
||||
|
contentType.includes("+json") || |
||||
|
contentType.includes("+xml") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function parseJsonForLog(text: string, fallback: string) { |
||||
|
try { |
||||
|
return JSON.parse(text); |
||||
|
} catch { |
||||
|
return fallback; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function headersToObject(headers: Headers) { |
||||
|
return Object.fromEntries(headers.entries()); |
||||
|
} |
||||
|
|
||||
|
function logProxyRequest( |
||||
|
request: NextRequest, |
||||
|
targetUrl: URL, |
||||
|
requestHeaders: Headers, |
||||
|
requestBody: ArrayBuffer | undefined, |
||||
|
) { |
||||
|
if (!shouldLogProxy) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
writeProxyLog("request", { |
||||
|
incomingRequest: { |
||||
|
method: request.method, |
||||
|
url: request.url, |
||||
|
nextUrl: request.nextUrl.toString(), |
||||
|
headers: headersToObject(request.headers), |
||||
|
body: getBodyLogValue( |
||||
|
requestBody, |
||||
|
request.headers.get("content-type") ?? undefined, |
||||
|
), |
||||
|
}, |
||||
|
upstreamRequest: { |
||||
|
method: request.method, |
||||
|
url: targetUrl.toString(), |
||||
|
headers: headersToObject(requestHeaders), |
||||
|
body: getBodyLogValue( |
||||
|
requestBody, |
||||
|
requestHeaders.get("content-type") ?? undefined, |
||||
|
), |
||||
|
}, |
||||
|
payload: getBodyLogValue( |
||||
|
requestBody, |
||||
|
request.headers.get("content-type") ?? undefined, |
||||
|
), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function logProxyResponse( |
||||
|
upstreamResponse: Response, |
||||
|
responseBody: ArrayBuffer, |
||||
|
) { |
||||
|
if (!shouldLogProxy) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
writeProxyLog("response", { |
||||
|
status: upstreamResponse.status, |
||||
|
statusText: upstreamResponse.statusText, |
||||
|
headers: headersToObject(upstreamResponse.headers), |
||||
|
body: getBodyLogValue( |
||||
|
responseBody, |
||||
|
upstreamResponse.headers.get("content-type") ?? undefined, |
||||
|
), |
||||
|
response: { |
||||
|
status: upstreamResponse.status, |
||||
|
statusText: upstreamResponse.statusText, |
||||
|
headers: headersToObject(upstreamResponse.headers), |
||||
|
body: getBodyLogValue( |
||||
|
responseBody, |
||||
|
upstreamResponse.headers.get("content-type") ?? undefined, |
||||
|
), |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function writeProxyLog(label: string, value: unknown) { |
||||
|
console.log(`[api-proxy] ${label} ${JSON.stringify(value)}`); |
||||
|
} |
||||
|
|
||||
|
async function proxyRequest(request: NextRequest) { |
||||
|
try { |
||||
|
const targetUrl = getTargetUrl(request); |
||||
|
const requestBody = |
||||
|
request.method === "GET" || request.method === "HEAD" |
||||
|
? undefined |
||||
|
: await request.arrayBuffer(); |
||||
|
const requestHeaders = getRequestHeaders(request, targetUrl); |
||||
|
|
||||
|
logProxyRequest(request, targetUrl, requestHeaders, requestBody); |
||||
|
|
||||
|
const upstreamResponse = await fetch(targetUrl, { |
||||
|
method: request.method, |
||||
|
headers: requestHeaders, |
||||
|
body: requestBody, |
||||
|
cache: "no-store", |
||||
|
}); |
||||
|
const responseBody = await upstreamResponse.arrayBuffer(); |
||||
|
|
||||
|
logProxyResponse(upstreamResponse, responseBody); |
||||
|
|
||||
|
return new Response(responseBody, { |
||||
|
status: upstreamResponse.status, |
||||
|
statusText: upstreamResponse.statusText, |
||||
|
headers: getResponseHeaders(upstreamResponse.headers), |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
if (error instanceof ProxyError) { |
||||
|
return Response.json({ error: error.message }, { status: error.status }); |
||||
|
} |
||||
|
|
||||
|
console.error("API proxy request failed", error); |
||||
|
|
||||
|
return Response.json( |
||||
|
{ error: "API proxy request failed" }, |
||||
|
{ status: 502 }, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export { |
||||
|
proxyRequest as DELETE, |
||||
|
proxyRequest as GET, |
||||
|
proxyRequest as HEAD, |
||||
|
proxyRequest as OPTIONS, |
||||
|
proxyRequest as PATCH, |
||||
|
proxyRequest as POST, |
||||
|
proxyRequest as PUT, |
||||
|
}; |
||||
@ -0,0 +1,326 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useRouter } from "next/navigation"; |
||||
|
import { useEffect, useMemo } from "react"; |
||||
|
import { QuestionAnswersProvider } from "@/components/questions/question-answer-storage"; |
||||
|
import QuestionButton from "@/components/questions/question-button"; |
||||
|
import QuestionDate from "@/components/questions/question-date"; |
||||
|
import QuestionDropdown from "@/components/questions/question-dropdown"; |
||||
|
import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button"; |
||||
|
import QuestionFile from "@/components/questions/question-file"; |
||||
|
import QuestionNumber from "@/components/questions/question-number"; |
||||
|
import QuestionPhone from "@/components/questions/question-phone"; |
||||
|
import QuestionPhoto from "@/components/questions/question-photo"; |
||||
|
import QuestionRadio from "@/components/questions/question-radio"; |
||||
|
import QuestionSectionFlow from "@/components/questions/question-section-flow"; |
||||
|
import QuestionSlider from "@/components/questions/question-slider"; |
||||
|
import QuestionText from "@/components/questions/question-text"; |
||||
|
import NavigationButton from "@/components/ui/navigation-button"; |
||||
|
import StickyHeader from "@/components/ui/sticky-header"; |
||||
|
import { PageBackground } from "@/components/utils/page-background"; |
||||
|
import { |
||||
|
getQuestionListItemBySlug, |
||||
|
isQuestionListItemVisibleForProfile, |
||||
|
isQuestionRequiredForProfile, |
||||
|
isQuestionVisibleForProfile, |
||||
|
type QuestionField, |
||||
|
} from "@/data/question-data"; |
||||
|
import type { MarriageGender } from "@/hooks/marriage/types"; |
||||
|
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; |
||||
|
import { defaultLocale, type Locale } from "@/i18n/config"; |
||||
|
import AnswerPaceSheet from "./answer-pace-sheet"; |
||||
|
|
||||
|
type QuestionDetailClientProps = { |
||||
|
closeLabel: string; |
||||
|
continueLabel: string; |
||||
|
description: string; |
||||
|
informationLabel: string; |
||||
|
itemSlug: string; |
||||
|
locale?: Locale; |
||||
|
questionsListHref: string; |
||||
|
title: string; |
||||
|
}; |
||||
|
|
||||
|
type StoredQuestionField = { |
||||
|
label?: string; |
||||
|
value?: unknown; |
||||
|
}; |
||||
|
|
||||
|
type StoredAnswers = { |
||||
|
fields?: StoredQuestionField[]; |
||||
|
}; |
||||
|
|
||||
|
function getQuestionStorageKey(slug: string) { |
||||
|
return `marriage:sections:${slug}:answers`; |
||||
|
} |
||||
|
|
||||
|
function parseStoredAge(value: unknown) { |
||||
|
if (typeof value === "number" && Number.isFinite(value)) { |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
if (typeof value === "string") { |
||||
|
const trimmedValue = value.trim(); |
||||
|
|
||||
|
if (!trimmedValue) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const numericAge = Number(trimmedValue); |
||||
|
|
||||
|
if (Number.isFinite(numericAge)) { |
||||
|
return numericAge; |
||||
|
} |
||||
|
|
||||
|
const dateOfBirth = new Date(trimmedValue); |
||||
|
|
||||
|
if (Number.isNaN(dateOfBirth.getTime())) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const today = new Date(); |
||||
|
let age = today.getFullYear() - dateOfBirth.getFullYear(); |
||||
|
const hasBirthdayPassed = |
||||
|
today.getMonth() > dateOfBirth.getMonth() || |
||||
|
(today.getMonth() === dateOfBirth.getMonth() && |
||||
|
today.getDate() >= dateOfBirth.getDate()); |
||||
|
|
||||
|
if (!hasBirthdayPassed) { |
||||
|
age -= 1; |
||||
|
} |
||||
|
|
||||
|
return age >= 0 ? age : null; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
function getStoredAge() { |
||||
|
try { |
||||
|
const rawValue = window.localStorage.getItem( |
||||
|
getQuestionStorageKey("personal_info"), |
||||
|
); |
||||
|
|
||||
|
if (!rawValue) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const storedAnswers = JSON.parse(rawValue) as StoredAnswers; |
||||
|
const ageField = storedAnswers.fields?.find( |
||||
|
(field) => field.label === "Age", |
||||
|
); |
||||
|
|
||||
|
if (ageField) { |
||||
|
return parseStoredAge(ageField.value); |
||||
|
} |
||||
|
|
||||
|
const dateOfBirthField = storedAnswers.fields?.find( |
||||
|
(field) => field.label === "Date of Birth", |
||||
|
); |
||||
|
|
||||
|
return parseStoredAge(dateOfBirthField?.value); |
||||
|
} catch { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function renderQuestion( |
||||
|
question: QuestionField, |
||||
|
questionIndex: number, |
||||
|
dobQuestion?: QuestionField, |
||||
|
dobQuestionIndex?: number, |
||||
|
) { |
||||
|
const compactTextHeight = |
||||
|
question.type === "text" && |
||||
|
(question.title.toLowerCase().includes("name") || |
||||
|
question.title.toLowerCase().includes("email") || |
||||
|
question.title.toLowerCase().includes("city") || |
||||
|
question.title.toLowerCase().includes("residence") || |
||||
|
question.title.toLowerCase().includes("location")) && |
||||
|
!question.title.toLowerCase().includes("describe") && |
||||
|
!question.title.toLowerCase().includes("biography") |
||||
|
? "h-[54px] min-h-0 py-2" |
||||
|
: undefined; |
||||
|
|
||||
|
switch (question.type) { |
||||
|
case "button": |
||||
|
return ( |
||||
|
<QuestionButton question={question} questionIndex={questionIndex} /> |
||||
|
); |
||||
|
case "date": |
||||
|
return <QuestionDate question={question} questionIndex={questionIndex} />; |
||||
|
case "dropdown": |
||||
|
return ( |
||||
|
<QuestionDropdown question={question} questionIndex={questionIndex} /> |
||||
|
); |
||||
|
case "file": |
||||
|
return <QuestionFile question={question} questionIndex={questionIndex} />; |
||||
|
case "number": |
||||
|
if ( |
||||
|
question.title === "Age" && |
||||
|
dobQuestion && |
||||
|
dobQuestionIndex !== undefined |
||||
|
) { |
||||
|
return ( |
||||
|
<QuestionNumber |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
derivedFromQuestion={dobQuestion} |
||||
|
derivedFromQuestionIndex={dobQuestionIndex} |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<QuestionNumber question={question} questionIndex={questionIndex} /> |
||||
|
); |
||||
|
case "phone": |
||||
|
return ( |
||||
|
<QuestionPhone question={question} questionIndex={questionIndex} /> |
||||
|
); |
||||
|
case "photo": |
||||
|
return ( |
||||
|
<QuestionPhoto question={question} questionIndex={questionIndex} /> |
||||
|
); |
||||
|
case "radio": |
||||
|
return ( |
||||
|
<QuestionRadio question={question} questionIndex={questionIndex} /> |
||||
|
); |
||||
|
case "slider": |
||||
|
return ( |
||||
|
<QuestionSlider question={question} questionIndex={questionIndex} /> |
||||
|
); |
||||
|
case "text": |
||||
|
return ( |
||||
|
<QuestionText |
||||
|
question={question} |
||||
|
questionIndex={questionIndex} |
||||
|
heightClassName={compactTextHeight} |
||||
|
/> |
||||
|
); |
||||
|
default: |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default function QuestionDetailClient({ |
||||
|
closeLabel, |
||||
|
continueLabel, |
||||
|
description, |
||||
|
informationLabel, |
||||
|
itemSlug, |
||||
|
locale = defaultLocale, |
||||
|
questionsListHref, |
||||
|
title, |
||||
|
}: QuestionDetailClientProps) { |
||||
|
const router = useRouter(); |
||||
|
const { data: profile } = useMarriageProfileQuery(); |
||||
|
const profileGender = profile?.gender; |
||||
|
const age = getStoredAge(); |
||||
|
const item = getQuestionListItemBySlug(itemSlug, locale); |
||||
|
const profileContext = useMemo( |
||||
|
() => ({ |
||||
|
age, |
||||
|
gender: profileGender as MarriageGender | null | undefined, |
||||
|
}), |
||||
|
[age, profileGender], |
||||
|
); |
||||
|
|
||||
|
const visibleQuestions = useMemo(() => { |
||||
|
if (!item) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
return item.questions |
||||
|
.filter((question) => |
||||
|
isQuestionVisibleForProfile(question, profileContext), |
||||
|
) |
||||
|
.map((question) => ({ |
||||
|
...question, |
||||
|
required: isQuestionRequiredForProfile(question, profileContext), |
||||
|
})); |
||||
|
}, [item, profileContext]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (!item || isQuestionListItemVisibleForProfile(item, profileContext)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
router.replace(questionsListHref); |
||||
|
}, [item, profileContext, questionsListHref, router]); |
||||
|
|
||||
|
if (!item || !isQuestionListItemVisibleForProfile(item, profileContext)) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const requiredQuestionsCount = useMemo( |
||||
|
() => visibleQuestions.filter((q) => q.required).length, |
||||
|
[visibleQuestions], |
||||
|
); |
||||
|
|
||||
|
const dobQuestion = visibleQuestions.find( |
||||
|
(question) => question.title === "Date of Birth", |
||||
|
); |
||||
|
const dobQuestionIndex = visibleQuestions.findIndex( |
||||
|
(question) => question.title === "Date of Birth", |
||||
|
); |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<PageBackground disabled /> |
||||
|
<AnswerPaceSheet |
||||
|
slug={item.slug} |
||||
|
title={title} |
||||
|
description={description} |
||||
|
continueLabel={continueLabel} |
||||
|
/> |
||||
|
|
||||
|
<QuestionAnswersProvider slug={item.slug} questions={visibleQuestions}> |
||||
|
<main className="-mx-[17px] flex min-h-screen flex-col bg-[#F7F1F0] pb-8"> |
||||
|
<StickyHeader className="rounded-b-[32px] px-[17px] pt-7 pb-6"> |
||||
|
<div className="flex items-start gap-4"> |
||||
|
<QuestionExitNavigationButton |
||||
|
className="shrink-0" |
||||
|
variant="transparent" |
||||
|
icon="close" |
||||
|
iconLabel={closeLabel} |
||||
|
/> |
||||
|
<h1 className="min-w-0 flex-1 text-center font-semibold text-white"> |
||||
|
{item.title} |
||||
|
</h1> |
||||
|
<NavigationButton |
||||
|
className="shrink-0" |
||||
|
variant="transparent" |
||||
|
icon="info" |
||||
|
iconLabel={informationLabel} |
||||
|
/> |
||||
|
</div> |
||||
|
</StickyHeader> |
||||
|
|
||||
|
<div className="mx-auto flex w-full max-w-md flex-col px-[17px] pt-7"> |
||||
|
<QuestionSectionFlow |
||||
|
key={item.slug} |
||||
|
total={requiredQuestionsCount} |
||||
|
continueLabel={continueLabel} |
||||
|
exitHref={questionsListHref} |
||||
|
> |
||||
|
{visibleQuestions.map((question, questionIndex) => ( |
||||
|
<div |
||||
|
key={`${item.slug}-${question.title}`} |
||||
|
data-question-required={String(question.required)} |
||||
|
> |
||||
|
{renderQuestion( |
||||
|
question, |
||||
|
questionIndex, |
||||
|
dobQuestion, |
||||
|
dobQuestionIndex, |
||||
|
)} |
||||
|
</div> |
||||
|
))} |
||||
|
</QuestionSectionFlow> |
||||
|
</div> |
||||
|
</main> |
||||
|
</QuestionAnswersProvider> |
||||
|
</> |
||||
|
); |
||||
|
} |
||||
@ -1,10 +1,68 @@ |
|||||
import axios from "axios"; |
|
||||
|
import axios, { type InternalAxiosRequestConfig } from "axios"; |
||||
|
|
||||
|
const PROXY_PATH_PARAM = "__proxyPath"; |
||||
|
const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]); |
||||
|
|
||||
|
function isAbsoluteUrl(url: string) { |
||||
|
return /^[a-z][a-z\d+\-.]*:\/\//i.test(url); |
||||
|
} |
||||
|
|
||||
|
function isLocalhost() { |
||||
|
if (typeof window === "undefined") { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return LOCALHOST_HOSTNAMES.has(window.location.hostname); |
||||
|
} |
||||
|
|
||||
|
function withProxyPathParam( |
||||
|
params: InternalAxiosRequestConfig["params"], |
||||
|
proxyPath: string, |
||||
|
) { |
||||
|
if (params instanceof URLSearchParams) { |
||||
|
const nextParams = new URLSearchParams(params); |
||||
|
nextParams.set(PROXY_PATH_PARAM, proxyPath); |
||||
|
return nextParams; |
||||
|
} |
||||
|
|
||||
|
if (typeof params === "string") { |
||||
|
const nextParams = new URLSearchParams(params); |
||||
|
nextParams.set(PROXY_PATH_PARAM, proxyPath); |
||||
|
return nextParams; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
...(params && typeof params === "object" ? params : {}), |
||||
|
[PROXY_PATH_PARAM]: proxyPath, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function getApiRequestUrl(path: string) { |
||||
|
if (!isAbsoluteUrl(path) && isLocalhost()) { |
||||
|
const searchParams = new URLSearchParams({ |
||||
|
[PROXY_PATH_PARAM]: path, |
||||
|
}); |
||||
|
|
||||
|
return `/api/proxy?${searchParams.toString()}`; |
||||
|
} |
||||
|
|
||||
|
return `${process.env.NEXT_PUBLIC_API_BASE_URL}${path}`; |
||||
|
} |
||||
|
|
||||
export const http = axios.create({ |
export const http = axios.create({ |
||||
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, |
|
||||
|
baseURL: isLocalhost() ? "/api/proxy" : process.env.NEXT_PUBLIC_API_BASE_URL, |
||||
headers: { |
headers: { |
||||
Accept: "application/json", |
Accept: "application/json", |
||||
"Content-Type": "application/json", |
"Content-Type": "application/json", |
||||
}, |
}, |
||||
withCredentials: true, |
withCredentials: true, |
||||
}); |
}); |
||||
|
|
||||
|
http.interceptors.request.use((config) => { |
||||
|
if (isLocalhost() && config.url && !isAbsoluteUrl(config.url)) { |
||||
|
config.params = withProxyPathParam(config.params, config.url); |
||||
|
config.url = ""; |
||||
|
} |
||||
|
|
||||
|
return config; |
||||
|
}); |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue