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