feat: form components StayDetailsStep, ImageGallery, PriceSummary, CustomerForm
This commit is contained in:
parent
38fa508e3f
commit
66fa4e77fd
62
src/components/reservation/CustomerForm.tsx
Normal file
62
src/components/reservation/CustomerForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
src/components/reservation/ImageGallery.tsx
Normal file
28
src/components/reservation/ImageGallery.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/components/reservation/PriceSummary.tsx
Normal file
31
src/components/reservation/PriceSummary.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
78
src/components/reservation/StayDetailsStep.tsx
Normal file
78
src/components/reservation/StayDetailsStep.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user