Convert hotel landing to React + Tailwind v4 with amoCRM lead capture
Ports the single-file ЭкзоОтель page to the same architecture as the fitness
landing: Vite + React 19 + TypeScript + Tailwind v4 on the client, Express 5 for
the /api/leads/hotels endpoint, zod schema shared between the two.
The original page is kept in legacy/index.html as the visual reference. Its 15
inlined base64 images are extracted to files (the 1 MB HTML becomes ~318 KB of
JS plus assets loaded on demand), and its text is reproduced line for line —
verified with an innerText diff. Section heights stay within 0.6% at 375 and
1440 px; the drift comes from Inter actually loading, which the original asked
for but never served.
Three deliberate departures, documented in the README:
* eyebrow and lead in the CTA block were dark teal on navy (3.4:1); they now
match the other dark sections
* hero fact values overflowed their 80px column into the label below 430px
* "2025–2026г.." typo in the market source note
Leads reuse the fitness amoCRM integration: contact lookup by phone in every
spelling, deal in the first stage of pipeline 10980758, account fields matched
automatically with the rest written to a note. Rate limit, honeypot, and a
localStorage fallback so a lead survives the CRM being down. Vite runs on 5174
and the API on 3001 so both landings can run at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
60de4ef27c
commit
35f713a267
@@ -0,0 +1,99 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/** Every icon on the page is a 24×24 outline drawn in `currentColor`. */
|
||||
function Icon({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none">
|
||||
{children}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArrowRightIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
||||
export function PhoneIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path
|
||||
d="M22 16.92v3a2 2 0 0 1-2.18 2 19.86 19.86 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.86 19.86 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.12.9.35 1.78.68 2.62a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.46-1.25a2 2 0 0 1 2.11-.45c.84.33 1.72.56 2.62.68A2 2 0 0 1 22 16.92Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.8"
|
||||
/>
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
||||
export function ChevronsIcon() {
|
||||
return (
|
||||
<Icon>
|
||||
<path d="M8 7l-5 5 5 5M16 7l5 5-5 5" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
|
||||
</Icon>
|
||||
)
|
||||
}
|
||||
|
||||
/** Icons of the guest portraits, keyed by `guestPortraits[].icon`. */
|
||||
export const guestIcons = {
|
||||
road: (
|
||||
<Icon>
|
||||
<path d="M3 12h18M5 8h4l2-4h2l2 4h4l2 4-2 4h-4l-2 4h-2l-2-4H5l-2-4 2-4Z" stroke="currentColor" strokeLinejoin="round" strokeWidth="1.7" />
|
||||
</Icon>
|
||||
),
|
||||
active: (
|
||||
<Icon>
|
||||
<path
|
||||
d="M13 5.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM10 22l1-6-3-3 2-5 4 1 2 3h4M5 22l2-6"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.7"
|
||||
/>
|
||||
</Icon>
|
||||
),
|
||||
wellness: (
|
||||
<Icon>
|
||||
<path d="M12 21c4.5-3 7-6.2 7-10a7 7 0 0 0-14 0c0 3.8 2.5 7 7 10Z" stroke="currentColor" strokeWidth="1.7" />
|
||||
<path
|
||||
d="M9 11c1.8.1 3.2-.9 4-3 1.8 1.4 2.4 3.2 1.5 5.3-1 2.2-3.3 3.1-5.5 2"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="1.7"
|
||||
/>
|
||||
</Icon>
|
||||
),
|
||||
business: (
|
||||
<Icon>
|
||||
<path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 9h18v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9Z" stroke="currentColor" strokeWidth="1.7" />
|
||||
<path d="M3 13h18M10 13v2h4v-2" stroke="currentColor" strokeWidth="1.7" />
|
||||
</Icon>
|
||||
),
|
||||
}
|
||||
|
||||
/** Icons of the "почему Экзо" list, keyed by `whyItems[].icon`. */
|
||||
export const whyIcons = {
|
||||
tech: (
|
||||
<Icon>
|
||||
<path d="M12 3 3 7.5 12 12l9-4.5L12 3Z" stroke="currentColor" strokeLinejoin="round" strokeWidth="1.7" />
|
||||
<path d="M5 11v5.5L12 21l7-4.5V11" stroke="currentColor" strokeLinejoin="round" strokeWidth="1.7" />
|
||||
</Icon>
|
||||
),
|
||||
factory: (
|
||||
<Icon>
|
||||
<path d="M3 21V9l5 3V7l5 3V3h8v18H3Z" stroke="currentColor" strokeLinejoin="round" strokeWidth="1.7" />
|
||||
<path d="M16 7h2M16 11h2M7 16h2M12 16h2M17 16h2" stroke="currentColor" strokeLinecap="round" strokeWidth="1.7" />
|
||||
</Icon>
|
||||
),
|
||||
launch: (
|
||||
<Icon>
|
||||
<path d="M4 4h16v16H4V4Z" stroke="currentColor" strokeWidth="1.7" />
|
||||
<path d="m8 12 2.5 2.5L16 9" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" />
|
||||
</Icon>
|
||||
),
|
||||
}
|
||||
Reference in New Issue
Block a user