feat: form components StayDetailsStep, ImageGallery, PriceSummary, CustomerForm

This commit is contained in:
Rodribm10 2026-04-13 23:57:11 -03:00
parent 38fa508e3f
commit 66fa4e77fd
4 changed files with 199 additions and 0 deletions

View File

@ -0,0 +1,62 @@
import { FormField } from '@/components/FormField'
import { maskCPF, maskPhone } from '@/lib/formatters'
import type { ReservationFormState } from '@/hooks/useReservationForm'
interface Props {
form: ReservationFormState
onChange: <K extends keyof ReservationFormState>(k: K, v: ReservationFormState[K]) => void
}
export function CustomerForm({ form, onChange }: Props) {
return (
<section className="space-y-4 rounded-2xl border border-champagne/20 bg-midnight/50 p-6 backdrop-blur">
<h2 className="font-serif text-2xl text-champagne">Seus dados</h2>
<FormField
label="Nome completo"
required
placeholder="Como no documento"
value={form.nome}
onChange={(e) => onChange('nome', e.target.value)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
label="Telefone / WhatsApp"
required
placeholder="(99) 99999-9999"
value={form.telefone}
onChange={(e) => onChange('telefone', maskPhone(e.target.value))}
/>
<FormField
label="CPF"
required
placeholder="000.000.000-00"
value={form.cpf}
onChange={(e) => onChange('cpf', maskCPF(e.target.value))}
/>
</div>
<FormField
label="E-mail"
type="email"
placeholder="seu@email.com"
value={form.email}
onChange={(e) => onChange('email', e.target.value)}
/>
<div>
<label className="font-sans text-xs uppercase tracking-widest text-champagne">
Observação (opcional)
</label>
<textarea
className="mt-2 w-full rounded-lg border border-champagne/30 bg-midnight/60 px-4 py-3 font-sans text-ivory placeholder:text-slate focus:border-champagne focus:outline-none"
rows={3}
placeholder="Alguma preferência especial?"
value={form.observacao}
onChange={(e) => onChange('observacao', e.target.value)}
/>
</div>
</section>
)
}

View File

@ -0,0 +1,28 @@
import type { Database } from '@/types/database'
type Foto = Database['reserva_hotel']['Tables']['fotos_categoria']['Row']
interface Props {
fotos: Foto[]
}
export function ImageGallery({ fotos }: Props) {
if (fotos.length === 0) return null
return (
<section className="grid grid-cols-1 md:grid-cols-2 gap-3">
{fotos.map((foto) => (
<div
key={foto.id}
className="aspect-video overflow-hidden rounded-xl border border-champagne/20"
>
<img
src={foto.url_foto}
alt={foto.alt ?? 'Foto da suíte'}
className="h-full w-full object-cover transition duration-500 hover:scale-105"
/>
</div>
))}
</section>
)
}

View File

@ -0,0 +1,31 @@
import { formatBRL } from '@/lib/formatters'
interface Props {
totalCents: number
depositCents: number
}
export function PriceSummary({ totalCents, depositCents }: Props) {
if (totalCents === 0) return null
const restante = totalCents - depositCents
return (
<section className="rounded-2xl border border-champagne/30 bg-midnight/60 p-6 backdrop-blur">
<div className="flex items-baseline justify-between mb-3">
<span className="text-slate text-sm uppercase tracking-widest">Preço estimado</span>
<span className="font-serif text-3xl text-champagne">{formatBRL(totalCents)}</span>
</div>
<div className="flex items-baseline justify-between text-slate text-sm mb-2">
<span>Pagar no check-in</span>
<span>{formatBRL(restante)}</span>
</div>
<div className="mt-4 flex items-baseline justify-between rounded-xl border border-champagne/40 bg-champagne/10 px-4 py-3">
<div>
<div className="text-xs uppercase tracking-widest text-champagne">Entrada via PIX (50%)</div>
<div className="text-slate text-xs">Necessário para confirmar</div>
</div>
<div className="font-serif text-3xl text-gradient-gold">{formatBRL(depositCents)}</div>
</div>
</section>
)
}

View File

@ -0,0 +1,78 @@
import { SelectField } from '@/components/SelectField'
import { FormField } from '@/components/FormField'
import type { ReservationFormState } from '@/hooks/useReservationForm'
import type { Database } from '@/types/database'
type Marca = Database['reserva_hotel']['Tables']['marcas']['Row']
type Unidade = Database['reserva_hotel']['Tables']['unidades']['Row']
interface Props {
form: ReservationFormState
marcas: Marca[]
unidades: Unidade[]
onChange: <K extends keyof ReservationFormState>(k: K, v: ReservationFormState[K]) => void
}
export function StayDetailsStep({ form, marcas, unidades, onChange }: Props) {
const marca = marcas.find((m) => m.id === form.marcaId) ?? null
const unidade = unidades.find((u) => u.id === form.unidadeId) ?? null
const categoriaOptions =
unidade?.categorias_visiveis?.map((c) => ({ value: c, label: c })) ??
marca?.categorias?.map((c) => ({ value: c, label: c })) ??
[]
const permanenciaOptions =
marca?.permanencias?.map((p) => ({ value: p, label: p })) ?? []
return (
<section className="space-y-6 rounded-2xl border border-champagne/20 bg-midnight/50 p-6 backdrop-blur">
<h2 className="font-serif text-2xl text-champagne">Detalhes da estadia</h2>
<SelectField
label="Marca"
required
value={form.marcaId}
onChange={(e) => onChange('marcaId', e.target.value)}
options={marcas.map((m) => ({ value: m.id, label: m.nome }))}
/>
<SelectField
label="Unidade do Hotel"
required
disabled={!form.marcaId}
value={form.unidadeId}
onChange={(e) => onChange('unidadeId', e.target.value)}
options={unidades.map((u) => ({ value: u.id, label: u.nome }))}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<SelectField
label="Permanência"
required
disabled={!form.marcaId}
value={form.permanencia}
onChange={(e) => onChange('permanencia', e.target.value)}
options={permanenciaOptions}
/>
<SelectField
label="Categoria da suíte"
required
disabled={!form.unidadeId}
value={form.categoria}
onChange={(e) => onChange('categoria', e.target.value)}
options={categoriaOptions}
/>
</div>
<FormField
label="Data e horário do check-in"
required
type="datetime-local"
value={form.checkinAt}
onChange={(e) => onChange('checkinAt', e.target.value)}
/>
</section>
)
}