From fb619bdcd82d87070dca5c102eec7824e9546151 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Wed, 6 Nov 2024 19:59:32 -0800 Subject: [PATCH 01/17] feat (wip): scaffold --- src/app/page.tsx | 4 +- src/components/creation/calendar/calendar.tsx | 0 src/components/creation/creation.tsx | 108 ++++++++++++++++++ .../creation/fields/meeting-name-field.tsx | 42 +++++++ .../creation/fields/meeting-time-field.tsx | 0 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/components/creation/calendar/calendar.tsx create mode 100644 src/components/creation/creation.tsx create mode 100644 src/components/creation/fields/meeting-name-field.tsx create mode 100644 src/components/creation/fields/meeting-time-field.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 88b69b01..f8c42b35 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,5 @@ +import { Creation } from "@/components/creation/creation"; + export default function Home() { - return
ZotMeet
; + return ; } diff --git a/src/components/creation/calendar/calendar.tsx b/src/components/creation/calendar/calendar.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/creation/creation.tsx b/src/components/creation/creation.tsx new file mode 100644 index 00000000..0ac9bb98 --- /dev/null +++ b/src/components/creation/creation.tsx @@ -0,0 +1,108 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { MeetingNameField } from "@/components/creation/fields/meeting-name-field"; +import { cn } from "@/lib/utils"; +import { ZotDate } from "@/lib/zotdate"; + +// import Calendar from "./creation"; + +// import MeetingTimeField from "./MeetingTimeField"; + +export function Creation() { + const [selectedDays, setSelectedDays] = useState([]); + const [startTime, setStartTime] = useState(null); + const [endTime, setEndTime] = useState(null); + const [meetingName, setMeetingName] = useState(""); + const router = useRouter(); + + const handleCreation = async () => { + const body = { + title: meetingName, + fromTime: startTime, + toTime: endTime, + meetingDates: selectedDays.map((zotDate) => + zotDate.day.toISOString() + ), + description: "", + }; + + const response = await fetch("/api/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + console.error("Failed to create meeting: ", response.statusText); + return; + } + + const { meetingId } = await response.json(); + + if (!meetingId) { + console.error("Failed to create meeting. Meeting ID not found."); + return; + } + + router.push(`/availability/${meetingId}`); + }; + + return ( +
+
+

+ Let's plan your next meeting. +

+

+ Select potential dates and times for you and your team. +

+
+ +
+
+ + {/* */} +
+
+ + + +
+

+ {selectedDays.length} days selected +

+ +
+
+ ); +} diff --git a/src/components/creation/fields/meeting-name-field.tsx b/src/components/creation/fields/meeting-name-field.tsx new file mode 100644 index 00000000..ff660c2e --- /dev/null +++ b/src/components/creation/fields/meeting-name-field.tsx @@ -0,0 +1,42 @@ +"use client"; + +import React, { Dispatch, SetStateAction, useEffect, useRef } from "react"; + +const DEFAULT_MEETING_NAME = ""; + +interface MeetingNameFieldProps { + meetingName: string; + setMeetingName: Dispatch>; +} + +export function MeetingNameField({ + meetingName, + setMeetingName, +}: MeetingNameFieldProps) { + const inputRef = useRef(null); + + useEffect(() => { + setMeetingName(DEFAULT_MEETING_NAME); + + if (inputRef.current) { + inputRef.current.focus(); + } + }, [setMeetingName]); + + const handleChange = (e: React.ChangeEvent) => { + setMeetingName(e.target.value); + }; + + return ( +
+ +
+ ); +} diff --git a/src/components/creation/fields/meeting-time-field.tsx b/src/components/creation/fields/meeting-time-field.tsx new file mode 100644 index 00000000..e69de29b From 303fc197b16bca28be1362a61cc9130d42895ed8 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Thu, 7 Nov 2024 00:44:15 -0800 Subject: [PATCH 02/17] feat: calendar scaffolded --- .../creation/calendar/calendar-body-day.tsx | 39 +++++ .../creation/calendar/calendar-body.tsx | 127 ++++++++++++++ src/components/creation/calendar/calendar.tsx | 157 ++++++++++++++++++ src/components/creation/creation.tsx | 3 +- 4 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 src/components/creation/calendar/calendar-body-day.tsx create mode 100644 src/components/creation/calendar/calendar-body.tsx diff --git a/src/components/creation/calendar/calendar-body-day.tsx b/src/components/creation/calendar/calendar-body-day.tsx new file mode 100644 index 00000000..a8852660 --- /dev/null +++ b/src/components/creation/calendar/calendar-body-day.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { cn } from "@/lib/utils"; +import { ZotDate } from "@/lib/zotdate"; + +interface CalendarBodyDayProps { + isHighlighted: boolean; + calendarDay: ZotDate; + isCurrentMonth: boolean; +} + +export function CalendarBodyDay({ + isHighlighted, + calendarDay, + isCurrentMonth, +}: CalendarBodyDayProps) { + const isSelected = calendarDay.isSelected; + + return ( +

+ {calendarDay.getDay()} +

+ ); +} diff --git a/src/components/creation/calendar/calendar-body.tsx b/src/components/creation/calendar/calendar-body.tsx new file mode 100644 index 00000000..bcbd0032 --- /dev/null +++ b/src/components/creation/calendar/calendar-body.tsx @@ -0,0 +1,127 @@ +import React, { useState } from "react"; +import { CalendarBodyDay } from "@/components/creation/calendar/calendar-body-day"; +import { ZotDate } from "@/lib/zotdate"; + +interface CalendarBodyProps { + calendarDays: ZotDate[][]; + updateCalendar: VoidFunction; + currentMonth: number; + updateSelectedRange: (startDate: ZotDate, endDate: ZotDate) => void; +} + +export function CalendarBody({ + calendarDays, + updateCalendar, + currentMonth, + updateSelectedRange, +}: CalendarBodyProps) { + const [startDaySelection, setStartDaySelection] = useState( + null + ); + const [endDaySelection, setEndDaySelection] = useState( + null + ); + + /** + * Updates the current highlight selection whenever a mobile user drags on the calendar + * @param {TouchEvent} e - Touch event from a mobile user + */ + const handleTouchMove = (e: React.TouchEvent) => { + const touchingElement = document.elementFromPoint( + e.touches[0].clientX, + e.touches[0].clientY + ); + + if (!touchingElement) return; + + const touchingDay = touchingElement.getAttribute("data-day"); + + if (startDaySelection && touchingDay) { + const day = ZotDate.extractDayFromElement(touchingElement); + setEndDaySelection(day); + } + }; + + /* Confirms the current highlight selection and updates calendar accordingly */ + const handleEndSelection = () => { + if (startDaySelection && endDaySelection) { + try { + updateSelectedRange(startDaySelection, endDaySelection); + } catch (err) { + console.error(err); + } + } + + updateCalendar(); + + setStartDaySelection(null); + setEndDaySelection(null); + }; + + return ( + + {calendarDays.map((calendarWeek, weekIndex) => ( + + {calendarWeek.map((calendarDay, dayIndex) => { + const isHighlighted = + startDaySelection && + endDaySelection && + calendarDay.determineDayWithinBounds( + startDaySelection, + endDaySelection + ); + + const isCurrentMonth = + currentMonth === calendarDay.getMonth(); + + return ( + { + if (startDaySelection) { + setEndDaySelection(calendarDay); + handleEndSelection(); + } + }} + > + + + ); + })} + + ))} + + ); +} diff --git a/src/components/creation/calendar/calendar.tsx b/src/components/creation/calendar/calendar.tsx index e69de29b..7cf4ccc8 100644 --- a/src/components/creation/calendar/calendar.tsx +++ b/src/components/creation/calendar/calendar.tsx @@ -0,0 +1,157 @@ +import React, { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from "react"; +import { CalendarBody } from "@/components/creation/calendar/calendar-body"; +import { MONTHS, WEEKDAYS } from "@/lib/types/chrono"; +import { ZotDate } from "@/lib/zotdate"; + +interface CalendarProps { + selectedDays: ZotDate[]; + setSelectedDays: Dispatch>; +} + +export function Calendar({ selectedDays, setSelectedDays }: CalendarProps) { + const today = new Date(); + const [currentMonth, setCurrentMonth] = useState(today.getMonth()); + const [currentYear, setCurrentYear] = useState(today.getFullYear()); + const [calendarDays, setCalendarDays] = useState([]); + + const monthName = MONTHS[currentMonth]; + + const updateCalendar = useCallback(() => { + const days = ZotDate.generateZotDates( + currentMonth, + currentYear, + selectedDays + ); + + setCalendarDays(days); + }, [currentMonth, currentYear, selectedDays]); + + const decrementMonth = () => { + let newMonth = currentMonth - 1; + let newYear = currentYear; + + if (newMonth < 0) { + newMonth = 11; + newYear -= 1; + } + + setCurrentMonth(newMonth); + setCurrentYear(newYear); + }; + + const incrementMonth = () => { + let newMonth = currentMonth + 1; + let newYear = currentYear; + + if (newMonth > 11) { + newMonth = 0; + newYear += 1; + } + + setCurrentMonth(newMonth); + setCurrentYear(newYear); + }; + + /** + * Updates a range of dates based on a user selection + * @param startDate the day that the user first initiated the date multiselect range + * @param endDate the day that the user ended the date multiselect range + */ + const updateSelectedRange = ( + startDate: ZotDate, + endDate: ZotDate + ): void => { + const highlightedRange: Date[] = ZotDate.generateRange( + startDate.day, + endDate.day + ); + + setSelectedDays((alreadySelectedDays: ZotDate[]) => { + let modifiedSelectedDays = [...alreadySelectedDays]; + + highlightedRange.forEach((highlightedZotDate: Date) => { + const foundSelectedDay = alreadySelectedDays.find( + (d) => d.compareTo(new ZotDate(highlightedZotDate)) === 0 + ); + + if (startDate.isSelected && foundSelectedDay) { + // Remove any selected days if the multiselect initiated from an already selected day + modifiedSelectedDays = modifiedSelectedDays.filter( + (d) => d.compareTo(foundSelectedDay) !== 0 + ); + } else if (!startDate.isSelected && !foundSelectedDay) { + // Add day to selected days if the multiselect did not initiate from an already selected day + modifiedSelectedDays.push( + new ZotDate(highlightedZotDate, true) + ); + } + }); + + return modifiedSelectedDays; + }); + }; + + useEffect(() => { + updateCalendar(); + }, [updateCalendar]); + + return ( +
+ + +
+
+

+ {monthName} {currentYear} +

+
+
+ + + + + {WEEKDAYS.map((dayOfWeek) => ( + + ))} + + + + +
+
+

+ {dayOfWeek} +

+
+
+
+
+ + +
+ ); +} diff --git a/src/components/creation/creation.tsx b/src/components/creation/creation.tsx index 0ac9bb98..f593da83 100644 --- a/src/components/creation/creation.tsx +++ b/src/components/creation/creation.tsx @@ -2,12 +2,11 @@ import React, { useState } from "react"; import { useRouter } from "next/navigation"; +import { Calendar } from "@/components/creation/calendar/calendar"; import { MeetingNameField } from "@/components/creation/fields/meeting-name-field"; import { cn } from "@/lib/utils"; import { ZotDate } from "@/lib/zotdate"; -// import Calendar from "./creation"; - // import MeetingTimeField from "./MeetingTimeField"; export function Creation() { From d267d043fc2f18b7509e5efab6b1a0b207b0bae4 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Thu, 7 Nov 2024 00:57:14 -0800 Subject: [PATCH 03/17] refactor: move files and code around --- .../calendar/calendar-body-day-square.tsx | 39 +++++ .../creation/calendar/calendar-body-day.tsx | 137 ++++++++++++++---- .../creation/calendar/calendar-body.tsx | 107 ++------------ 3 files changed, 163 insertions(+), 120 deletions(-) create mode 100644 src/components/creation/calendar/calendar-body-day-square.tsx diff --git a/src/components/creation/calendar/calendar-body-day-square.tsx b/src/components/creation/calendar/calendar-body-day-square.tsx new file mode 100644 index 00000000..4be45f3a --- /dev/null +++ b/src/components/creation/calendar/calendar-body-day-square.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { cn } from "@/lib/utils"; +import { ZotDate } from "@/lib/zotdate"; + +interface CalendarBodyDaySquareProps { + isHighlighted: boolean; + calendarDay: ZotDate; + isCurrentMonth: boolean; +} + +export function CalendarBodyDaySquare({ + isHighlighted, + calendarDay, + isCurrentMonth, +}: CalendarBodyDaySquareProps) { + const isSelected = calendarDay.isSelected; + + return ( +

+ {calendarDay.getDay()} +

+ ); +} diff --git a/src/components/creation/calendar/calendar-body-day.tsx b/src/components/creation/calendar/calendar-body-day.tsx index a8852660..03c0d69e 100644 --- a/src/components/creation/calendar/calendar-body-day.tsx +++ b/src/components/creation/calendar/calendar-body-day.tsx @@ -1,39 +1,124 @@ -import React from "react"; -import { cn } from "@/lib/utils"; +import { Dispatch, SetStateAction } from "react"; +import { CalendarBodyDaySquare } from "@/components/creation/calendar/calendar-body-day-square"; import { ZotDate } from "@/lib/zotdate"; interface CalendarBodyDayProps { - isHighlighted: boolean; calendarDay: ZotDate; - isCurrentMonth: boolean; + startDaySelection: ZotDate | null; + setStartDaySelection: Dispatch>; + endDaySelection: ZotDate | null; + setEndDaySelection: Dispatch>; + updateCalendar: VoidFunction; + currentMonth: number; + updateSelectedRange: (startDate: ZotDate, endDate: ZotDate) => void; } export function CalendarBodyDay({ - isHighlighted, calendarDay, - isCurrentMonth, + startDaySelection, + setStartDaySelection, + endDaySelection, + setEndDaySelection, + updateCalendar, + currentMonth, + updateSelectedRange, }: CalendarBodyDayProps) { - const isSelected = calendarDay.isSelected; + const isHighlighted = + startDaySelection && + endDaySelection && + calendarDay.determineDayWithinBounds( + startDaySelection, + endDaySelection + ); + + const isCurrentMonth = currentMonth === calendarDay.getMonth(); + + /* Confirms the current highlight selection and updates calendar accordingly */ + const handleEndSelection = () => { + if (startDaySelection && endDaySelection) { + try { + updateSelectedRange(startDaySelection, endDaySelection); + } catch (err) { + console.error(err); + } + } + + updateCalendar(); + + setStartDaySelection(null); + setEndDaySelection(null); + }; + + /** + * Updates the current highlight selection whenever a mobile user drags on the calendar + * @param {TouchEvent} e - Touch event from a mobile user + */ + const handleTouchMove = (e: React.TouchEvent) => { + const touchingElement = document.elementFromPoint( + e.touches[0].clientX, + e.touches[0].clientY + ); + + if (!touchingElement) return; + + const touchingDay = touchingElement.getAttribute("data-day"); + + if (startDaySelection && touchingDay) { + const day = ZotDate.extractDayFromElement(touchingElement); + setEndDaySelection(day); + } + }; + + const handleTouchStart = (e: React.TouchEvent) => { + if (e.cancelable) { + e.preventDefault(); + } + setStartDaySelection(calendarDay); + }; + + const handleTouchEnd = (e: React.TouchEvent) => { + if (e.cancelable) { + e.preventDefault(); + } + if (!endDaySelection) { + setEndDaySelection(calendarDay); + } + handleEndSelection(); + }; + + const handleMouseMove = () => { + if (startDaySelection) { + setEndDaySelection(calendarDay); + } + }; + + const handleMouseUp = () => { + if (startDaySelection) { + setEndDaySelection(calendarDay); + handleEndSelection(); + } + }; + + const handleMouseDown = () => { + setStartDaySelection(calendarDay); + }; return ( -

- {calendarDay.getDay()} -

+ + + ); } diff --git a/src/components/creation/calendar/calendar-body.tsx b/src/components/creation/calendar/calendar-body.tsx index bcbd0032..49a48957 100644 --- a/src/components/creation/calendar/calendar-body.tsx +++ b/src/components/creation/calendar/calendar-body.tsx @@ -22,104 +22,23 @@ export function CalendarBody({ null ); - /** - * Updates the current highlight selection whenever a mobile user drags on the calendar - * @param {TouchEvent} e - Touch event from a mobile user - */ - const handleTouchMove = (e: React.TouchEvent) => { - const touchingElement = document.elementFromPoint( - e.touches[0].clientX, - e.touches[0].clientY - ); - - if (!touchingElement) return; - - const touchingDay = touchingElement.getAttribute("data-day"); - - if (startDaySelection && touchingDay) { - const day = ZotDate.extractDayFromElement(touchingElement); - setEndDaySelection(day); - } - }; - - /* Confirms the current highlight selection and updates calendar accordingly */ - const handleEndSelection = () => { - if (startDaySelection && endDaySelection) { - try { - updateSelectedRange(startDaySelection, endDaySelection); - } catch (err) { - console.error(err); - } - } - - updateCalendar(); - - setStartDaySelection(null); - setEndDaySelection(null); - }; - return ( {calendarDays.map((calendarWeek, weekIndex) => ( - {calendarWeek.map((calendarDay, dayIndex) => { - const isHighlighted = - startDaySelection && - endDaySelection && - calendarDay.determineDayWithinBounds( - startDaySelection, - endDaySelection - ); - - const isCurrentMonth = - currentMonth === calendarDay.getMonth(); - - return ( - { - if (startDaySelection) { - setEndDaySelection(calendarDay); - handleEndSelection(); - } - }} - > - - - ); - })} + {calendarWeek.map((calendarDay, dayIndex) => ( + + ))} ))} From 6bc836aa198f21d034ea37add597979ea417c440 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Thu, 7 Nov 2024 01:09:09 -0800 Subject: [PATCH 04/17] fix: selection --- .../creation/calendar/calendar-body-day-square.tsx | 6 +++--- src/components/creation/calendar/calendar-body-day.tsx | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/creation/calendar/calendar-body-day-square.tsx b/src/components/creation/calendar/calendar-body-day-square.tsx index 4be45f3a..f982d039 100644 --- a/src/components/creation/calendar/calendar-body-day-square.tsx +++ b/src/components/creation/calendar/calendar-body-day-square.tsx @@ -19,11 +19,11 @@ export function CalendarBodyDaySquare({

{ - if (startDaySelection && endDaySelection) { + if (startDaySelection) { try { - updateSelectedRange(startDaySelection, endDaySelection); + setEndDaySelection(calendarDay); + updateSelectedRange(startDaySelection, calendarDay); } catch (err) { console.error(err); } @@ -80,9 +81,7 @@ export function CalendarBodyDay({ if (e.cancelable) { e.preventDefault(); } - if (!endDaySelection) { - setEndDaySelection(calendarDay); - } + handleEndSelection(); }; @@ -94,7 +93,6 @@ export function CalendarBodyDay({ const handleMouseUp = () => { if (startDaySelection) { - setEndDaySelection(calendarDay); handleEndSelection(); } }; From 2ea68a2a930fff1b0bcd88c250ed975d3233760a Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Thu, 7 Nov 2024 01:35:34 -0800 Subject: [PATCH 05/17] feat: misc. --- .../calendar/calendar-body-day-square.tsx | 6 +++--- src/components/creation/calendar/calendar.tsx | 19 +++++++++++-------- src/components/creation/creation.tsx | 6 ++++-- tailwind.config.ts | 15 +++++++++++++++ 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/components/creation/calendar/calendar-body-day-square.tsx b/src/components/creation/calendar/calendar-body-day-square.tsx index f982d039..4be45f3a 100644 --- a/src/components/creation/calendar/calendar-body-day-square.tsx +++ b/src/components/creation/calendar/calendar-body-day-square.tsx @@ -19,11 +19,11 @@ export function CalendarBodyDaySquare({

- +

{monthName} {currentYear}

-
+ +
@@ -131,7 +134,7 @@ export function Calendar({ selectedDays, setSelectedDays }: CalendarProps) { {dayOfWeek}

-
+ ))} @@ -146,12 +149,12 @@ export function Calendar({ selectedDays, setSelectedDays }: CalendarProps) {
- +
); } diff --git a/src/components/creation/creation.tsx b/src/components/creation/creation.tsx index f593da83..f24814aa 100644 --- a/src/components/creation/creation.tsx +++ b/src/components/creation/creation.tsx @@ -51,12 +51,13 @@ export function Creation() { }; return ( -
+

Let's plan your next meeting.

-

+ +

Select potential dates and times for you and your team.

@@ -85,6 +86,7 @@ export function Creation() {

{selectedDays.length} days selected

+ +
); diff --git a/src/components/creation/fields/meeting-name-field.tsx b/src/components/creation/fields/meeting-name-field.tsx index ff660c2e..a74f544a 100644 --- a/src/components/creation/fields/meeting-name-field.tsx +++ b/src/components/creation/fields/meeting-name-field.tsx @@ -1,6 +1,6 @@ -"use client"; - import React, { Dispatch, SetStateAction, useEffect, useRef } from "react"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; const DEFAULT_MEETING_NAME = ""; @@ -29,9 +29,12 @@ export function MeetingNameField({ return (
- >; + setEndTime: Dispatch>; +} + +export const MeetingTimeField = ({ + setStartTime, + setEndTime, +}: MeetingTimeFieldProps) => { + const [startHour, setStartHour] = useState(9); + const [endHour, setEndHour] = useState(4); + const [startPeriod, setStartPeriod] = useState("AM"); + const [endPeriod, setEndPeriod] = useState("PM"); + + const convertTo24Hour = (hour: number, period: string) => { + if (period === "PM" && hour !== 12) { + hour += 12; + } else if (period === "AM" && hour === 12) { + hour = 0; + } + return hour.toString().padStart(2, "0"); + }; + + const handleStartHourChange = (value: string) => { + setStartHour(parseInt(value)); + }; + + const handleStartPeriodChange = (value: string) => { + setStartPeriod(value); + }; + + const handleEndHourChange = (value: string) => { + setEndHour(parseInt(value)); + }; + + const handleEndPeriodChange = (value: string) => { + setEndPeriod(value); + }; + + useEffect(() => { + const newStartTime = + `${convertTo24Hour(startHour, startPeriod)}:00` as HourMinuteString; + const newEndTime = + `${convertTo24Hour(endHour, endPeriod)}:00` as HourMinuteString; + + setStartTime(newStartTime); + setEndTime(newEndTime); + }, [startHour, startPeriod, endHour, endPeriod, setStartTime, setEndTime]); + + return ( +
+
+ +

+ ANY TIME BETWEEN (PST) +

+
+ +
+
+ + + +
+ + and + +
+ + + +
+
+
+ ); +}; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 00000000..f4e961b0 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,159 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; From 5957a08c3f029ec69b8b6b28886def6ff8c3c2f2 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Thu, 7 Nov 2024 02:17:28 -0800 Subject: [PATCH 07/17] feat: setup route --- src/app/api/create/route.ts | 85 ++++++++++++++++++++++++++++ src/app/availability/[slug]/page.tsx | 6 +- src/lib/types/meetings.ts | 44 ++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 src/app/api/create/route.ts create mode 100644 src/lib/types/meetings.ts diff --git a/src/app/api/create/route.ts b/src/app/api/create/route.ts new file mode 100644 index 00000000..fa02b882 --- /dev/null +++ b/src/app/api/create/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getUserIdFromSession, insertMeeting } from "@/lib/db/databaseUtils"; +import { CreateMeetingPostParams } from "@/lib/types/meetings"; + +export async function POST(request: NextRequest) { + try { + // Parse the incoming JSON body + const body: CreateMeetingPostParams = await request.json(); + + const { + title, + description, + fromTime, + toTime, + meetingDates, + sessionId, + } = body; + + console.log( + "Creating meeting:", + title, + description, + fromTime, + toTime, + meetingDates + ); + + // Validate the input data + if (fromTime >= toTime) { + return NextResponse.json( + { error: "From time must be before to time" }, + { status: 400 } + ); + } + + if (!meetingDates || meetingDates.length === 0) { + return NextResponse.json( + { error: "At least one date must be provided" }, + { status: 400 } + ); + } + + // Limit the number of dates to prevent abuse + if (meetingDates.length > 100) { + return NextResponse.json( + { error: "Too many dates provided" }, + { status: 400 } + ); + } + + // Sort the dates in UTC order + const sortedDates = meetingDates + .map((dateString) => new Date(dateString)) + .sort((a, b) => a.getTime() - b.getTime()); + + // Retrieve the host ID if the session ID is provided + const host_id = sessionId + ? await getUserIdFromSession(sessionId) + : undefined; + + // Prepare the meeting data + const meeting = { + title, + description: description || "", + from_time: fromTime, + to_time: toTime, + host_id, + }; + + // Insert the meeting into the database + const meetingId = await insertMeeting(meeting, sortedDates); + + // Return the created meeting ID + return NextResponse.json({ meetingId }); + } catch (err) { + const error = err as Error; + console.error("Error creating meeting:", error.message); + + // Return a 500 error response + return NextResponse.json( + { error: `Error creating meeting: ${error.message}` }, + { status: 500 } + ); + } +} diff --git a/src/app/availability/[slug]/page.tsx b/src/app/availability/[slug]/page.tsx index 85e27e83..b92ec64f 100644 --- a/src/app/availability/[slug]/page.tsx +++ b/src/app/availability/[slug]/page.tsx @@ -26,9 +26,9 @@ export default async function Page({ params }: PageProps) { } const meetingData = await getExistingMeeting(slug); - // if (!meetingData) { - // redirect("/error"); - // } + if (!meetingData) { + redirect("/error"); + } const meetingDates = await getMeetingDates(slug); const availability = user ? await getAvailability(user, slug) : null; diff --git a/src/lib/types/meetings.ts b/src/lib/types/meetings.ts new file mode 100644 index 00000000..67d314a9 --- /dev/null +++ b/src/lib/types/meetings.ts @@ -0,0 +1,44 @@ +import type { HourMinuteString } from "./chrono"; + +export type Group = { name: string; id: number; img: string; link: string }; + +export type Meeting = ScheduledMeeting | UnscheduledMeeting; + +export type Attendance = "accepted" | "maybe" | "declined" | undefined; + +export type ScheduledMeeting = { + name: string; + id: number; + link: string; + date: string; + startTime: string; + endTime: string; + attendance: Attendance; + location: string; +}; + +export type UnscheduledMeeting = { + name: string; + id: number; + link: string; + startDate: string; + endDate: string; + startTime: string; + endTime: string; + location: string; + // hasIndicated: boolean; +}; + +export type MeetingTime = { + startTime: HourMinuteString; + endTime: HourMinuteString; +}; + +export interface CreateMeetingPostParams { + title: string; + description: string; + fromTime: HourMinuteString; + toTime: HourMinuteString; + meetingDates: string[]; // ISO date strings + sessionId?: string; +} From a9203fd169a6812f1a0f89f7d428164d78376523 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Thu, 7 Nov 2024 02:19:49 -0800 Subject: [PATCH 08/17] style: misc. --- src/components/creation/fields/meeting-time-field.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/creation/fields/meeting-time-field.tsx b/src/components/creation/fields/meeting-time-field.tsx index e08a75b2..03ea1995 100644 --- a/src/components/creation/fields/meeting-time-field.tsx +++ b/src/components/creation/fields/meeting-time-field.tsx @@ -67,7 +67,7 @@ export const MeetingTimeField = ({

-
+