diff --git a/client/app/(app)/calendar/page.tsx b/client/app/(app)/calendar/page.tsx index 457b22e..4b3cf95 100644 --- a/client/app/(app)/calendar/page.tsx +++ b/client/app/(app)/calendar/page.tsx @@ -2,15 +2,17 @@ import React from "react"; import CalendarPage from "@/components/calendar/calendar"; import { getEvents } from "@/actions/events"; import { getCurrentTime } from "@/actions/setTime"; -import { SelfieEvent } from "@/helpers/types"; +import { People, SelfieEvent } from "@/helpers/types"; +import { getFriends } from "@/actions/friends"; const Page = async () => { try { - const [events, dbDate]: [SelfieEvent[], Date] = await Promise.all([ + const [events, dbDate, friends]: [SelfieEvent[], Date, People] = await Promise.all([ getEvents(), getCurrentTime(), + getFriends(), ]); - return ; + return ; } catch (error) { console.log(error); } diff --git a/client/components/calendar/calendar.tsx b/client/components/calendar/calendar.tsx index d0fefaf..dfdcf47 100644 --- a/client/components/calendar/calendar.tsx +++ b/client/components/calendar/calendar.tsx @@ -1,14 +1,15 @@ "use client"; -import { Calendar, Chip, Button, Tooltip } from "@nextui-org/react"; +import { Chip, Button, Tooltip } from "@nextui-org/react"; import React, { useState, useEffect } from "react"; import EventAdder from "@/components/calendar/eventAdder"; import CalendarCell from "@/components/calendar/calendarCell"; -import { SelfieEvent } from "@/helpers/types"; +import { SelfieEvent, People } from "@/helpers/types"; import { reloadContext, mobileContext } from "./reloadContext" interface CalendarPageProps { initialEvents: SelfieEvent[]; dbdate: Date; + friends: People; } const CalendarPage = (props: CalendarPageProps) => { @@ -79,8 +80,8 @@ const CalendarPage = (props: CalendarPageProps) => { useEffect(() => { if (reloadEvents) { - console.log("sto fetchando"); - setCurrentTime(); + console.log("sto fetchando gli eventi"); + //setCurrentTime(); setAllEvents(); setReloadEvents(false); } @@ -190,6 +191,7 @@ const CalendarPage = (props: CalendarPageProps) => { < { delay={0} closeDelay={0} placement="top" - className="text-white border-2 border-purple-600 bg-violet-400 text-black" - > + classNames={{ + base: [ + "before:bg-neutral-400 dark:before:bg-white", + ], + content: [ + "py-2 px-4 shadow-xl", + "text-black bg-gradient-to-br from-white to-neutral-400", + ], + }}> {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()} @@ -211,7 +220,7 @@ const CalendarPage = (props: CalendarPageProps) => { @@ -230,7 +239,7 @@ const CalendarPage = (props: CalendarPageProps) => { (day) => ( {day} diff --git a/client/components/calendar/calendarCell.tsx b/client/components/calendar/calendarCell.tsx index 60dc747..4db23ef 100644 --- a/client/components/calendar/calendarCell.tsx +++ b/client/components/calendar/calendarCell.tsx @@ -1,7 +1,7 @@ "use client"; -import React, { useState } from "react"; -import { Card, CardBody } from "@nextui-org/react"; +import React, { useState, useEffect } from "react"; +import { Card, CardBody, Modal, ModalContent, ModalHeader, ModalBody, Button } from "@nextui-org/react"; import ShowEvent from './showEvent' import { SelfieEvent } from "@/helpers/types"; @@ -13,37 +13,118 @@ const areSameDay = (date1: Date, date2: Date): boolean => { ); }; +const isAM = (date: Date): boolean => { + return date.getHours() < 12; +}; + const getEventsByDay = ( events: SelfieEvent[] | undefined, date: Date, ): SelfieEvent[] => { if (!Array.isArray(events)) { - return []; // Restituisci un array vuoto invece di lanciare un errore + return []; } - return events.filter((event) => { - const eventDate = new Date(event.dtstart); - return areSameDay(eventDate, date); + const filteredEvents = events.filter((event) => { + const eventDateStart = new Date(event.dtstart); + const eventDateEnd = new Date(event.dtend); + return ( + (date.getTime() <= eventDateEnd.getTime() && date.getTime() >= eventDateStart.getTime() + ) || ( + areSameDay(date, eventDateStart))) + ? + true : false; + }); + + // Sort events by AM/PM first, then by time + return filteredEvents.sort((a, b) => { + const dateA = new Date(a.dtstart); + const dateB = new Date(b.dtstart); + + // Compare AM/PM first + const aIsAM = isAM(dateA); + const bIsAM = isAM(dateB); + + if (aIsAM && !bIsAM) return -1; + if (!aIsAM && bIsAM) return 1; + + // If both are AM or both are PM, sort by time + return dateA.getTime() - dateB.getTime(); + }); +}; + +const formatDate = (date: Date): string => { + return date.toLocaleDateString('it-IT', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' }); }; +const useIsMobile = () => { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkIsMobile = () => { + setIsMobile(window.innerWidth < 768); // 768px is typical mobile breakpoint + }; + + checkIsMobile(); + window.addEventListener('resize', checkIsMobile); + + return () => { + window.removeEventListener('resize', checkIsMobile); + }; + }, []); + + return isMobile; +}; + const showEvents = ( events: SelfieEvent[] | undefined, date: Date, handleOpen: (event: SelfieEvent) => void, + isMobile: boolean ): JSX.Element[] => { const todayEvents = getEventsByDay(events, date); - return todayEvents.map((event, index) => ( + const eventsToShow = todayEvents.slice(0, 2); + + return eventsToShow.map((event, index) => ( )); }; +const monthNames = [ + "Gennaio", + "Febbraio", + "Marzo", + "Aprile", + "Maggio", + "Giugno", + "Luglio", + "Agosto", + "Settembre", + "Ottobre", + "Novembre", + "Dicembre", +]; + interface CalendarCellProps { day: number; date: Date; @@ -55,43 +136,96 @@ const CalendarCell: React.FC = ({ day, date, isToday, - events = [], // Questo giĆ  fornisce un default, ma assicuriamoci che sia un array + events = [], }) => { + const [selectedEvents, setSelectedEvents] = useState(null); + const [isOpenSE, setIsOpenSE] = useState(false); + const [isAllEventsOpen, setIsAllEventsOpen] = useState(false); + const isMobile = useIsMobile(); const cellDate = new Date(date.getFullYear(), date.getMonth(), day); const safeEvents = Array.isArray(events) ? events : []; - const [selectedEvents, setSelectedEvents] = useState(null) - const [isOpen, setIsOpen] = useState(false); + const todayEvents = getEventsByDay(safeEvents, cellDate); + const hasMoreEvents = todayEvents.length > 2; const handleClose = () => { setSelectedEvents(null); - setIsOpen(false); + setIsOpenSE(false); }; const handleOpen = (e: SelfieEvent) => { setSelectedEvents(e); - setIsOpen(true); + setIsOpenSE(true); + }; + + const formatEventTime = (event: SelfieEvent) => { + const eventDate = new Date(event.dtstart); + const timeString = eventDate.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); + return `${timeString}`; }; return ( -
setIsAllEventsOpen(true)} + className={`justify-end rounded-[100px] text-sm font-bold ${isToday ? "text-slate-200 bg-[#9353d3] border-2 border-slate-300" : "bg-slate-800 text-white dark:text-white"}`} > {day} -
-
- {showEvents(safeEvents, cellDate, handleOpen)} + +
+ {showEvents(safeEvents, cellDate, handleOpen, isMobile)} + {hasMoreEvents && ( + + )}
+ - - + setIsAllEventsOpen(false)} + size="md" + > + + Eventi del {day} {monthNames[date.getMonth()]} + +
+ {todayEvents.map((event, index) => ( +
{ + handleOpen(event); + setIsAllEventsOpen(false); + }} + > +

+ + {formatEventTime(event)} + + {" - "} + {event.title.toString()} +

+

+ {formatDate(new Date(event.dtstart))} +

+
+ ))} +
+
+
+
+ ); }; diff --git a/client/components/calendar/eventAdder.tsx b/client/components/calendar/eventAdder.tsx index c4d042f..80bcb71 100644 --- a/client/components/calendar/eventAdder.tsx +++ b/client/components/calendar/eventAdder.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useContext } from "react"; +import React, { useState, useContext, useEffect } from "react"; import { Button, Modal, @@ -11,6 +11,13 @@ import { Input, Textarea, Switch, + Autocomplete, + AutocompleteItem, + Avatar, + Dropdown, + DropdownTrigger, + DropdownMenu, + DropdownItem, } from "@nextui-org/react"; import RepetitionMenu from "@/components/calendar/repetitionMenu"; import EventDatePicker from "@/components/calendar/eventDatePicker"; @@ -18,7 +25,7 @@ import { SelfieEvent, SelfieNotification, FrequencyType, - Person, + People, } from "@/helpers/types"; import NotificationMenu from "./notificationMenu"; const EVENTS_API_URL = "/api/events"; @@ -32,7 +39,7 @@ async function createEvent(event: SelfieEvent): Promise { "Content-Type": "application/json", }, body: JSON.stringify({ event: event }), - cache: "no-store", // This ensures fresh data on every request + cache: "no-store", }); if (res.status === 401) { @@ -48,75 +55,135 @@ async function createEvent(event: SelfieEvent): Promise { return true; } -export default function EventAdder() { - const [isOpen, setIsOpen] = useState(false); - const [eventData, setEventData] = useState>({ +const initialEvent = { + title: "", + summary: "", + status: "confirmed", + transp: "OPAQUE", + dtstart: new Date(), + dtend: new Date(), + dtstamp: new Date().toISOString(), + categories: [""], + location: "", + description: "", + URL: "", + participants: [] as People, + rrule: { + freq: "weekly" as FrequencyType, + interval: 1, + bymonth: 1, + bymonthday: 1, + }, + notification: { title: "", - summary: "", - status: "confirmed", - transp: "OPAQUE", - dtstart: new Date(), - dtend: new Date(), - dtstamp: new Date().toISOString(), - categories: [""], - location: "", description: "", - URL: "", - participants: [] as Person[], - rrule: { - freq: "weekly", - interval: 1, - bymonth: 1, - bymonthday: 1, - }, - notification: { - title: "", - description: "", - type: "", - repetition: { - freq: "", - interval: 1, - }, - fromDate: new Date(), + type: "", + repetition: { + freq: "", + interval: 0, }, - }); + fromDate: new Date(), + }, +}; + +interface EventAdderProps { + friends: People; +} + +const EventAdder: React.FC = ({ + friends, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [eventData, setEventData] = useState>(initialEvent); const [repeatEvent, setRepeatEvent] = useState(false); const [allDayEvent, setAllDayEvent] = useState(false); const [notifications, setNotifications] = useState(false); + const [isError, setIsError] = useState(false); + const [notificationError, setNotificationError] = useState(false); const { reloadEvents, setReloadEvents } = useContext(reloadContext) as any; + const availableFriends = friends.filter(friend => + !eventData.participants?.some(participant => participant.email === friend.email) + ); + + useEffect(() => { + setEventData((prev: any) => ({ + ...prev, + notification: { + ...prev.notification, + title: prev.notification?.title || "", + description: prev.notification?.description || "", + type: "", + fromDate: new Date(), + repetition: { + freq: "", + interval: 0, + }, + } + })); + console.log(eventData.notification); + }, [eventData.dtstart, eventData.dtend]); + const handleOpen = () => { setIsOpen(true); }; - const handleClose = () => { - setIsOpen(false); - }; - const handleInputChange = ( e: | React.ChangeEvent | { target: { name: string; value: any } }, ) => { - var { name, value } = e.target; - if (name.startsWith("notification.")) { - const notificationField = name.split(".")[1]; - if (name.endsWith("fromDate")) { - value = new Date(value).toISOString(); - } - setEventData((prev: any) => ({ - ...prev, - notification: { - ...prev.notification, - [notificationField]: value, - }, - })); - console.log("name: ", name, "value: ", value); + const { name, value } = e.target; + + if (name.startsWith("title")) { + setIsError(false); + } + + if (name.startsWith("notification")) { + const [_, notificationField, repetitionField] = name.split("."); + + setEventData((prev: any) => { + // Se stiamo gestendo un campo di repetition + if (notificationField === "repetition") { + return { + ...prev, + notification: { + ...prev.notification, + repetition: { + ...prev.notification.repetition, + [repetitionField]: value + } + } + }; + } + + // Se stiamo gestendo fromDate + if (notificationField === "fromDate") { + return { + ...prev, + notification: { + ...prev.notification, + fromDate: new Date(value).toISOString() + } + }; + } + + // Per tutti gli altri campi della notification + return { + ...prev, + notification: { + ...prev.notification, + [notificationField]: value + } + }; + }); } else { - setEventData((prev) => ({ ...prev, [name]: value })); + // Per tutti i campi non correlati alla notification + setEventData(prev => ({ ...prev, [name]: value })); } }; + const handleDateChange = (start: Date | string, end: Date | string) => { setEventData((prev) => ({ ...prev, @@ -155,37 +222,76 @@ export default function EventAdder() { })); }; + const handleParticipantSelect = (friend: any) => { + setEventData((prev) => ({ + ...prev, + participants: [...(prev.participants || []), friend], + })); + }; + + const handleRemoveParticipant = (friendToRemove: any) => { + setEventData((prev) => ({ + ...prev, + participants: (prev.participants || []).filter( + (friend) => friend.email !== friendToRemove.email + ), + })); + }; + + const handleRemoveAllParticipants = () => { + setEventData((prev) => ({ + ...prev, + participants: [], + })); + }; + + const handleExit = () => { + setIsOpen(false); + setIsError(false); + setNotificationError(false); + setEventData(initialEvent); + } + + const handleSave = () => { + if (eventData.title === "") { + setIsError(true); + } else { + setIsError(false); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const newEvent: SelfieEvent = { - ...eventData, - sequence: 0, - categories: eventData.categories || [], - participants: eventData.participants || [], - } as SelfieEvent; - - try { - console.log("newEvent: ", newEvent); - const success = await createEvent(newEvent); - console.log("success: ", success); - if (success) { - console.log("Event created successfully"); - handleClose(); - } else { - console.error("Failed to create event"); + console.log(isError, notificationError); + if (!isError && !notificationError) { + const newEvent: SelfieEvent = { + ...eventData, + sequence: 0, + categories: eventData.categories || [], + participants: eventData.participants || [], + } as SelfieEvent; + + try { + const success = await createEvent(newEvent); + if (success) { + console.log("Event created successfully"); + handleExit(); + } else { + console.error("Failed to create event"); + } + } catch (error) { + console.error("Error submitting event", error); } - } catch (error) { - console.error("Error submitting event", error); - } - setReloadEvents(true); + setReloadEvents(true); + } }; return ( <> + + + {eventData.participants?.length ? ( + eventData.participants.map((participant) => ( + handleRemoveParticipant(participant)} + > + Rimuovi + + } + > +
+ +
+ {participant.username} + {participant.email} +
+
+
+ )) + ) : ( + + Nessun invitato + + )} +
+ + + +
+ - @@ -329,3 +516,5 @@ export default function EventAdder() { ); } + +export default EventAdder; diff --git a/client/components/calendar/eventDatePicker.tsx b/client/components/calendar/eventDatePicker.tsx index ee79be0..3bacab2 100644 --- a/client/components/calendar/eventDatePicker.tsx +++ b/client/components/calendar/eventDatePicker.tsx @@ -7,6 +7,7 @@ import { DateValue, } from "@nextui-org/react"; import { mobileContext } from "./reloadContext" +import { parseDateTime } from "@internationalized/date"; interface EventDatePickerProps { isAllDay: boolean; @@ -27,6 +28,26 @@ const EventDatePicker: React.FC = ({ } }; + const getDefaultDateRange = (isAllDay: boolean) => { + const today = new Date(); + + if (isAllDay) { + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + return { + start: parseDateTime(today.toISOString().split('T')[0]), + end: parseDateTime(tomorrow.toISOString().split('T')[0]) + }; + } else { + const nextHour = new Date(today); + nextHour.setHours(today.getHours() + 1); + return { + start: parseDateTime(today.toISOString().split('T')[0]), + end: parseDateTime(nextHour.toISOString().split('T')[0]) + }; + } + }; + return ( = ({ visibleMonths={isMobile ? 1 : 2} onChange={handleDateRangeChange} granularity={isAllDay ? "day" : "minute"} + defaultValue={getDefaultDateRange(isAllDay)} /> ); }; diff --git a/client/components/calendar/notificationMenu.tsx b/client/components/calendar/notificationMenu.tsx index 5fa7e2d..3682f55 100644 --- a/client/components/calendar/notificationMenu.tsx +++ b/client/components/calendar/notificationMenu.tsx @@ -2,41 +2,57 @@ import React from "react"; import { Input, Select, SelectItem } from "@nextui-org/react"; import { DatePicker } from "@nextui-org/react"; import { SelfieNotification } from "@/helpers/types"; +import { parseDateTime } from "@internationalized/date"; interface NotificationMenuProps { value: boolean; notification: SelfieNotification; onChange: (event: { target: { name: string; value: any } }) => void; - eventDate: Date; + startEventDate: Date; + isAllDay: boolean; + notificationError: boolean; + setNotificationError: React.Dispatch>; } const NotificationMenu: React.FC = ({ value, notification, onChange, + startEventDate, + isAllDay, + notificationError, + setNotificationError, }) => { if (!value) return null; const handleNotificationChange = (name: string, value: any) => { - onChange({ - target: { - name: `notification.${name}`, - value, - }, - }); + if (name.startsWith("fromDate")) { + if (new Date(value).getTime() > startEventDate.getTime()) + setNotificationError(true); + else + setNotificationError(false); + } + if (!notificationError) { + onChange({ + target: { + name: `notification.${name}`, + value, + }, + }); + } }; return (
handleNotificationChange("title", e.target.value)} placeholder="Inserisci il titolo della notifica" /> handleNotificationChange("description", e.target.value) } @@ -44,8 +60,9 @@ const NotificationMenu: React.FC = ({ /> handleNotificationChange("fromDate", date)} + isInvalid={notificationError} + granularity={isAllDay ? "day" : "minute"} + errorMessage="Inserire una data prima dell'inizio dell'evento" /> handleNotificationChange("repetition.interval", e)} + value={notification?.repetition?.interval?.toString() || ""} + onChange={(e) => handleNotificationChange("repetition.interval", e.target.value)} placeholder="Inserisci l'intervallo di ripetizione" />
diff --git a/server/src/models/user-model.js b/server/src/models/user-model.js index d8eaa5a..d897d68 100644 --- a/server/src/models/user-model.js +++ b/server/src/models/user-model.js @@ -1,5 +1,4 @@ import { mongoose } from "mongoose"; -import { activitySchema } from "./event-model.js"; export const userSchema = new mongoose.Schema({ //campi user di default per l'iscrizione