279 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|