Files
convite-rael/src/components/admin/EventForm.tsx

279 lines
11 KiB
TypeScript

"use client";
import { type ChangeEvent, useActionState, useEffect, useState } from "react";
import type { FormState } from "@/app/admin/actions";
import { Button, Input } from "@/components/ui";
import { BACKGROUND_MAX_MB, BACKGROUND_TYPES } from "@/lib/background";
import { DEFAULT_THEME_ID, defaultFooterText, getTheme, themeGroups, themes, themeStyle } from "@/lib/themes";
import { formatMessageDate, sampleInviteLink } from "@/lib/whatsapp-message";
import { WhatsAppMessageField } from "./WhatsAppMessageField";
interface EventFormProps {
action: (prev: FormState, formData: FormData) => Promise<FormState>;
submitLabel: string;
defaults?: {
name: string;
date: string;
time: string;
venue: string;
address: string;
rsvpDeadline: string | null;
theme: string;
badgeText: string | null;
footerText: string | null;
whatsappMessage: string | null;
// URL da imagem de fundo atual (rota do admin), ou null se o evento não tem fundo
backgroundUrl: string | null;
};
}
export function EventForm({ action, submitLabel, defaults }: EventFormProps) {
const [state, formAction, pending] = useActionState(action, {});
const [themeId, setThemeId] = useState(defaults?.theme ?? DEFAULT_THEME_ID);
const theme = getTheme(themeId);
const [badgeText, setBadgeText] = useState(defaults?.badgeText ?? "");
const [footerText, setFooterText] = useState(defaults?.footerText ?? "");
const [eventName, setEventName] = useState(defaults?.name ?? "");
const [date, setDate] = useState(defaults?.date ?? "");
const [time, setTime] = useState(defaults?.time ?? "");
const [venue, setVenue] = useState(defaults?.venue ?? "");
const [address, setAddress] = useState(defaults?.address ?? "");
const [whatsappMessage, setWhatsappMessage] = useState(defaults?.whatsappMessage ?? "");
const [newBackground, setNewBackground] = useState<string | null>(null);
const [removeBackground, setRemoveBackground] = useState(false);
const [backgroundError, setBackgroundError] = useState<string | null>(null);
// Libera a URL temporária da prévia ao trocar de arquivo ou sair da página
useEffect(() => {
return () => {
if (newBackground) URL.revokeObjectURL(newBackground);
};
}, [newBackground]);
const handleBackgroundChange = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
setBackgroundError(null);
if (!file) {
setNewBackground(null);
return;
}
if (file.size > BACKGROUND_MAX_MB * 1024 * 1024) {
setBackgroundError(`A imagem deve ter no máximo ${BACKGROUND_MAX_MB} MB.`);
e.target.value = "";
setNewBackground(null);
return;
}
setRemoveBackground(false);
setNewBackground(URL.createObjectURL(file));
};
const currentBackground = removeBackground ? null : (newBackground ?? defaults?.backgroundUrl ?? null);
return (
<form action={formAction} className="space-y-4">
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Nome da festa</span>
<Input
tone="neutral"
name="name"
required
value={eventName}
onChange={(e) => setEventName(e.target.value)}
placeholder="1º Aniversário do Arthur"
/>
</label>
<div className="grid gap-4 sm:grid-cols-2">
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Data</span>
<Input tone="neutral" type="date" name="date" required value={date} onChange={(e) => setDate(e.target.value)} />
</label>
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Horário</span>
<Input tone="neutral" type="time" name="time" required value={time} onChange={(e) => setTime(e.target.value)} />
</label>
</div>
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Local</span>
<Input
tone="neutral"
name="venue"
required
value={venue}
onChange={(e) => setVenue(e.target.value)}
placeholder="Espaço Festa"
/>
</label>
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Endereço</span>
<Input
tone="neutral"
name="address"
required
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="Rua, número - Cidade/UF"
/>
</label>
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Confirmar presença até (opcional)</span>
<Input tone="neutral" type="date" name="rsvpDeadline" defaultValue={defaults?.rsvpDeadline ?? undefined} />
<span className="block text-xs text-slate-500 mt-1">
Depois deste dia, quem não respondeu conta como ausente e o convite não aceita mais alterações.
</span>
</label>
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Tema do convite</span>
<select
name="theme"
value={themeId}
onChange={(e) => setThemeId(e.target.value)}
className="w-full px-4 py-3 rounded-xl border border-slate-300 bg-white text-slate-900 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 transition-colors"
>
{(Object.keys(themeGroups) as (keyof typeof themeGroups)[]).map((group) => (
<optgroup key={group} label={themeGroups[group]}>
{themes
.filter((t) => t.group === group)
.map((t) => (
<option key={t.id} value={t.id}>
{t.heroIcon} {t.label}
</option>
))}
</optgroup>
))}
</select>
</label>
<div className="grid gap-4 sm:grid-cols-2">
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Texto do selo (opcional)</span>
<Input
tone="neutral"
name="badgeText"
maxLength={40}
value={badgeText}
onChange={(e) => setBadgeText(e.target.value)}
placeholder={theme.badge}
/>
</label>
<label className="block">
<span className="block text-sm font-medium text-slate-700 mb-1">Texto do rodapé (opcional)</span>
<Input
tone="neutral"
name="footerText"
maxLength={120}
value={footerText}
onChange={(e) => setFooterText(e.target.value)}
placeholder={defaultFooterText(eventName || "Nome da festa")}
/>
</label>
</div>
<span className="block -mt-2 text-xs text-slate-500">Em branco, usa o texto padrão do tema.</span>
<div className="block">
<label htmlFor="background" className="block text-sm font-medium text-slate-700 mb-1">
Imagem de fundo (opcional)
</label>
<input
id="background"
type="file"
name="background"
accept={Object.keys(BACKGROUND_TYPES).join(",")}
onChange={handleBackgroundChange}
className="block w-full text-sm text-slate-600 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-100 file:px-3 file:py-2 file:text-sm file:font-medium file:text-slate-700 hover:file:bg-slate-200"
/>
<span className="block text-xs text-slate-500 mt-1">
JPG, PNG ou WebP, até {BACKGROUND_MAX_MB} MB. Aparece no topo do convite, com um véu claro por cima, e também
na prévia do link no WhatsApp.
</span>
{backgroundError && (
<span className="block text-xs text-rose-600 mt-1" role="alert">
{backgroundError}
</span>
)}
{currentBackground && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={currentBackground}
alt="Prévia da imagem de fundo"
className="mt-2 h-32 w-full rounded-xl border border-slate-200 object-cover"
/>
)}
{removeBackground && <input type="hidden" name="removeBackground" value="on" />}
{defaults?.backgroundUrl && !newBackground && (
<div className="mt-2 flex flex-wrap items-center gap-3">
{removeBackground ? (
<>
<span className="text-sm text-slate-600">A imagem será removida ao salvar.</span>
<button
type="button"
onClick={() => setRemoveBackground(false)}
className="text-sm font-medium text-indigo-600 hover:text-indigo-800"
>
Desfazer
</button>
</>
) : (
<button
type="button"
onClick={() => setRemoveBackground(true)}
className="rounded-lg border border-rose-200 px-3 py-1.5 text-sm font-medium text-rose-700 hover:bg-rose-50"
>
Remover imagem de fundo
</button>
)}
</div>
)}
</div>
{/* Prévia: as classes amber-* abaixo assumem as cores do tema escolhido */}
<div
style={themeStyle(theme)}
className="rounded-xl border border-amber-200 bg-gradient-to-b from-amber-100 to-(--theme-accent-soft) p-4 text-center"
aria-label={`Prévia do tema ${theme.label}`}
>
<span className="inline-flex items-center gap-2 bg-amber-100/80 rounded-full px-3 py-1 text-sm font-medium text-amber-800">
<span className="text-lg">{theme.heroIcon}</span>
{badgeText.trim() || theme.badge}
</span>
<p className="mt-2 text-xl font-bold text-amber-900">Família Exemplo</p>
<p className="text-sm text-amber-700">estamos esperando por você(s)! {theme.dividerIcon}</p>
<div className="mt-3 flex items-center justify-center gap-1.5" aria-hidden="true">
{([100, 300, 500, 700, 900] as const).map((shade) => (
<span
key={shade}
className="w-5 h-5 rounded-full border border-white shadow-sm"
style={{ backgroundColor: theme.palette[shade] }}
/>
))}
<span
className="w-5 h-5 rounded-full border border-white shadow-sm"
style={{ backgroundColor: theme.accent }}
/>
</div>
<p className="mt-3 text-xs text-amber-500">
{footerText.trim() || defaultFooterText(eventName || "Nome da festa")}
</p>
</div>
<WhatsAppMessageField
value={whatsappMessage}
onChange={setWhatsappMessage}
sample={{
nome: "Família Exemplo",
evento: eventName || "Nome da festa",
data: date ? formatMessageDate(date) : "15/11/2026",
hora: time || "16:00",
local: venue || "Espaço Festa",
endereco: address || "Rua, número - Cidade/UF",
icone: theme.heroIcon,
link: sampleInviteLink("https://convite.seu-dominio.com.br"),
}}
/>
{state.error && (
<p className="text-sm text-rose-600" role="alert">
{state.error}
</p>
)}
<Button tone="neutral" type="submit" disabled={pending} className="w-full">
{pending ? "Salvando..." : submitLabel}
</Button>
</form>
);
}