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,65 @@
|
||||
/**
|
||||
* Verifies the amoCRM connection and prints what the integration will do with
|
||||
* your account: which pipeline stage leads land in, and which of your custom
|
||||
* fields the lead form's data was matched to.
|
||||
*
|
||||
* npm run amo:check
|
||||
*/
|
||||
import { AmoClient } from '../server/src/amocrm.ts'
|
||||
import { readAmoConfig } from '../server/src/config.ts'
|
||||
import { resolveLeadFieldMap } from '../server/src/lead-mapper.ts'
|
||||
|
||||
const config = readAmoConfig()
|
||||
if (!config) {
|
||||
console.error('✗ AMO_SUBDOMAIN and AMO_LONG_LIVED_TOKEN must be set in .env')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const amo = new AmoClient(config)
|
||||
|
||||
console.info(`→ ${config.baseUrl}\n`)
|
||||
|
||||
const account = await amo.getAccount()
|
||||
console.info(`✓ Account: ${account?.name} (id ${account?.id})`)
|
||||
|
||||
const pipeline = await amo.getPipeline(config.pipelineId)
|
||||
if (!pipeline) {
|
||||
console.error(`✗ Pipeline ${config.pipelineId} not found in this account.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const stages = [...(pipeline._embedded?.statuses ?? [])].sort((a, b) => a.sort - b.sort)
|
||||
console.info(`✓ Pipeline ${pipeline.id}: «${pipeline.name}»`)
|
||||
console.info(` Leads will be created in the first stage: «${stages[0]?.name ?? 'unknown'}»`)
|
||||
console.info(` All stages: ${stages.map((s) => s.name).join(' → ')}\n`)
|
||||
|
||||
const leadFields = await amo.getLeadFields()
|
||||
const map = resolveLeadFieldMap(leadFields)
|
||||
|
||||
console.info(`Lead data → amoCRM field mapping (${leadFields.length} custom fields in the account):`)
|
||||
const keys = [
|
||||
'company',
|
||||
'comment',
|
||||
'page',
|
||||
'referrer',
|
||||
'form',
|
||||
'utm_source',
|
||||
'utm_medium',
|
||||
'utm_campaign',
|
||||
'utm_content',
|
||||
'utm_term',
|
||||
]
|
||||
for (const key of keys) {
|
||||
const field = map.get(key)
|
||||
console.info(
|
||||
field
|
||||
? ` ✓ ${key.padEnd(14)} → «${field.name}» (id ${field.id}, ${field.type})`
|
||||
: ` · ${key.padEnd(14)} → note on the lead`,
|
||||
)
|
||||
}
|
||||
|
||||
const contactFields = await amo.getContactFields()
|
||||
const hasPhone = contactFields.some((f) => f.code === 'PHONE')
|
||||
const hasEmail = contactFields.some((f) => f.code === 'EMAIL')
|
||||
console.info(`\nContact fields: PHONE ${hasPhone ? '✓' : '✗ missing'}, EMAIL ${hasEmail ? '✓' : '✗ missing'}`)
|
||||
console.info('\nAll good — the form is ready to create leads.')
|
||||
Reference in New Issue
Block a user