chore: contact from submission

This commit is contained in:
Zotov Ivan Yuryevich
2026-06-17 23:14:09 +03:00
parent df4a48f124
commit 460998aa6e
2 changed files with 75 additions and 5 deletions

View File

@@ -1,4 +1,5 @@
import { Calendar } from 'lucide-react' import { Calendar } from 'lucide-react'
import { useState } from 'react'
export type ContactFormField = export type ContactFormField =
| 'name' | 'name'
@@ -30,9 +31,10 @@ export interface ContactFormProps {
submitLabel?: string submitLabel?: string
className?: string className?: string
idPrefix?: string idPrefix?: string
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void
} }
type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error'
const DEFAULT_VISIBLE_FIELDS: readonly ContactFormField[] = ['name', 'company', 'email', 'phone', 'message'] const DEFAULT_VISIBLE_FIELDS: readonly ContactFormField[] = ['name', 'company', 'email', 'phone', 'message']
function withPrefix(prefix: string, fieldName: string): string { function withPrefix(prefix: string, fieldName: string): string {
@@ -46,15 +48,40 @@ export function ContactForm({
submitLabel = 'Отправить заявку', submitLabel = 'Отправить заявку',
className = 'space-y-5', className = 'space-y-5',
idPrefix = 'contact-form', idPrefix = 'contact-form',
onSubmit,
}: ContactFormProps) { }: ContactFormProps) {
const fields = new Set(visibleFields) const fields = new Set(visibleFields)
const showNameCompanyRow = fields.has('name') || fields.has('company') const showNameCompanyRow = fields.has('name') || fields.has('company')
const showEmailPhoneRow = fields.has('email') || fields.has('phone') const showEmailPhoneRow = fields.has('email') || fields.has('phone')
const showPurposeTimelineRow = fields.has('purpose') || fields.has('timeline') const showPurposeTimelineRow = fields.has('purpose') || fields.has('timeline')
const [status, setStatus] = useState<SubmitStatus>('idle')
const isSubmitting = status === 'submitting'
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (isSubmitting) return
const form = event.currentTarget
const payload = Object.fromEntries(new FormData(form).entries())
setStatus('submitting')
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!response.ok) throw new Error(`Request failed with status ${response.status}`)
form.reset()
setStatus('success')
} catch (error) {
console.error('Contact form submission failed', error)
setStatus('error')
}
}
return ( return (
<form className={className} onSubmit={onSubmit}> <form className={className} onSubmit={handleSubmit}>
{submissionSource ? <input type="hidden" name="submissionSource" value={submissionSource} /> : null} {submissionSource ? <input type="hidden" name="submissionSource" value={submissionSource} /> : null}
{showNameCompanyRow && ( {showNameCompanyRow && (
<div className="grid md:grid-cols-2 gap-5"> <div className="grid md:grid-cols-2 gap-5">
@@ -231,12 +258,24 @@ export function ContactForm({
<div className="flex flex-col sm:flex-row items-center gap-4"> <div className="flex flex-col sm:flex-row items-center gap-4">
<button <button
type="submit" type="submit"
className="w-full sm:w-auto px-8 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all" disabled={isSubmitting}
className="w-full sm:w-auto px-8 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all disabled:opacity-60 disabled:cursor-not-allowed"
> >
{submitLabel} {isSubmitting ? 'Отправляем…' : submitLabel}
</button> </button>
<p className="text-sm text-gray-500">Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности</p> <p className="text-sm text-gray-500">Нажимая кнопку, вы соглашаетесь с политикой конфиденциальности</p>
</div> </div>
{status === 'success' && (
<p className="text-sm text-green-600" role="status">
Спасибо! Мы свяжемся с вами в ближайшее время.
</p>
)}
{status === 'error' && (
<p className="text-sm text-red-600" role="alert">
Не удалось отправить заявку. Пожалуйста, попробуйте ещё раз.
</p>
)}
</form> </form>
) )
} }

31
src/pages/api/contact.ts Normal file
View File

@@ -0,0 +1,31 @@
import type { APIRoute } from 'astro'
// Served on demand: this accepts live form submissions, never a build-time snapshot.
export const prerender = false
function json(body: unknown, status: number): Response {
return new Response(JSON.stringify(body), {
status,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
},
})
}
export const POST: APIRoute = async ({ request }) => {
let data: Record<string, unknown>
try {
data = await request.json()
} catch {
return json({ ok: false, error: 'Invalid JSON body' }, 400)
}
// TODO: forward to the CRM (Bitrix24) and/or persist. For now just log on the server.
console.info('[contact]', {
receivedAt: new Date().toISOString(),
data,
})
return json({ ok: true }, 200)
}