Refactor event creation page to use reusable EventForm component
Replaced inlined event creation logic on `CreateEventPage` with a new reusable `EventForm` component. This decouples the form functionality, improves modularity, and enables future reuse for editing events. Enhanced validation and added support for event themes and additional settings within `EventForm`.
This commit is contained in:
@@ -1,218 +1,26 @@
|
|||||||
|
// src/app/dashboard/events/new/page.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useEvents } from "@/context/event-context";
|
import { useEvents } from "@/context/event-context";
|
||||||
import { EventCreate } from "@/client";
|
|
||||||
import Navbar from "@/components/layout/navbar";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { EventForm } from "@/components/events/event-form";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { RESERVED_SLUGS } from "@/lib/constants";
|
|
||||||
|
|
||||||
export default function CreateEventPage() {
|
export default function CreateEventPage() {
|
||||||
const router = useRouter();
|
|
||||||
const { createEvent, isCreating } = useEvents();
|
const { createEvent, isCreating } = useEvents();
|
||||||
const [slugError, setSlugError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState<EventCreate>({
|
|
||||||
title: "",
|
|
||||||
description: "",
|
|
||||||
location_name: "",
|
|
||||||
location_address: "",
|
|
||||||
location_url: "",
|
|
||||||
event_date: "",
|
|
||||||
event_start_time: "",
|
|
||||||
event_end_time: "",
|
|
||||||
timezone: "UTC",
|
|
||||||
slug: "",
|
|
||||||
is_public: false,
|
|
||||||
rsvp_enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onChange = (
|
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
||||||
) => {
|
|
||||||
const { name, value, type } = e.target;
|
|
||||||
|
|
||||||
const checked =
|
|
||||||
type === "checkbox" ? (e.target as HTMLInputElement).checked : undefined;
|
|
||||||
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: type === "checkbox" ? checked : value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// live validation for slug
|
|
||||||
if (name === "slug") {
|
|
||||||
if (RESERVED_SLUGS.includes(value)) {
|
|
||||||
setSlugError(`The slug "${value}" is reserved.`);
|
|
||||||
} else {
|
|
||||||
setSlugError(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTogglePublic = (checked: boolean) => {
|
|
||||||
setFormData((prev) => ({ ...prev, is_public: checked }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (RESERVED_SLUGS.includes(formData.slug)) {
|
|
||||||
alert(
|
|
||||||
`The slug "${formData.slug}" is reserved and cannot be used. Please choose another slug.`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const event = await createEvent(formData);
|
|
||||||
router.push(`/dashboard/events/${event.slug}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to create event:", error);
|
|
||||||
alert("Failed to create event. Please check your data and try again.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Card className="shadow-lg">
|
||||||
<Card className="shadow-lg">
|
<CardHeader>
|
||||||
<CardHeader>
|
<CardTitle className="text-2xl">Create New Event</CardTitle>
|
||||||
<CardTitle className="text-2xl">Create New Event</CardTitle>
|
</CardHeader>
|
||||||
</CardHeader>
|
<CardContent>
|
||||||
<CardContent>
|
<EventForm
|
||||||
<form onSubmit={onSubmit} className="grid grid-cols-1 gap-6">
|
mode="create"
|
||||||
<div>
|
onSubmit={createEvent}
|
||||||
<Label className={"mb-2"}>Event Title *</Label>
|
isSubmitting={isCreating}
|
||||||
<Input
|
/>
|
||||||
required
|
</CardContent>
|
||||||
name="title"
|
</Card>
|
||||||
placeholder="My Awesome Event"
|
|
||||||
value={formData.title}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label className={"mb-2"}>Description</Label>
|
|
||||||
<Textarea
|
|
||||||
name="description"
|
|
||||||
placeholder="Quick description of your event"
|
|
||||||
value={formData.description || ""}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className={"mb-2"}>Event Date *</Label>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
required
|
|
||||||
name="event_date"
|
|
||||||
value={formData.event_date}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className={"mb-2"}>Slug *</Label>
|
|
||||||
<Input
|
|
||||||
required
|
|
||||||
name="slug"
|
|
||||||
pattern="^[a-z0-9-]+$"
|
|
||||||
placeholder="my-awesome-event"
|
|
||||||
value={formData.slug}
|
|
||||||
onChange={onChange}
|
|
||||||
className={slugError ? "border-red-500" : ""}
|
|
||||||
/>
|
|
||||||
{slugError && (
|
|
||||||
<p className="text-sm text-red-500 mt-1">{slugError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className={"mb-2"}>Start Time</Label>
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
name="event_start_time"
|
|
||||||
value={formData.event_start_time || ""}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className={"mb-2"}>End Time</Label>
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
name="event_end_time"
|
|
||||||
value={formData.event_end_time || ""}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label className={"mb-2"}>Location Name</Label>
|
|
||||||
<Input
|
|
||||||
name="location_name"
|
|
||||||
placeholder="Venue name"
|
|
||||||
value={formData.location_name || ""}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className={"mb-2"}>Location Address</Label>
|
|
||||||
<Input
|
|
||||||
name="location_address"
|
|
||||||
placeholder="123 Main Street"
|
|
||||||
value={formData.location_address || ""}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label className={"mb-2"}>Location URL</Label>
|
|
||||||
<Input
|
|
||||||
type="url"
|
|
||||||
name="location_url"
|
|
||||||
placeholder="https://maps.app/location"
|
|
||||||
value={formData.location_url || ""}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
id="is_public"
|
|
||||||
checked={formData.is_public}
|
|
||||||
onCheckedChange={onTogglePublic}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="is_public">Public Event</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 mt-4">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
type="button"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button disabled={isCreating} type="submit">
|
|
||||||
{isCreating ? "Creating..." : "Create Event"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
591
frontend/src/components/events/event-form.tsx
Normal file
591
frontend/src/components/events/event-form.tsx
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
// src/components/events/event-form.tsx
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { EventCreate, EventResponse, EventThemeResponse } from "@/client";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { RESERVED_SLUGS } from "@/lib/constants";
|
||||||
|
import { useEventThemes } from "@/context/event-theme-context";
|
||||||
|
|
||||||
|
// Define timezones - this should be expanded with a complete list
|
||||||
|
const TIMEZONES = [
|
||||||
|
"UTC",
|
||||||
|
"America/New_York",
|
||||||
|
"America/Chicago",
|
||||||
|
"America/Denver",
|
||||||
|
"America/Los_Angeles",
|
||||||
|
"Europe/London",
|
||||||
|
"Europe/Paris",
|
||||||
|
"Asia/Tokyo",
|
||||||
|
"Australia/Sydney",
|
||||||
|
];
|
||||||
|
|
||||||
|
type EventFormProps = {
|
||||||
|
event?: EventResponse;
|
||||||
|
// themes?: EventThemeResponse[];
|
||||||
|
mode: "create" | "edit";
|
||||||
|
onSubmit: (data: EventCreate) => Promise<EventResponse>;
|
||||||
|
isSubmitting?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EventForm({
|
||||||
|
event,
|
||||||
|
// themes = [],
|
||||||
|
mode,
|
||||||
|
onSubmit,
|
||||||
|
isSubmitting = false,
|
||||||
|
}: EventFormProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [slugError, setSlugError] = useState<string | null>(null);
|
||||||
|
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
|
||||||
|
const { themes } = useEventThemes();
|
||||||
|
const [formData, setFormData] = useState<EventCreate>({
|
||||||
|
title: event?.title || "",
|
||||||
|
description: event?.description || "",
|
||||||
|
location_name: event?.location_name || "",
|
||||||
|
location_address: event?.location_address || "",
|
||||||
|
location_url: event?.location_url || "",
|
||||||
|
event_date: event?.event_date || "",
|
||||||
|
event_start_time: event?.event_start_time || "",
|
||||||
|
event_end_time: event?.event_end_time || "",
|
||||||
|
timezone: event?.timezone || "UTC",
|
||||||
|
rsvp_deadline: event?.rsvp_deadline || "",
|
||||||
|
is_public: event?.is_public !== undefined ? event.is_public : false,
|
||||||
|
access_code: event?.access_code || "",
|
||||||
|
theme_id: event?.theme_id || null,
|
||||||
|
custom_theme_settings: event?.custom_theme_settings || null,
|
||||||
|
additional_info: event?.additional_info || null,
|
||||||
|
is_active: event?.is_active !== undefined ? event.is_active : true,
|
||||||
|
rsvp_enabled: event?.rsvp_enabled !== undefined ? event.rsvp_enabled : true,
|
||||||
|
gift_registry_enabled:
|
||||||
|
event?.gift_registry_enabled !== undefined
|
||||||
|
? event.gift_registry_enabled
|
||||||
|
: false,
|
||||||
|
updates_enabled:
|
||||||
|
event?.updates_enabled !== undefined ? event.updates_enabled : false,
|
||||||
|
max_guests_per_invitation: event?.max_guests_per_invitation || null,
|
||||||
|
contact_email: event?.contact_email || "",
|
||||||
|
contact_phone: event?.contact_phone || "",
|
||||||
|
slug: event?.slug || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
|
) => {
|
||||||
|
const { name, value, type } = e.target;
|
||||||
|
|
||||||
|
const checked =
|
||||||
|
type === "checkbox" ? (e.target as HTMLInputElement).checked : undefined;
|
||||||
|
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: type === "checkbox" ? checked : value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// live validation for slug
|
||||||
|
if (name === "slug" && mode === "create") {
|
||||||
|
if (RESERVED_SLUGS.includes(value)) {
|
||||||
|
setSlugError(`The slug "${value}" is reserved.`);
|
||||||
|
} else {
|
||||||
|
setSlugError(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear error for field when changed
|
||||||
|
if (formErrors[name]) {
|
||||||
|
setFormErrors((prev) => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
delete updated[name];
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectChange = (name: string, value: string | null) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleSwitch = (name: string, checked: boolean) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: checked,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!formData.title.trim()) {
|
||||||
|
errors.title = "Event title is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.event_date) {
|
||||||
|
errors.event_date = "Event date is required";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.slug.trim()) {
|
||||||
|
errors.slug = "Event slug is required";
|
||||||
|
} else if (mode === "create" && RESERVED_SLUGS.includes(formData.slug)) {
|
||||||
|
errors.slug = `The slug "${formData.slug}" is reserved`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.is_public === false && !formData.access_code) {
|
||||||
|
errors.access_code = "Access code is required for private events";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.contact_email && !validateEmail(formData.contact_email)) {
|
||||||
|
errors.contact_email = "Invalid email address";
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormErrors(errors);
|
||||||
|
return Object.keys(errors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateEmail = (email: string): boolean => {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await onSubmit(formData);
|
||||||
|
router.push(`/dashboard/events/${result.slug}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to ${mode} event:`, error);
|
||||||
|
alert(`Failed to ${mode} event. Please check your data and try again.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-6">
|
||||||
|
{/* Basic Event Information */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Basic Information</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="title" className="mb-2">
|
||||||
|
Event Title *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
required
|
||||||
|
name="title"
|
||||||
|
placeholder="My Awesome Event"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={onChange}
|
||||||
|
className={formErrors.title ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{formErrors.title && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{formErrors.title}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="description" className="mb-2">
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
placeholder="Quick description of your event"
|
||||||
|
value={formData.description || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="theme_id" className="mb-2">
|
||||||
|
Event Theme
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
name="theme_id"
|
||||||
|
value={formData.theme_id || ""}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onSelectChange("theme_id", value === "none" ? null : value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="theme_id">
|
||||||
|
<SelectValue placeholder="Select a theme" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
{themes &&
|
||||||
|
themes.map((theme) => (
|
||||||
|
<SelectItem key={theme.id} value={theme.id}>
|
||||||
|
{theme.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Choose a visual theme for your event or select 'None' for a default
|
||||||
|
appearance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date & Time Information */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Date & Time</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="event_date" className="mb-2">
|
||||||
|
Event Date *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="event_date"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
name="event_date"
|
||||||
|
value={formData.event_date}
|
||||||
|
onChange={onChange}
|
||||||
|
className={formErrors.event_date ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{formErrors.event_date && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{formErrors.event_date}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="event_start_time" className="mb-2">
|
||||||
|
Start Time
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="event_start_time"
|
||||||
|
type="time"
|
||||||
|
name="event_start_time"
|
||||||
|
value={formData.event_start_time || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="event_end_time" className="mb-2">
|
||||||
|
End Time
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="event_end_time"
|
||||||
|
type="time"
|
||||||
|
name="event_end_time"
|
||||||
|
value={formData.event_end_time || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="timezone" className="mb-2">
|
||||||
|
Timezone *
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
name="timezone"
|
||||||
|
value={formData.timezone}
|
||||||
|
onValueChange={(value) => onSelectChange("timezone", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="timezone">
|
||||||
|
<SelectValue placeholder="Select timezone" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIMEZONES.map((timezone) => (
|
||||||
|
<SelectItem key={timezone} value={timezone}>
|
||||||
|
{timezone}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="rsvp_deadline" className="mb-2">
|
||||||
|
RSVP Deadline
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="rsvp_deadline"
|
||||||
|
type="date"
|
||||||
|
name="rsvp_deadline"
|
||||||
|
value={formData.rsvp_deadline || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location Information */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Location</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="location_name" className="mb-2">
|
||||||
|
Location Name
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="location_name"
|
||||||
|
name="location_name"
|
||||||
|
placeholder="Venue Name"
|
||||||
|
value={formData.location_name || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="location_address" className="mb-2">
|
||||||
|
Address
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="location_address"
|
||||||
|
name="location_address"
|
||||||
|
placeholder="Full address"
|
||||||
|
value={formData.location_address || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="location_url" className="mb-2">
|
||||||
|
Location URL
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="location_url"
|
||||||
|
name="location_url"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://maps.example.com/venue"
|
||||||
|
value={formData.location_url || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Optional: Add a map link or venue website
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Information */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Contact Information</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="contact_email" className="mb-2">
|
||||||
|
Contact Email
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="contact_email"
|
||||||
|
name="contact_email"
|
||||||
|
type="email"
|
||||||
|
placeholder="event@example.com"
|
||||||
|
value={formData.contact_email || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
className={formErrors.contact_email ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{formErrors.contact_email && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{formErrors.contact_email}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="contact_phone" className="mb-2">
|
||||||
|
Contact Phone
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="contact_phone"
|
||||||
|
name="contact_phone"
|
||||||
|
type="tel"
|
||||||
|
placeholder="+1 (555) 123-4567"
|
||||||
|
value={formData.contact_phone || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Event Settings */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Event Settings</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="slug" className="mb-2">
|
||||||
|
Event Slug *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="slug"
|
||||||
|
required
|
||||||
|
name="slug"
|
||||||
|
placeholder="my-event"
|
||||||
|
value={formData.slug}
|
||||||
|
onChange={onChange}
|
||||||
|
disabled={mode === "edit"}
|
||||||
|
className={formErrors.slug ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{formErrors.slug && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">{formErrors.slug}</p>
|
||||||
|
)}
|
||||||
|
{mode === "create" && (
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
This will be used as the URL for your event
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="max_guests_per_invitation" className="mb-2">
|
||||||
|
Max Guests Per RSVP
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="max_guests_per_invitation"
|
||||||
|
name="max_guests_per_invitation"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="1"
|
||||||
|
value={
|
||||||
|
formData.max_guests_per_invitation === null
|
||||||
|
? ""
|
||||||
|
: formData.max_guests_per_invitation
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value
|
||||||
|
? parseInt(e.target.value, 10)
|
||||||
|
: null;
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
max_guests_per_invitation: value,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Leave empty for unlimited guests per invitation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Switch
|
||||||
|
id="is_public"
|
||||||
|
checked={formData.is_public}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onToggleSwitch("is_public", checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="is_public">Make event public</Label>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground ml-10">
|
||||||
|
Public events can be viewed by anyone with the link
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!formData.is_public && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="access_code" className="mb-2">
|
||||||
|
Access Code *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="access_code"
|
||||||
|
name="access_code"
|
||||||
|
placeholder="secret-code"
|
||||||
|
value={formData.access_code || ""}
|
||||||
|
onChange={onChange}
|
||||||
|
className={formErrors.access_code ? "border-red-500" : ""}
|
||||||
|
required={!formData.is_public}
|
||||||
|
/>
|
||||||
|
{formErrors.access_code && (
|
||||||
|
<p className="text-red-500 text-sm mt-1">
|
||||||
|
{formErrors.access_code}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Required for private events - guests will need this code to access
|
||||||
|
the event
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="rsvp_enabled"
|
||||||
|
checked={formData.rsvp_enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onToggleSwitch("rsvp_enabled", checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="rsvp_enabled">Enable RSVP functionality</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="gift_registry_enabled"
|
||||||
|
checked={formData.gift_registry_enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onToggleSwitch("gift_registry_enabled", checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="gift_registry_enabled">Enable Gift Registry</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="updates_enabled"
|
||||||
|
checked={formData.updates_enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onToggleSwitch("updates_enabled", checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="updates_enabled">Enable Event Updates</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="is_active"
|
||||||
|
checked={formData.is_active}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onToggleSwitch("is_active", checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="is_active">Event Active</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Actions */}
|
||||||
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
|
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting
|
||||||
|
? "Saving..."
|
||||||
|
: mode === "create"
|
||||||
|
? "Create Event"
|
||||||
|
: "Update Event"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user