diff --git a/package.json b/package.json index 191cd94..6b5193c 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@armco/react-vite-rtk-template", + "name": "@armco/calendar", "private": true, "version": "0.0.0", "type": "module", @@ -65,7 +65,7 @@ "main": "index.tsx", "repository": { "type": "git", - "url": "git+https://github.com/ReStruct-Corporate-Advantage/.git" + "url": "git+https://github.com/ReStruct-Corporate-Advantage/calendar.git" }, "keywords": [ "components", @@ -75,7 +75,7 @@ ], "license": "ISC", "bugs": { - "url": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template/issues" + "url": "https://github.com/ReStruct-Corporate-Advantage/calendar/issues" }, - "homepage": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template#readme" + "homepage": "https://github.com/ReStruct-Corporate-Advantage/calendar#readme" } diff --git a/src/Calendar.component.scss b/src/Calendar.component.scss new file mode 100755 index 0000000..1f2414b --- /dev/null +++ b/src/Calendar.component.scss @@ -0,0 +1,54 @@ +@use "./MonthSelector.component.scss"; +@use "./JustCalendar.component.scss"; +@use "./MonthNavigator.component.scss"; +@use "./EventManager.component.scss"; + + +.ar-Calendar { + --ar-footer-height: 3rem; + &.is-mini { + --ar-footer-height: 2rem; + width: calc(2.5rem * 7 + 1rem) !important; + height: calc(3rem + (2.5rem * 6) + 3rem + 1px) !important; + } + + &__cal-event-decade-month-year-selector { + + & > * { + transition: left 0.3s; + } + + .ar-MonthSelector, .ar-EventForm { + left: 100%; + } + .ar-JustCalendar { + left: 0; + } + &.to-month-year, &.to-event-form { + .ar-JustCalendar { + left: -100% + } + } + &.to-month-year { + .ar-MonthSelector { + left: 0; + } + } + &.to-event-form { + .ar-EventForm { + left: 0; + } + } + } + .ar-Calendar__cal-event-decade-month-year-selector { + height: calc(100% - var(--ar-footer-height)); + } + .ar-Calendar__footer { + transition: height 0.3s; + height: var(--ar-footer-height); + + .ar-Calendar__status { + color: var(--ar-color-obscure); + } + } +} diff --git a/src/app/pages/Home/Home.test.ts b/src/Calendar.test.ts similarity index 54% rename from src/app/pages/Home/Home.test.ts rename to src/Calendar.test.ts index b4c8c84..2efddbf 100755 --- a/src/app/pages/Home/Home.test.ts +++ b/src/Calendar.test.ts @@ -1,7 +1,7 @@ import React from "react" -import Home from "./Home" +import Calendar from "./Calendar" -describe("Home", () => { +describe("Calendar", () => { it("renders without error", () => { }) diff --git a/src/Calendar.tsx b/src/Calendar.tsx new file mode 100755 index 0000000..0ad9b2c --- /dev/null +++ b/src/Calendar.tsx @@ -0,0 +1,298 @@ +import { useEffect, useState } from "react" +import MonthSelector from "./MonthSelector" +import JustCalendar from "./JustCalendar" +import EventManager from "./EventManager" +import MonthNavigator from "./MonthNavigator" +import { + ArButtonVariants, + ArCalViews, + ArMonthSelectorViews, + ArSizes, + CalendarDate, + CalendarProps, + Event, +} from "@armco/types" +import { Button, CalHelper, isWithinSchedule } from "../.." +import "./Calendar.component.scss" + +const calHelper = new CalHelper({ siblingMonths: true, weekStart: 1 }) + +const today = new Date() + +const Calendar = (props: CalendarProps): JSX.Element => { + const { + allowEventSetting, + classes, + events, + customDayEventSetter, + hasTimeControls, + isSingleSelect, + maxDate, + minDate, + miniMode, + onDateSelected, + startDate, + endDate, + theme, + } = props + const [calendar, setCalendar] = useState>() + const [refDate, setRefDate] = useState(today) + const [startDateLocal, setStartDate] = useState() + const [endDateLocal, setEndDate] = useState() + const [eventDate, setEventDate] = useState() + const [hovered, setHovered] = useState() + const [currentView, setCurrentView] = useState( + ArCalViews.CALENDAR, + ) + const [localEvents, setLocalEvents] = useState>() + const [currentMonthNavView, setCurrentMonthNavView] = + useState() + + const currentDate = (startDateLocal || { + day: today.getDate(), + month: today.getMonth(), + year: today.getFullYear(), + }) as CalendarDate + + const [currentDecade, setCurrentDecade] = useState( + currentDate?.year && Math.floor(currentDate.year / 10) * 10, + ) + const [currentYear, setCurrentYear] = useState( + currentDate?.year, + ) + + useEffect(() => { + setLocalEvents(events) + }, [events]) + + useEffect(() => { + if (startDate) { + setStartDate({ + year: (startDate as Date).getFullYear(), + month: (startDate as Date).getMonth(), + day: (startDate as Date).getDate(), + }) + } + }, [startDate]) + + useEffect(() => { + if (endDate) { + setEndDate({ + year: (endDate as Date).getFullYear(), + month: (endDate as Date).getMonth(), + day: (endDate as Date).getDate(), + }) + } + }, [endDate]) + + useEffect(() => { + if (refDate) { + const year = (refDate as Date).getFullYear() + const month = (refDate as Date).getMonth() + const calendar = calHelper.getCalendar(year, month) + setCalendar(calendar) + } + }, [refDate]) + + const onDateSelectedLocal = (date: CalendarDate, isSpecialOp?: boolean) => { + if (!startDateLocal || isSingleSelect) { + setStartDate(date) + calHelper.setStartDate(date) + onDateSelected && + onDateSelected(isSingleSelect ? date : { startDate: date }) + if (isSpecialOp && allowEventSetting) { + setEventDate(date) + setCurrentView(ArCalViews.EVENT_FORM) + } + return + } + if (startDateLocal && endDateLocal) { + if ( + CalHelper.compare(startDateLocal, date) === -1 && + CalHelper.compare(date, endDateLocal) === -1 + ) { + calHelper.setEndDate(date) + setEndDate(date) + onDateSelected && + onDateSelected( + isSingleSelect + ? date + : { startDate: startDateLocal, endDate: date }, + ) + } else { + calHelper.setEndDate(null) + setEndDate(null) + calHelper.setStartDate(date) + setStartDate(date) + onDateSelected && + onDateSelected( + isSingleSelect ? date : { startDate: date, endDate: null }, + ) + } + } else { + if (CalHelper.compare(date, startDateLocal) === -1) { + setEndDate(startDateLocal) + calHelper.setEndDate(startDateLocal) + setStartDate(date) + calHelper.setStartDate(date) + onDateSelected && + onDateSelected( + isSingleSelect + ? date + : { startDate: date, endDate: startDateLocal }, + ) + } else { + setEndDate(date) + calHelper.setEndDate(date) + onDateSelected && + onDateSelected( + isSingleSelect + ? date + : { startDate: startDateLocal, endDate: date }, + ) + } + setHovered(null) + } + if (isSpecialOp && allowEventSetting) { + setEventDate(date) + setCurrentView(ArCalViews.EVENT_FORM) + } + } + + return ( +
+ {refDate && currentView !== ArCalViews.EVENT_FORM && ( + + )} +
+ + { + setCurrentView(ArCalViews.CALENDAR) + setRefDate(date) + }} + setCurrentDecade={setCurrentDecade} + setCurrentMonthNavView={setCurrentMonthNavView} + setCurrentView={setCurrentView} + setCurrentYear={setCurrentYear} + /> + {eventDate && currentView === ArCalViews.EVENT_FORM && ( + + isWithinSchedule(eventDate, event), + )} + miniMode={miniMode} + onEventSubmit={(event: Event) => { + const localEventsClone = localEvents ? [...localEvents] : [] + localEventsClone.push(event) + setLocalEvents(localEventsClone) + setEventDate(null) + setCurrentView(ArCalViews.CALENDAR) + }} + selectedDate={eventDate as CalendarDate} + setCurrentView={setCurrentView} + /> + )} +
+
+ {startDateLocal && !miniMode && ( +
+ + {CalHelper.toString(startDateLocal)} + {endDateLocal && " - " + CalHelper.toString(endDateLocal)} + +
+ )} +
+
+
+
+ ) +} + +export default Calendar diff --git a/src/DatePicker.component.scss b/src/DatePicker.component.scss new file mode 100755 index 0000000..0f53ce0 --- /dev/null +++ b/src/DatePicker.component.scss @@ -0,0 +1,17 @@ +.ar-DatePicker { + border-top-left-radius: 0.125rem; + border-bottom-left-radius: 0.125rem; + .ar-DatePicker__picker { + outline: none; + } + + .ar-DatePicker__clear-button { + visibility: hidden; + padding: 0 0.75rem; + + &.has-value { + visibility: visible; + } + } + +} diff --git a/src/DatePicker.tsx b/src/DatePicker.tsx new file mode 100755 index 0000000..165db85 --- /dev/null +++ b/src/DatePicker.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react" +import { + ArPopoverPositions, + ArPopoverSlots, + CalendarDate, + DatePickerProps, + FunctionType, +} from "@armco/types" +import { Helper } from "@armco/utils" +import { Icon } from "@armco/icon" +import { Popover } from "@armco/shared-components" +import Calendar from "." +import "./DatePicker.component.scss" + +const DatePicker = (props: DatePickerProps): JSX.Element => { + const { allowEventSetting, classes, id, label, onChange } = props + const [date, setDate] = useState() + const [pickerDisplayed, showPicker] = useState() + + useEffect(() => { + !allowEventSetting && date && showPicker(false) + }, [allowEventSetting, date]) + + const localId = id || "ar-DatePicker__input" + + const onLocalChange = (date: CalendarDate) => { + setDate(date) + onChange && (onChange as FunctionType)(date) + } + + return ( +
+ +
!allowEventSetting && showPicker(true)} + > + {label && ( + + )} + + +
+ +
+
+ ) +} + +export default DatePicker diff --git a/src/DateRangePicker.component.scss b/src/DateRangePicker.component.scss new file mode 100755 index 0000000..5a7502a --- /dev/null +++ b/src/DateRangePicker.component.scss @@ -0,0 +1,5 @@ +.ar-DateRangePicker { + .ar-DateRangePicker__input { + min-width: 15rem; + } +} diff --git a/src/DateRangePicker.tsx b/src/DateRangePicker.tsx new file mode 100755 index 0000000..af2c6f3 --- /dev/null +++ b/src/DateRangePicker.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState } from "react" +import { + ArPopoverPositions, + ArPopoverSlots, + CalendarDate, + DateRangePickerProps, +} from "@armco/types" +import { Icon } from "@armco/icon" +import { DateFormatter, Helper, toDate } from "@armco/utils" +import { Popover } from "@armco/shared-components" +import Calendar from "." +import "./DateRangePicker.component.scss" + +const DateRangePicker = (props: DateRangePickerProps): JSX.Element => { + const { closeOnEndSelect, id, label, mask } = props + const [startDate, setStartDate] = useState() + const [endDate, setEndDate] = useState() + const [pickerDisplayed, showPicker] = useState() + let value + const localId = id || "ar-DateRangePicker__input" + + useEffect(() => { + if (endDate !== null && closeOnEndSelect) { + showPicker(false) + } + }, [closeOnEndSelect, endDate]) + + if (startDate && endDate) { + if (mask) { + value = + DateFormatter(toDate(startDate), mask) + + " - " + + DateFormatter(toDate(endDate), mask) + } else { + value = + Helper.pad(startDate.day) + + "/" + + Helper.pad(startDate.month + 1) + + "/" + + startDate.year + value += " - " + value += + Helper.pad(endDate.day) + + "/" + + Helper.pad(endDate.month + 1) + + "/" + + endDate.year + } + } + + const setDate = (dates: { + startDate: CalendarDate + endDate?: CalendarDate + }) => { + setStartDate(dates.startDate) + if (dates.endDate) { + setEndDate(dates.endDate) + } + } + + return ( +
+ +
showPicker(true)} + > + {label && ( + + )} + + +
+ +
+
+ ) +} + +export default DateRangePicker diff --git a/src/EventForm.tsx b/src/EventForm.tsx new file mode 100644 index 0000000..daffde1 --- /dev/null +++ b/src/EventForm.tsx @@ -0,0 +1,285 @@ +import { useState } from "react" +import { + ArButtonVariants, + ArEventStates, + ArEventTypes, + ArSchedules, + ArSizes, + Event, + EventFormProps, + FunctionType, + ObjectType, +} from "@armco/types" +import { + Button, + SegmentedControl, + Select, + TextArea, + TextInput, + TimeEntry, + Toggle, + toDate, +} from "../.." +import { EventListItem } from "./EventList" + +const durationOptions = [ + { + name: "15min", + label: "15 minutes", + value: "15min", + data: { minutes: 15 }, + }, + { + name: "30min", + label: "30 minutes", + value: "30min", + data: { minutes: 30 }, + }, + { + name: "45min", + label: "45 minutes", + value: "45min", + data: { minutes: 45 }, + }, + { + name: "1hour", + label: "1 hour", + value: "1hour", + data: { hours: 1 }, + }, + { + name: "2hours", + label: "2 hours", + value: "2hours", + data: { hours: 2 }, + }, + { name: "custom", label: "Custom", value: "custom" }, +] + +export const segments = [ + { name: "task", label: "Task", icon: "md.MdAddTask" }, + { + name: "event", + label: "Event", + icon: "md.MdOutlineEventAvailable", + }, + { + name: "birthday", + label: "Birthday", + icon: "fa.FaBirthdayCake", + }, + { + name: "meeting", + label: "Meeting", + icon: "md.MdOutlineMeetingRoom", + }, +] + +const validateEvent = (event: Event) => { + return ( + event && + event.eventType && + event.title && + event.schedule && + event.schedule.starts && + event.schedule.ends + ) +} + +const EventForm = (props: EventFormProps) => { + const { miniMode, onSubmit, selectedDate } = props + const [localEvent, setLocalEvent] = useState({ + title: "", + state: ArEventStates.DRAFT, + schedule: {}, + }) + + const onChange = ( + key: string, + value: string | boolean | ArSchedules | ObjectType, + ) => { + const eventClone = { ...localEvent } + if (key === "isWholeDay" || key === "starts" || key === "ends") { + if (!eventClone.schedule) { + eventClone.schedule = {} + } + } else if (key !== "duration") { + ;(eventClone[key as keyof Event] as string | boolean | ArSchedules) = + value as string + } + if (key === "isWholeDay" && eventClone.schedule) { + eventClone.schedule.isWholeDay = value as boolean + if (value) { + eventClone.schedule.starts = toDate(selectedDate) + eventClone.schedule.ends = toDate({ + ...selectedDate, + day: selectedDate.day + 1, + }) + } + } + if ((key === "starts" || key === "ends") && eventClone.schedule) { + const timeDetails = (value as ObjectType).raw as ObjectType + let date + try { + date = + timeDetails && + timeDetails.hour && + toDate( + selectedDate, + +(timeDetails.hour || 0), + +(timeDetails.minute || 0), + timeDetails.ampm as string, + ) + } catch (e) { + console.warn("Failed attempt to convert string to number") + } + date && (eventClone.schedule[key] = date) + } + if (key === "eventType" && value === "birthday") { + if (!eventClone.schedule) { + eventClone.schedule = {} + } + eventClone.schedule.isWholeDay = true + eventClone.schedule.starts = toDate(selectedDate) + eventClone.schedule.ends = toDate({ + ...selectedDate, + day: selectedDate.day + 1, + }) + } + if (key === "duration" && eventClone.schedule) { + const durationData = ((value as ObjectType).data as ObjectType)?.data + const currentHours = (eventClone.schedule.starts as Date)?.getHours() + const currentMinutes = (eventClone.schedule.starts as Date)?.getMinutes() + const currentAmPm = (eventClone.schedule.starts as Date) + ?.toLocaleString("en-US", { hour12: true }) + .split(" ")[1] + const addHours = (durationData as ObjectType)?.hours + const addMinutes = (durationData as ObjectType)?.minutes + let endHours = currentHours, + endMinutes = currentMinutes, + ampm = currentAmPm, + endsNextDay = false + if ( + addMinutes && + currentMinutes && + currentMinutes + (addMinutes as number) > 59 + ) { + endMinutes = currentMinutes + (addMinutes as number) - 60 + endHours += 1 + } + if (addHours && endHours + (addHours as number) >= 12) { + endHours = endHours + (addHours as number) - 12 + if (ampm === "PM") { + endsNextDay = true + } + ampm = ampm === "AM" ? "PM" : "AM" + } + eventClone.schedule.ends = toDate( + endsNextDay + ? { ...selectedDate, day: selectedDate.day + 1 } + : selectedDate, + endHours, + endMinutes, + ) + } + setLocalEvent(eventClone) + } + + return ( +
+ { + onChange("eventType", segment.name) + }} + /> + + onChange("title", (e.target as HTMLInputElement).value) + } + required + /> +