Compare commits
No commits in common. "6a8d588731d16723f36ea0d3fdbbbda5c7078027" and "480d4c2128e249e87074c9672e15875337a889e7" have entirely different histories.
6a8d588731
...
480d4c2128
2 changed files with 0 additions and 351 deletions
|
|
@ -21,7 +21,6 @@ import HeroBar from "@/components/trade-in/v2/HeroBar";
|
||||||
import ParamsPanel from "@/components/trade-in/v2/ParamsPanel";
|
import ParamsPanel from "@/components/trade-in/v2/ParamsPanel";
|
||||||
import ResultPanel from "@/components/trade-in/v2/ResultPanel";
|
import ResultPanel from "@/components/trade-in/v2/ResultPanel";
|
||||||
import { ObjectSummary } from "@/components/trade-in/v2/ObjectSummary";
|
import { ObjectSummary } from "@/components/trade-in/v2/ObjectSummary";
|
||||||
import { LeadForm } from "@/components/trade-in/v2/LeadForm";
|
|
||||||
import { Footer } from "@/components/trade-in/v2/Footer";
|
import { Footer } from "@/components/trade-in/v2/Footer";
|
||||||
import SectionOverlay from "@/components/trade-in/v2/SectionOverlay";
|
import SectionOverlay from "@/components/trade-in/v2/SectionOverlay";
|
||||||
import { LocationDrawer } from "@/components/trade-in/v2/LocationDrawer";
|
import { LocationDrawer } from "@/components/trade-in/v2/LocationDrawer";
|
||||||
|
|
@ -929,14 +928,6 @@ export default function TradeInV2Page() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lead form (issue #2395 — v2-native mount of #2377's lead capture,
|
|
||||||
previously stranded in the dead HeroTransparency/HeroSummary tree).
|
|
||||||
Lives outside the scaled 1536×1024 artboard transform (in normal page
|
|
||||||
flow) so it stays legible at any viewport/scale instead of shrinking
|
|
||||||
with the HUD; only shown once a real, sufficient estimate is on
|
|
||||||
screen — never on the empty/insufficient-data states. */}
|
|
||||||
{hasEstimate && <LeadForm estimateId={currentEstimateId} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,342 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
// LeadForm — «Оставить заявку на трейд-ин» v2-native card (issue #2395: fixup
|
|
||||||
// of #2377 — the working lead form only existed in the dead HeroTransparency/
|
|
||||||
// HeroSummary legacy tree, unreachable from the live `/trade-in/v2` route,
|
|
||||||
// which redirects everything else. This is the v2-native mount).
|
|
||||||
//
|
|
||||||
// Reused AS-IS (per #2395 DoD): `useCreateLeadMutation` (trade-in-api.ts) +
|
|
||||||
// `TradeInLeadInput`/`TradeInLeadResponse` types — backend is untouched and
|
|
||||||
// already live (`POST /api/v1/trade-in/lead`). The state machine / validation /
|
|
||||||
// submit-handler shape is ported from HeroTransparency.tsx (idle → submitting →
|
|
||||||
// success/error, phone regex mirroring the backend's `_PHONE_PATTERN`, explicit
|
|
||||||
// ФЗ-152 consent) — but the markup/styling below is a fresh v2 HUD card built on
|
|
||||||
// `tokens.ts`, not the legacy `.hero-lead*` CSS classes (those stay in
|
|
||||||
// trade-in.css, unused by this component).
|
|
||||||
//
|
|
||||||
// Critical fixup carried over from the #2377 code review: the submit <button>
|
|
||||||
// must NEVER receive the `disabled` attribute from validity state — only from
|
|
||||||
// `leadMutation.isPending`. A `disabled` button does not dispatch a submit
|
|
||||||
// event, so an invalid attempt would silently no-op instead of running the
|
|
||||||
// handler and showing a hint. The form has `noValidate` and every submit
|
|
||||||
// (valid or not) goes through `handleSubmit`, which surfaces an inline
|
|
||||||
// explanation for whichever field blocked it.
|
|
||||||
import { useState } from "react";
|
|
||||||
import type { CSSProperties, FormEvent } from "react";
|
|
||||||
import { tokens } from "./tokens";
|
|
||||||
import { useCreateLeadMutation } from "@/lib/trade-in-api";
|
|
||||||
|
|
||||||
const {
|
|
||||||
accent,
|
|
||||||
accentDeep,
|
|
||||||
onAccent,
|
|
||||||
ink,
|
|
||||||
body2,
|
|
||||||
muted,
|
|
||||||
line,
|
|
||||||
line2,
|
|
||||||
danger,
|
|
||||||
success,
|
|
||||||
bracket,
|
|
||||||
surface,
|
|
||||||
font,
|
|
||||||
} = tokens;
|
|
||||||
|
|
||||||
// Mirrors the backend's `_PHONE_PATTERN` (tradein-mvp/backend/app/api/v1/
|
|
||||||
// lead.py): optional "+", digits/spaces/parens/dashes/dots, 5-32 chars.
|
|
||||||
// Client-side validation gives instant feedback ahead of the network 422 (full
|
|
||||||
// E.164 normalization is out of MVP scope). Duplicated rather than imported
|
|
||||||
// from HeroTransparency.tsx — that component is legacy/unreachable and this
|
|
||||||
// file is intentionally zero-coupled to it.
|
|
||||||
const PHONE_PATTERN = /^[+]?[\d\s().-]{5,32}$/;
|
|
||||||
function isValidPhone(v: string): boolean {
|
|
||||||
return PHONE_PATTERN.test(v.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = `
|
|
||||||
.lf-input:focus-visible{outline:none;border-color:${accentDeep};box-shadow:0 0 0 2px ${accentDeep}}
|
|
||||||
.lf-submit-btn{background:linear-gradient(90deg,${accentDeep},${accent});transition:all .18s}
|
|
||||||
.lf-submit-btn:hover:not(:disabled){background:${accentDeep};box-shadow:0 6px 18px rgba(46,139,255,.35)}
|
|
||||||
.lf-submit-btn:active:not(:disabled){transform:translateY(1px)}
|
|
||||||
.lf-submit-btn:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(46,139,255,.35)}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const cardStyle: CSSProperties = {
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: 640,
|
|
||||||
margin: "16px auto 0",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
background: surface.w50,
|
|
||||||
backdropFilter: "blur(6px)",
|
|
||||||
border: `1px solid ${line2}`,
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: "18px 22px 20px",
|
|
||||||
fontFamily: font.sans,
|
|
||||||
color: ink,
|
|
||||||
};
|
|
||||||
|
|
||||||
const bracketBase: CSSProperties = {
|
|
||||||
position: "absolute",
|
|
||||||
width: 13,
|
|
||||||
height: 13,
|
|
||||||
pointerEvents: "none",
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputStyle: CSSProperties = {
|
|
||||||
height: 36,
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
boxSizing: "border-box",
|
|
||||||
background: surface.w62,
|
|
||||||
border: `1px solid ${line}`,
|
|
||||||
borderRadius: 6,
|
|
||||||
padding: "0 13px",
|
|
||||||
fontFamily: font.sans,
|
|
||||||
fontSize: 13,
|
|
||||||
color: ink,
|
|
||||||
outline: "none",
|
|
||||||
};
|
|
||||||
|
|
||||||
const errorTextStyle: CSSProperties = {
|
|
||||||
marginTop: 8,
|
|
||||||
fontSize: 10.5,
|
|
||||||
letterSpacing: 0.3,
|
|
||||||
lineHeight: 1.4,
|
|
||||||
color: danger,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LeadFormProps {
|
|
||||||
/** estimate_id для привязки заявки (POST /lead, source="result"). Компонент
|
|
||||||
* монтируется только при hasEstimate=true (см. app/v2/page.tsx), поэтому на
|
|
||||||
* практике почти всегда non-null — но проп остаётся nullable честно, т.к.
|
|
||||||
* TradeInLeadInput.estimate_id опционален и на бэкенде. */
|
|
||||||
estimateId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LeadForm({ estimateId }: LeadFormProps) {
|
|
||||||
const [phone, setPhone] = useState("");
|
|
||||||
const [consent, setConsent] = useState(false);
|
|
||||||
const [touched, setTouched] = useState(false);
|
|
||||||
const leadMutation = useCreateLeadMutation();
|
|
||||||
|
|
||||||
const phoneInvalid = touched && !isValidPhone(phone);
|
|
||||||
const consentInvalid = touched && !phoneInvalid && !consent;
|
|
||||||
const canSubmit = isValidPhone(phone) && consent && !leadMutation.isPending;
|
|
||||||
const apiError = leadMutation.isError
|
|
||||||
? leadMutation.error?.message || "Не удалось отправить заявку, попробуйте ещё раз"
|
|
||||||
: null;
|
|
||||||
|
|
||||||
function handleSubmit(e: FormEvent<HTMLFormElement>) {
|
|
||||||
e.preventDefault();
|
|
||||||
setTouched(true);
|
|
||||||
if (!isValidPhone(phone) || !consent || leadMutation.isPending) return;
|
|
||||||
leadMutation.mutate({
|
|
||||||
phone: phone.trim(),
|
|
||||||
consent: true,
|
|
||||||
estimate_id: estimateId,
|
|
||||||
source: "result",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leadMutation.isSuccess) {
|
|
||||||
return (
|
|
||||||
<div style={cardStyle}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...bracketBase,
|
|
||||||
left: -1,
|
|
||||||
top: -1,
|
|
||||||
borderLeft: `1.5px solid ${bracket}`,
|
|
||||||
borderTop: `1.5px solid ${bracket}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...bracketBase,
|
|
||||||
right: -1,
|
|
||||||
bottom: -1,
|
|
||||||
borderRight: `1.5px solid ${bracket}`,
|
|
||||||
borderBottom: `1.5px solid ${bracket}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
role="status"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
margin: 0,
|
|
||||||
fontSize: 12.5,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: success,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2.4"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
||||||
<polyline points="22 4 12 14.01 9 11.01" />
|
|
||||||
</svg>
|
|
||||||
Заявка принята, мы свяжемся с вами
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} noValidate style={cardStyle}>
|
|
||||||
<style>{styles}</style>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...bracketBase,
|
|
||||||
left: -1,
|
|
||||||
top: -1,
|
|
||||||
borderLeft: `1.5px solid ${bracket}`,
|
|
||||||
borderTop: `1.5px solid ${bracket}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...bracketBase,
|
|
||||||
right: -1,
|
|
||||||
bottom: -1,
|
|
||||||
borderRight: `1.5px solid ${bracket}`,
|
|
||||||
borderBottom: `1.5px solid ${bracket}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: 14,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="15"
|
|
||||||
height="15"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke={accent}
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path d="M4 4h16v12H5.17L4 17.17V4z" />
|
|
||||||
<line x1="8" y1="9" x2="16" y2="9" />
|
|
||||||
<line x1="8" y1="12.5" x2="13" y2="12.5" />
|
|
||||||
</svg>
|
|
||||||
<span style={{ fontSize: 13, fontWeight: 600, letterSpacing: 2 }}>
|
|
||||||
ОСТАВИТЬ ЗАЯВКУ НА ТРЕЙД-ИН
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
|
|
||||||
<input
|
|
||||||
className="lf-input"
|
|
||||||
type="tel"
|
|
||||||
inputMode="tel"
|
|
||||||
autoComplete="tel"
|
|
||||||
placeholder="+7 900 000-00-00"
|
|
||||||
aria-label="Телефон для связи"
|
|
||||||
aria-invalid={phoneInvalid || undefined}
|
|
||||||
value={phone}
|
|
||||||
onChange={(e) => setPhone(e.target.value)}
|
|
||||||
disabled={leadMutation.isPending}
|
|
||||||
style={
|
|
||||||
phoneInvalid
|
|
||||||
? { ...inputStyle, border: `1px solid ${danger}` }
|
|
||||||
: inputStyle
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="lf-submit-btn"
|
|
||||||
disabled={leadMutation.isPending}
|
|
||||||
aria-disabled={canSubmit ? undefined : true}
|
|
||||||
style={{
|
|
||||||
height: 36,
|
|
||||||
flex: "0 0 auto",
|
|
||||||
padding: "0 20px",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: 6,
|
|
||||||
fontFamily: "inherit",
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
letterSpacing: 1,
|
|
||||||
color: onAccent,
|
|
||||||
cursor: leadMutation.isPending ? "wait" : "pointer",
|
|
||||||
opacity: canSubmit ? 1 : 0.55,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{leadMutation.isPending ? "ОТПРАВЛЯЕМ…" : "ОСТАВИТЬ ЗАЯВКУ"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
gap: 8,
|
|
||||||
marginTop: 12,
|
|
||||||
fontSize: 11,
|
|
||||||
lineHeight: 1.45,
|
|
||||||
color: body2,
|
|
||||||
cursor: leadMutation.isPending ? "default" : "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={consent}
|
|
||||||
onChange={(e) => setConsent(e.target.checked)}
|
|
||||||
disabled={leadMutation.isPending}
|
|
||||||
aria-invalid={consentInvalid || undefined}
|
|
||||||
style={{ marginTop: 2, cursor: "inherit", flex: "0 0 auto" }}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
Согласен(-на) на обработку персональных данных в соответствии с
|
|
||||||
Федеральным законом «О персональных данных» № 152-ФЗ
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{phoneInvalid && (
|
|
||||||
<p role="alert" style={errorTextStyle}>
|
|
||||||
Введите телефон (5–32 символа: цифры, +, пробелы, скобки, дефис)
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{consentInvalid && (
|
|
||||||
<p role="alert" style={errorTextStyle}>
|
|
||||||
Необходимо согласие на обработку персональных данных
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{apiError && (
|
|
||||||
<p role="alert" style={errorTextStyle}>
|
|
||||||
{apiError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
marginTop: 10,
|
|
||||||
marginBottom: 0,
|
|
||||||
fontSize: 9.5,
|
|
||||||
letterSpacing: 0.3,
|
|
||||||
color: muted,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Менеджер свяжется с вами для уточнения деталей трейд-ина
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue