81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
// Отправка заявки на POST /api/lead и перевод ответа сервера в состояние формы.
|
|
import type { FieldErrors, LeadField, LeadRequest } from '@kvarta/shared/lead';
|
|
|
|
export const SUBMIT_TIMEOUT_MS = 10_000;
|
|
|
|
/** Что сказать посетителю после отправки. */
|
|
export type SubmitNotice = 'created' | 'clarified' | 'invalid' | 'rate_limited' | 'failed';
|
|
|
|
export type SubmitResult = {
|
|
status: 'success' | 'error';
|
|
notice: SubmitNotice;
|
|
/** подсказки под полями при 400 */
|
|
fieldErrors: FieldErrors;
|
|
};
|
|
|
|
export type SendLead = (body: LeadRequest) => Promise<SubmitResult>;
|
|
|
|
const FIELDS: readonly LeadField[] = ['name', 'phone', 'interest', 'comment'];
|
|
|
|
const success = (notice: 'created' | 'clarified'): SubmitResult => ({ status: 'success', notice, fieldErrors: {} });
|
|
const error = (notice: SubmitNotice, fieldErrors: FieldErrors = {}): SubmitResult => ({
|
|
status: 'error',
|
|
notice,
|
|
fieldErrors,
|
|
});
|
|
|
|
/** Сеть, таймаут, отказ fetch. */
|
|
export const FAILED: SubmitResult = error('failed');
|
|
|
|
function pickFieldErrors(value: unknown): FieldErrors {
|
|
if (!value || typeof value !== 'object') return {};
|
|
const src = value as Record<string, unknown>;
|
|
const out: FieldErrors = {};
|
|
for (const field of FIELDS) {
|
|
const message = src[field];
|
|
if (typeof message === 'string' && message !== '') out[field] = message;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Ответ сервера → состояние формы. Тело может быть чем угодно, в том числе null. */
|
|
export function resultFromResponse(status: number, body: unknown): SubmitResult {
|
|
const data = (body && typeof body === 'object' ? body : {}) as Record<string, unknown>;
|
|
if (status === 201) return success('created');
|
|
if (status === 200 && data.ok === true) return success(data.clarified === true ? 'clarified' : 'created');
|
|
if (status === 400) {
|
|
const fieldErrors = pickFieldErrors(data.fieldErrors);
|
|
return Object.keys(fieldErrors).length > 0 ? error('invalid', fieldErrors) : FAILED;
|
|
}
|
|
if (status === 429) return error('rate_limited');
|
|
// 413, 415, 5xx, 503 и всё неожиданное — «попробуйте ещё раз или позвоните»
|
|
return FAILED;
|
|
}
|
|
|
|
export function createSendLead({
|
|
fetchImpl = (...args: Parameters<typeof fetch>) => fetch(...args),
|
|
timeoutMs = SUBMIT_TIMEOUT_MS,
|
|
url = '/api/lead',
|
|
}: { fetchImpl?: typeof fetch; timeoutMs?: number; url?: string } = {}): SendLead {
|
|
return async (body) => {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
try {
|
|
const res = await fetchImpl(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
signal: controller.signal,
|
|
});
|
|
const data: unknown = await res.json().catch(() => null);
|
|
return resultFromResponse(res.status, data);
|
|
} catch {
|
|
return FAILED;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
};
|
|
}
|
|
|
|
export const sendLead: SendLead = createSendLead();
|