bb286e1667
Convert inline styles to reusable CSS classes for better maintainability
201 lines
6.5 KiB
TypeScript
201 lines
6.5 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { format } from "date-fns";
|
|
import Button from "@/app/ui/Button";
|
|
import { useEventsStore } from "@/hooks/useEventsStore";
|
|
import type { Event } from "@timetoleave/core";
|
|
|
|
/**
|
|
* Modal for creating or editing a manually added event.
|
|
*
|
|
* Collects title, destination, date, and time. In edit mode, populates
|
|
* fields from the existing `editEvent` and calls `updateEvent` instead of `addEvent`.
|
|
* Shows a brief success overlay before closing on submit.
|
|
*
|
|
* Closes on Escape key press or backdrop click.
|
|
*/
|
|
type AddEventModalProps = {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
editEvent?: Event;
|
|
className?: string;
|
|
};
|
|
|
|
const AddEventModal: React.FC<AddEventModalProps> = ({ isOpen, onClose, editEvent, className = "" }) => {
|
|
const isEditing = !!editEvent;
|
|
const [title, setTitle] = useState("");
|
|
const [destination, setDestination] = useState("");
|
|
const [eventTime, setEventTime] = useState("");
|
|
const [eventDate, setEventDate] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
const { addEvent, updateEvent } = useEventsStore();
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
if (editEvent) {
|
|
setTitle(editEvent.title);
|
|
setDestination(editEvent.destination);
|
|
setEventDate(format(editEvent.eventTime, "yyyy-MM-dd"));
|
|
setEventTime(format(editEvent.eventTime, "HH:mm"));
|
|
} else {
|
|
setTitle("");
|
|
setDestination("");
|
|
setEventDate("");
|
|
setEventTime("");
|
|
}
|
|
setSuccess(false);
|
|
}, [editEvent, isOpen]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!title.trim() || !destination.trim() || !eventTime.trim() || !eventDate.trim()) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const eventTimeDate = new Date(`${eventDate}T${eventTime}`);
|
|
|
|
if (isEditing && editEvent) {
|
|
updateEvent(editEvent.id, {
|
|
title: title.trim(),
|
|
destination: destination.trim(),
|
|
eventTime: eventTimeDate,
|
|
});
|
|
} else {
|
|
await addEvent({
|
|
id: `manual-${Date.now()}`,
|
|
title: title.trim(),
|
|
destination: destination.trim(),
|
|
eventTime: eventTimeDate,
|
|
source: "manual",
|
|
});
|
|
}
|
|
|
|
setSuccess(true);
|
|
setTimeout(() => {
|
|
onClose();
|
|
setSuccess(false);
|
|
}, 1200);
|
|
} catch (err) {
|
|
console.error("Error saving event:", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
if (e.target === e.currentTarget) onClose();
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose();
|
|
};
|
|
if (isOpen) document.addEventListener("keydown", handleEscape);
|
|
return () => document.removeEventListener("keydown", handleEscape);
|
|
}, [isOpen, onClose]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm"
|
|
onClick={handleBackdropClick}
|
|
>
|
|
<div className={`brand-panel relative w-full max-w-md ${className}`}>
|
|
<div className="p-6">
|
|
<p className="mb-2 eyebrow">
|
|
{isEditing ? "Edit event" : "Manual event"}
|
|
</p>
|
|
<h3 className="mb-6 text-2xl font-bold text-white">
|
|
{isEditing ? "Edit departure target" : "Add a departure target"}
|
|
</h3>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label htmlFor="event-title" className="mb-1 block text-sm font-medium text-[#F4F1EA]/76">
|
|
Event Title
|
|
</label>
|
|
<input
|
|
id="event-title"
|
|
type="text"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="Team Meeting"
|
|
className="brand-input"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="event-destination" className="mb-1 block text-sm font-medium text-[#F4F1EA]/76">
|
|
Destination
|
|
</label>
|
|
<input
|
|
id="event-destination"
|
|
type="text"
|
|
value={destination}
|
|
onChange={(e) => setDestination(e.target.value)}
|
|
placeholder="Technikum Wien or Hoechstaedtplatz 6, 1200 Wien"
|
|
className="brand-input"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label htmlFor="event-date" className="mb-1 block text-sm font-medium text-[#F4F1EA]/76">
|
|
Date
|
|
</label>
|
|
<input
|
|
id="event-date"
|
|
type="date"
|
|
value={eventDate}
|
|
onChange={(e) => setEventDate(e.target.value)}
|
|
className="brand-input"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="event-time" className="mb-1 block text-sm font-medium text-[#F4F1EA]/76">
|
|
Time
|
|
</label>
|
|
<input
|
|
id="event-time"
|
|
type="time"
|
|
value={eventTime}
|
|
onChange={(e) => setEventTime(e.target.value)}
|
|
className="brand-input"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end space-x-3 mt-6">
|
|
<Button type="button" variant="secondary" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={loading}>
|
|
{isEditing ? "Save Changes" : "Add Event"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
{success && (
|
|
<div className="absolute inset-0 flex items-center justify-center rounded-2xl bg-[#090816]/90 backdrop-blur-sm">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-[#34C759] to-[#30d158] text-3xl shadow-lg">
|
|
✓
|
|
</div>
|
|
<p className="text-lg font-semibold text-white">
|
|
{isEditing ? "Changes saved" : "Event added"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AddEventModal;
|