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
-
36src/app/new-match/profile/page.tsx
-
20src/app/questions-list/[slug]/answer-pace-sheet.tsx
-
114src/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
-
179src/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
-
58src/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({ |
|||
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL, |
|||
baseURL: isLocalhost() ? "/api/proxy" : process.env.NEXT_PUBLIC_API_BASE_URL, |
|||
headers: { |
|||
Accept: "application/json", |
|||
"Content-Type": "application/json", |
|||
}, |
|||
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