"use client"; import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; import { useSession } from "next-auth/react"; import { createPortal } from "react-dom"; const MapPicker = dynamic(() => import("./MapPicker"), { ssr: false }); type Category = { id: string; name: string; }; type PlaceResult = { description: string; place_id: string; }; type EventFormProps = { variant?: "card" | "inline"; open?: boolean; onOpenChange?: (open: boolean) => void; prefillStartAt?: string | Date | null; showTrigger?: boolean; }; export default function EventForm({ variant = "card", open, onOpenChange, prefillStartAt, showTrigger = true }: EventFormProps) { const { data } = useSession(); const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [categories, setCategories] = useState([]); const [isOpen, setIsOpen] = useState(false); const [startAt, setStartAt] = useState(""); const [endAt, setEndAt] = useState(""); const [placesProvider, setPlacesProvider] = useState<"google" | "osm">("osm"); const [placesKey, setPlacesKey] = useState(""); const [placeQuery, setPlaceQuery] = useState(""); const [placeResults, setPlaceResults] = useState([]); const [placeId, setPlaceId] = useState(null); const [placeLat, setPlaceLat] = useState(null); const [placeLng, setPlaceLng] = useState(null); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); useEffect(() => { const loadCategories = async () => { try { const response = await fetch("/api/categories"); if (!response.ok) return; setCategories(await response.json()); } catch { // Ignore category load errors in UI. } }; loadCategories(); }, []); const isControlled = open !== undefined; const modalOpen = isControlled ? open : isOpen; const setModalOpen = (value: boolean) => { if (isControlled) { onOpenChange?.(value); } else { setIsOpen(value); } }; const formatLocalDateTime = (value: Date) => { const offset = value.getTimezoneOffset() * 60000; return new Date(value.getTime() - offset).toISOString().slice(0, 16); }; useEffect(() => { const loadKey = async () => { try { const response = await fetch("/api/settings/google-places"); if (!response.ok) return; const payload = await response.json(); setPlacesKey(payload.apiKey || ""); setPlacesProvider(payload.provider === "google" ? "google" : "osm"); } catch { // ignore } }; if (modalOpen) { loadKey(); } }, [modalOpen]); useEffect(() => { const fetchPlaces = async () => { if (placeQuery.trim().length < 3) { setPlaceResults([]); return; } if (placesProvider === "google" && !placesKey) { setPlaceResults([]); return; } const response = await fetch( `/api/places/autocomplete?input=${encodeURIComponent(placeQuery)}&countries=de,fr,ch,at` ); if (!response.ok) return; const payload = await response.json(); setPlaceResults(payload.predictions || []); }; const timer = window.setTimeout(() => { fetchPlaces(); }, 350); return () => window.clearTimeout(timer); }, [placeQuery, placesKey, placesProvider]); useEffect(() => { if (!modalOpen) return; if (prefillStartAt) { const startDate = prefillStartAt instanceof Date ? prefillStartAt : new Date(prefillStartAt); if (Number.isNaN(startDate.getTime())) return; const startValue = formatLocalDateTime(startDate); setStartAt(startValue); const endDate = new Date(startDate.getTime() + 3 * 60 * 60 * 1000); setEndAt(formatLocalDateTime(endDate)); return; } const startDate = new Date(); startDate.setHours(12, 0, 0, 0); setStartAt(formatLocalDateTime(startDate)); const endDate = new Date(startDate.getTime() + 3 * 60 * 60 * 1000); setEndAt(formatLocalDateTime(endDate)); }, [modalOpen, prefillStartAt]); const selectPlace = async (place: PlaceResult) => { setPlaceResults([]); setPlaceQuery(place.description); setPlaceId(place.place_id); const response = await fetch( `/api/places/details?placeId=${encodeURIComponent(place.place_id)}` ); if (!response.ok) return; const payload = await response.json(); setPlaceLat(payload.lat ?? null); setPlaceLng(payload.lng ?? null); }; const reverseLookup = async (lat: number, lng: number) => { try { const response = await fetch( `/api/places/reverse?lat=${lat}&lng=${lng}` ); if (!response.ok) return; const payload = await response.json(); if (payload.label) { setPlaceQuery(payload.label); } if (payload.placeId) { setPlaceId(payload.placeId); } } catch { // ignore } }; const pickOnMap = (lat: number, lng: number) => { setPlaceLat(lat); setPlaceLng(lng); setPlaceResults([]); reverseLookup(lat, lng); }; const toIsoString = (value: FormDataEntryValue | null) => { if (!value) return null; const raw = String(value); if (!raw) return null; const date = new Date(raw); if (Number.isNaN(date.getTime())) return null; return date.toISOString(); }; const onSubmit = async (event: React.FormEvent) => { event.preventDefault(); const form = event.currentTarget; setStatus(null); setError(null); const formData = new FormData(form); const payload = { title: formData.get("title"), description: formData.get("description"), location: formData.get("location"), locationPlaceId: formData.get("locationPlaceId"), locationLat: formData.get("locationLat"), locationLng: formData.get("locationLng"), startAt: toIsoString(formData.get("startAt")), endAt: toIsoString(formData.get("endAt")), categoryId: formData.get("categoryId") }; try { const response = await fetch("/api/events", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || "Fehler beim Speichern."); } form.reset(); setStartAt(""); setEndAt(""); setStatus("Termin vorgeschlagen. Ein Admin bestätigt ihn."); setModalOpen(false); window.dispatchEvent(new Event("views-updated")); setPlaceQuery(""); setPlaceResults([]); setPlaceId(null); setPlaceLat(null); setPlaceLng(null); } catch (err) { setError((err as Error).message); } }; const triggerButton = showTrigger ? ( ) : null; return ( <> {variant === "card" ? (
{triggerButton}
{categories.length === 0 && (

Noch keine Kategorien vorhanden. Bitte Admin um Anlage.

)} {status &&

{status}

} {error &&

{error}

}
) : triggerButton ? (
{triggerButton} {status &&

{status}

} {error &&

{error}

}
) : null} {modalOpen && mounted && createPortal(

Neuen Termin anlegen

{data?.user?.role === "USER" && (

Hinweis: Terminvorschläge müssen von einem Admin freigegeben werden.

)}
{ setPlaceQuery(event.target.value); setPlaceId(null); setPlaceLat(null); setPlaceLng(null); }} className="w-full rounded-xl border border-slate-300 px-3 py-2" /> {placeResults.length > 0 && (
{placeResults.map((place) => ( ))}
)} {placesProvider === "google" && !placesKey && placeQuery.length > 0 && (
Google Places ist nicht konfiguriert.
)}
pickOnMap(coords.lat, coords.lng)} />

Klick auf die Karte, um den Ort zu setzen.

{ const nextStart = event.target.value; setStartAt(nextStart); if (nextStart) { const startDate = new Date(nextStart); const endDate = new Date(startDate.getTime() + 3 * 60 * 60 * 1000); const offset = endDate.getTimezoneOffset() * 60000; const local = new Date(endDate.getTime() - offset) .toISOString() .slice(0, 16); setEndAt(local); } else { setEndAt(""); } }} className="rounded-xl border border-slate-300 px-3 py-2" /> setEndAt(event.target.value)} className="rounded-xl border border-slate-300 px-3 py-2" />