diff --git a/components/CalendarExportModal/CalendarExportModal.tsx b/components/CalendarExportModal/CalendarExportModal.tsx index 19a61e61..f0bd4b2b 100644 --- a/components/CalendarExportModal/CalendarExportModal.tsx +++ b/components/CalendarExportModal/CalendarExportModal.tsx @@ -5,22 +5,21 @@ import { useState, useEffect, useCallback } from "react"; import { Collapse } from "antd"; import { IFilterDTO } from "../../dtos"; +import { useAppInfo } from "../../contexts/AppInfoProvider"; type CalendarExportModalProps = { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; - isHome: boolean; - filters: IFilterDTO[]; }; const CalendarExportModal = ({ isOpen, setIsOpen, - isHome, - filters, }: CalendarExportModalProps) => { const [isCopied, setIsCopied] = useState(false); const [URL, setURL] = useState(""); + const info = useAppInfo(); + const filters = info.filters as IFilterDTO[]; const generateURL = useCallback((): string => { // checks if a filter exists for each filterId in checkedEvents @@ -49,10 +48,10 @@ const CalendarExportModal = ({ const domain = process.env.NEXT_PUBLIC_DOMAIN; const baseURL: string = - domain + "/api/export/" + (isHome ? "events" : "schedule") + "?"; + domain + "/api/export/" + (info.isEvents ? "events" : "schedule") + "?"; var query: string = ""; - if (isHome) { + if (info.isEvents) { // fecth checked events from localStorage const checkedEvents: number[] = JSON.parse( localStorage.getItem("checked") @@ -115,7 +114,7 @@ const CalendarExportModal = ({ // still, in an edge case of an empty 'query' string, return empty string (empty URL => warning message on modal) if (query !== "") return baseURL + query; else return ""; - }, [isHome, filters]); + }, [info.isEvents, filters]); useEffect(() => { setURL(generateURL()); @@ -154,7 +153,7 @@ const CalendarExportModal = ({ {URL === "" ? ( @@ -196,33 +195,34 @@ const CalendarExportModal = ({ subscribe to your active{" "} - {isHome ? "events" : "schedule"} + {info.isEvents ? "events" : "schedule"} {" "} in Calendarium, using your favorite calendar app.

This means that you will be able to see your{" "} - {isHome ? "events" : "schedule"} in your calendar app, - and add event notifications, change colors and make - other customizations. + {info.isEvents ? "events" : "schedule"} in your + calendar app, and add event notifications, change + colors and make other customizations.

- Your {isHome ? "events" : "schedule"} will be + Your {info.isEvents ? "events" : "schedule"} will be automatically synced with Calendarium.

{" "} To export your{" "} - {isHome ? "schedule" : "events"} + {info.isEvents ? "schedule" : "events"} {" "} please navigate to{" "} - {isHome ? "/schedule" : "the main page"}. + {info.isEvents ? "/schedule" : "the main page"}.

{" "} If you make any changes to your{" "} - {isHome ? "events" : "schedule"} in Calendarium, you + {info.isEvents ? "events" : "schedule"} in + Calendarium, you {"'"}ll need to{" "} re-export and re-subscribe to the calendar @@ -342,7 +342,7 @@ const CalendarExportModal = ({
- {isHome ? ( + {info.isEvents ? (
Currently exporting your visible{" "} diff --git a/components/ClearSelectionButton/ClearSelectionButton.tsx b/components/ClearSelectionButton/ClearSelectionButton.tsx index 5d5ca7e3..9c73d89a 100644 --- a/components/ClearSelectionButton/ClearSelectionButton.tsx +++ b/components/ClearSelectionButton/ClearSelectionButton.tsx @@ -1,11 +1,10 @@ +import { useAppInfo } from "../../contexts/AppInfoProvider"; import ConfirmPopUpButton from "../ConfirmPopUpButton"; const ClearSelectionButton = ({ - isHome, isSettings, clearSelection, }: { - isHome: boolean; isSettings: boolean; clearSelection: () => void; }) => { @@ -14,11 +13,14 @@ const ClearSelectionButton = ({ "cursor-not-allowed hover:text-error/50 dark:hover:text-red-400/60" }`; + const info = useAppInfo(); + return ( clearSelection()} onCancel={() => {}} @@ -31,7 +33,7 @@ const ClearSelectionButton = ({ title: "Clear", className: classNameData, "data-umami-event": "clear-selection-button", - "data-umami-event-type": isHome ? "events" : "shifts", + "data-umami-event-type": info.isEvents ? "events" : "shifts", }} > diff --git a/components/CustomToolbar/CustomToolbar.tsx b/components/CustomToolbar/CustomToolbar.tsx index 0c344ba6..4a298719 100644 --- a/components/CustomToolbar/CustomToolbar.tsx +++ b/components/CustomToolbar/CustomToolbar.tsx @@ -2,7 +2,7 @@ import { Navigate, ToolbarProps } from "react-big-calendar"; import { Fragment } from "react"; import { Menu, Transition } from "@headlessui/react"; -import { useWindowSize } from "../../utils"; +import useWindowSize from "../../hooks/useWindowSize"; const MobileToolbar = ({ view, onView }) => { return ( diff --git a/components/EventFilters/EventFilters.tsx b/components/EventFilters/EventFilters.tsx index b0f84fda..427fef16 100644 --- a/components/EventFilters/EventFilters.tsx +++ b/components/EventFilters/EventFilters.tsx @@ -3,38 +3,32 @@ import "antd/dist/reset.css"; import FilterBlock from "../FilterBlock"; -import { CheckBoxProps, SelectedShift } from "../../types"; - -import { IFilterDTO } from "../../dtos"; +import { CheckBoxProps } from "../../types"; +import { IFilterDTO, ISelectedFilterDTO } from "../../dtos"; +import { useAppInfo } from "../../contexts/AppInfoProvider"; type EventFiltersProps = { - filters: any; - handleFilters: (selectedFilter: number[]) => void; clearEvents: boolean; - checked: number[] | SelectedShift[]; - setChecked: (obj: number[] | SelectedShift[]) => void; + checked: number[] | ISelectedFilterDTO[]; + setChecked: (obj: number[] | ISelectedFilterDTO[]) => void; }; const EventFilters = ({ - filters, - handleFilters, clearEvents, checked, setChecked, }: EventFiltersProps) => { + const info = useAppInfo(); + const filters = info.filters as IFilterDTO[]; + const handleFilters = info.handleFilters; + useEffect(() => { const stored: number[] = JSON.parse(localStorage.getItem("checked")) ?? []; setChecked(stored); handleFilters(stored); }, [setChecked, handleFilters]); - let event: { - map: any; - id: number; - name: string; - groupId: number; - semester: number; - }[][] = []; + let event: IFilterDTO[][] = []; const mei = ["4ᵗʰ year", "5ᵗʰ year"]; @@ -74,9 +68,9 @@ const EventFilters = ({ const clearSelection = useCallback(() => { setChecked([]); - handleFilters([]); + info.handleFilters([]); localStorage.setItem("checked", JSON.stringify([])); - }, [handleFilters, setChecked]); + }, [info, setChecked]); useEffect(() => { clearEvents && clearSelection(); @@ -91,7 +85,6 @@ const EventFilters = ({ checkBoxes={getCheckBoxes().slice(0, 6)} checked={checked} setChecked={setChecked as (v: number[]) => void} - handleFilters={handleFilters} isShifts={false} /> {/* MEI */} @@ -102,7 +95,6 @@ const EventFilters = ({ checked={checked} setChecked={setChecked as (v: number[]) => void} exception={1} - handleFilters={handleFilters} isShifts={false} /> diff --git a/components/ExportButton/ExportButton.tsx b/components/ExportButton/ExportButton.tsx index cdc23e89..937271b3 100644 --- a/components/ExportButton/ExportButton.tsx +++ b/components/ExportButton/ExportButton.tsx @@ -1,17 +1,10 @@ -import { Fragment, useState } from "react"; -import { Menu, Transition } from "@headlessui/react"; - +import { useState } from "react"; import CalendarExportModal from "../CalendarExportModal"; +import { useAppInfo } from "../../contexts/AppInfoProvider"; -import { IFilterDTO } from "../../dtos"; - -type ExportButtonProps = { - isHome: boolean; - filters: IFilterDTO[]; -}; - -const ExportButton = ({ isHome, filters }: ExportButtonProps) => { - const [isModalOpen, setIsModalOpen] = useState(false); +const ExportButton = () => { + const [isModalOpen, setIsModalOpen] = useState(false); + const info = useAppInfo(); return (
@@ -20,17 +13,12 @@ const ExportButton = ({ isHome, filters }: ExportButtonProps) => { title="Export" className="h-10 w-full rounded-xl p-2 font-medium leading-3 text-neutral-300 shadow-md ring-1 ring-neutral-200/50 transition-all duration-300 hover:text-neutral-900 hover:shadow-lg dark:bg-neutral-800/70 dark:text-neutral-500 dark:ring-neutral-400/20 dark:hover:text-neutral-200" data-umami-event="export-button" - data-umami-event-type={isHome ? "events" : "shifts"} + data-umami-event-type={info.isEvents ? "events" : "shifts"} > Export - +
); }; diff --git a/components/FilterBlock/FilterBlock.tsx b/components/FilterBlock/FilterBlock.tsx index 9b546212..373f6c64 100644 --- a/components/FilterBlock/FilterBlock.tsx +++ b/components/FilterBlock/FilterBlock.tsx @@ -3,16 +3,17 @@ import "antd/dist/reset.css"; import { Fragment } from "react"; -import { CheckBoxProps, SelectedShift } from "../../types"; +import { CheckBoxProps } from "../../types"; +import { ISelectedFilterDTO } from "../../dtos"; +import { useAppInfo } from "../../contexts/AppInfoProvider"; type FilterBlockProps = { layer1: string[]; // contains the titles for the collapses of the 1st layer layer2?: string[]; // contains the titles for the collapses of the 2nd layer checkBoxes: CheckBoxProps[][]; // contains the checkboxes information in a universal format exception?: number; // indicates the index of an element from layer1 where the layer2 should be ignored, for example "5th year" - checked: number[] | SelectedShift[]; // assumes different types when called from ScheduleFilters.tsx and EventFilters.tsx - setChecked: (obj: number[] | SelectedShift[]) => void; // assumes different types when called from ScheduleFilters.tsx and EventFilters.tsx - handleFilters: any; // assumes different types when called from ScheduleFilters.tsx and EventFilters.tsx + checked: number[] | ISelectedFilterDTO[]; // assumes different types when called from ScheduleFilters.tsx and EventFilters.tsx + setChecked: (obj: number[] | ISelectedFilterDTO[]) => void; // assumes different types when called from ScheduleFilters.tsx and EventFilters.tsx isShifts: boolean; // used to know if FilterBlock is being called from ScheduleFilters.tsx or EventFilters.tsx }; @@ -23,9 +24,10 @@ const FilterBlock = ({ exception, checked, setChecked, - handleFilters, isShifts, }: FilterBlockProps) => { + const info = useAppInfo(); + // "Select All" checkbox const SelectAll = ({ index1, @@ -103,7 +105,7 @@ const FilterBlock = ({ else newCheck.splice(currentIdIndex, 1); setChecked(newCheck); - handleFilters(newCheck); + info.handleFilters(newCheck); localStorage.setItem("checked", JSON.stringify(newCheck)); } @@ -120,7 +122,7 @@ const FilterBlock = ({ } setChecked(newChecked); - handleFilters(newChecked); + info.handleFilters(newChecked); localStorage.setItem("checked", JSON.stringify(newChecked)); } @@ -226,31 +228,33 @@ const FilterBlock = ({ // Handles the toggle of a checkbox containing a shift (only used for Schedule) function handleShiftToggle(id: number, shift: string) { - const currentIdIndex = (checked as SelectedShift[]).findIndex( - (selectedShift: SelectedShift) => + const currentIdIndex = (checked as ISelectedFilterDTO[]).findIndex( + (selectedShift: ISelectedFilterDTO) => selectedShift.id === id && selectedShift.shift === shift ); - const newChecked: SelectedShift[] = [...checked] as SelectedShift[]; + const newChecked: ISelectedFilterDTO[] = [ + ...checked, + ] as ISelectedFilterDTO[]; - const shiftObj: SelectedShift = { id: id, shift: shift }; + const shiftObj: ISelectedFilterDTO = { id: id, shift: shift }; if (currentIdIndex === -1) newChecked.push(shiftObj); else newChecked.splice(currentIdIndex, 1); setChecked(newChecked); - handleFilters(newChecked); + info.handleFilters(newChecked); localStorage.setItem("shifts", JSON.stringify(newChecked)); } // Checks if a specific shift is selected (checked) (only used for Schedule) const isShiftChecked = (id: number, shift: string): boolean => { - return (checked as SelectedShift[]).some((shiftObj) => { + return (checked as ISelectedFilterDTO[]).some((shiftObj) => { return id === shiftObj.id && shift === shiftObj.shift; }); }; // Checks if some shift under a certain subject is selected (checked) (only used for Schedule) const isSomeSubjectShiftChecked = (id: number): boolean => { - return (checked as SelectedShift[]).some((s) => s.id === id); + return (checked as ISelectedFilterDTO[]).some((s) => s.id === id); }; // Checks if a shift that falls under a Collapse from the 2nd layer is selected (checked) (only used for Schedule) diff --git a/components/Layout/Layout.tsx b/components/Layout/Layout.tsx index 431bff67..98c229d5 100644 --- a/components/Layout/Layout.tsx +++ b/components/Layout/Layout.tsx @@ -1,34 +1,31 @@ import { useState, ReactNode, useEffect } from "react"; - import Link from "next/link"; - import Sidebar from "../Sidebar"; import Notifications from "../Notifications"; - import styles from "./layout.module.scss"; - import { useTheme } from "next-themes"; import Head from "next/head"; import Image from "next/image"; +import { AppInfoProvider } from "../../contexts/AppInfoProvider"; interface ILayoutProps { children: ReactNode; - isHome: boolean; + isEvents: boolean; filters: any; handleFilters: any; - saveTheme: () => void; + fetchTheme: () => void; } const Layout = ({ children, - isHome, + isEvents, filters, handleFilters, - saveTheme, + fetchTheme, }: ILayoutProps) => { const [isOpen, setIsOpen] = useState(false); const hamburgerLine = `h-1 w-6 my-0.5 rounded-full bg-black transition ease transform duration-300 dark:bg-neutral-200 bg-neutral-900`; - const [image, setImage] = useState(""); + const [image, setImage] = useState("/calendarium-light.svg"); const { resolvedTheme } = useTheme(); useEffect(() => { @@ -40,96 +37,98 @@ const Layout = ({ }, [resolvedTheme]); return ( -
- - {/* Status Bar configuration for Android devices */} - - {/* Status Bar configuration for IOS devices */} - - - {/* Open/Close Sidebar Button */} - + +
+ + {/* Status Bar configuration for Android devices */} + + {/* Status Bar configuration for IOS devices */} + + + {/* Open/Close Sidebar Button */} + - {/* Notification Badges */} - + {/* Notification Badges */} + - {/* Calendarium Logo */} -
-
- - Calendarium Logo - + {/* Calendarium Logo */} +
+
+ + Calendarium Logo + +
-
- {/* Sidebar */} -
- -
+ {/* Sidebar */} +
+ +
- {/* Feedback Button */} -
- -
+ +
- {/* Children */} -
- {children} -
-
+ {/* Children */} +
+ {children} +
+
+ ); }; diff --git a/components/ScheduleFilters/ScheduleFilters.tsx b/components/ScheduleFilters/ScheduleFilters.tsx index 9c1b3381..05ea9cff 100644 --- a/components/ScheduleFilters/ScheduleFilters.tsx +++ b/components/ScheduleFilters/ScheduleFilters.tsx @@ -1,25 +1,26 @@ import { useCallback, useEffect } from "react"; -import { IFilterDTO } from "../../dtos"; +import { IFilterDTO, ISelectedFilterDTO } from "../../dtos"; import FilterBlock from "../FilterBlock"; -import { CheckBoxProps, SelectedShift } from "../../types"; +import { CheckBoxProps } from "../../types"; +import { useAppInfo } from "../../contexts/AppInfoProvider"; interface ISelectScheduleProps { - filters: IFilterDTO[]; - handleFilters: (selectedFilter: SelectedShift[]) => void; clearSchedule: boolean; - checked: number[] | SelectedShift[]; - setChecked: (obj: number[] | SelectedShift[]) => void; + checked: number[] | ISelectedFilterDTO[]; + setChecked: (obj: number[] | ISelectedFilterDTO[]) => void; } const ScheduleFilters = ({ - filters, - handleFilters, clearSchedule, checked, setChecked, }: ISelectScheduleProps) => { + const info = useAppInfo(); + const filters = info.filters as IFilterDTO[]; + const handleFilters = info.handleFilters; + useEffect(() => { const stored = JSON.parse(localStorage.getItem("shifts")) ?? []; setChecked(stored); @@ -29,8 +30,8 @@ const ScheduleFilters = ({ const clearSelection = useCallback(() => { setChecked([]); localStorage.setItem("shifts", JSON.stringify([])); - handleFilters([]); - }, [handleFilters, setChecked]); + info.handleFilters([]); + }, [info, setChecked]); useEffect(() => { clearSchedule && clearSelection(); @@ -113,8 +114,7 @@ const ScheduleFilters = ({ layer2={semesters} checkBoxes={getCheckBoxes().slice(0, 6)} checked={checked} - setChecked={setChecked as (v: SelectedShift[]) => void} - handleFilters={handleFilters} + setChecked={setChecked as (v: ISelectedFilterDTO[]) => void} isShifts /> {/* MEI */} @@ -123,8 +123,7 @@ const ScheduleFilters = ({ layer2={semesters} checkBoxes={getCheckBoxes().slice(6, 9)} checked={checked} - setChecked={setChecked as (v: SelectedShift[]) => void} - handleFilters={handleFilters} + setChecked={setChecked as (v: ISelectedFilterDTO[]) => void} isShifts /> diff --git a/components/Settings/Settings.tsx b/components/Settings/Settings.tsx index 14d5dc6d..b1d8b4cd 100644 --- a/components/Settings/Settings.tsx +++ b/components/Settings/Settings.tsx @@ -6,35 +6,19 @@ import DarkModeToggler from "../DarkModeToggler"; import { BeforeInstallPromptEvent } from "../../types"; type SettingsProps = { - saveTheme: () => void; - filters: IFilterDTO[]; isOpen: boolean; setIsOpen: (isOpen: boolean) => void; - isHome: boolean; installPwaPrompt: BeforeInstallPromptEvent; }; -const Settings = ({ - saveTheme, - filters, - isOpen, - setIsOpen, - isHome, - installPwaPrompt, -}: SettingsProps) => { +const Settings = ({ isOpen, setIsOpen, installPwaPrompt }: SettingsProps) => { return (
{/* Title */}
Settings
{/* Configs */} - +
diff --git a/components/ShareButton/ShareButton.tsx b/components/ShareButton/ShareButton.tsx index abdea1e7..61c0314f 100644 --- a/components/ShareButton/ShareButton.tsx +++ b/components/ShareButton/ShareButton.tsx @@ -1,24 +1,14 @@ import { useState } from "react"; - import ShareModal from "../ShareModal"; - -import { IFilterDTO } from "../../dtos"; - -import { SelectedShift } from "../../types"; +import { ISelectedFilterDTO } from "../../dtos"; +import { useAppInfo } from "../../contexts/AppInfoProvider"; type ShareButtonProps = { - isHome: boolean; - filters: IFilterDTO[]; - handleFilters: any; - setChecked: (obj: number[] | SelectedShift[]) => void; + setChecked: (obj: number[] | ISelectedFilterDTO[]) => void; }; -const ShareButton = ({ - isHome, - filters, - handleFilters, - setChecked, -}: ShareButtonProps) => { +const ShareButton = ({ setChecked }: ShareButtonProps) => { + const info = useAppInfo(); const [isModalOpen, setIsModalOpen] = useState(false); return ( @@ -28,7 +18,7 @@ const ShareButton = ({ title="Share" onClick={() => setIsModalOpen(true)} data-umami-event="share-button" - data-umami-event-type={isHome ? "events" : "shifts"} + data-umami-event-type={info.isEvents ? "events" : "shifts"} > @@ -36,9 +26,6 @@ const ShareButton = ({
diff --git a/components/ShareModal/ShareModal.tsx b/components/ShareModal/ShareModal.tsx index 28e61791..61b03a77 100644 --- a/components/ShareModal/ShareModal.tsx +++ b/components/ShareModal/ShareModal.tsx @@ -2,34 +2,26 @@ import { Backdrop, Box, Fade, Modal } from "@mui/material"; import { useCallback, useEffect, useState } from "react"; -import { IFilterDTO } from "../../dtos"; - -import { SelectedShift } from "../../types"; +import { IFilterDTO, ISelectedFilterDTO } from "../../dtos"; import { Collapse } from "antd"; +import { useAppInfo } from "../../contexts/AppInfoProvider"; type ShareModalProps = { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; - isHome: boolean; - filters: IFilterDTO[]; - handleFilters: any; - setChecked: (obj: number[] | SelectedShift[]) => void; + setChecked: (obj: number[] | ISelectedFilterDTO[]) => void; }; -const ShareModal = ({ - isOpen, - setIsOpen, - isHome, - filters, - handleFilters, - setChecked, -}: ShareModalProps) => { +const ShareModal = ({ isOpen, setIsOpen, setChecked }: ShareModalProps) => { const [code, setCode] = useState(""); const [isCopied, setIsCopied] = useState(false); const [isImported, setIsImported] = useState(false); const [isError, setIsError] = useState(false); + const info = useAppInfo(); + const filters = info.filters as IFilterDTO[]; + /** * Check if the id corresponds to a filter * @param id - The id to be checked @@ -55,7 +47,7 @@ const ShareModal = ({ * The reason for a list of shifts being returned is because the same id can have multiple shifts separated by a comma * @param shift - The shift string to be parsed */ - function parseShiftValid(shift: string): SelectedShift[] | undefined { + function parseShiftValid(shift: string): ISelectedFilterDTO[] | undefined { const [idOrName, shiftName] = shift.split("="); if (!idOrName || !shiftName) return undefined; @@ -63,9 +55,10 @@ const ShareModal = ({ if (!filter) return undefined; const shifts = shiftName.split(",").map((s) => s.toUpperCase()); - if (!shifts.every((s) => filter.shifts.includes(s))) return undefined; + if (!shifts.every((s) => (filter as IFilterDTO).shifts.includes(s))) + return undefined; - return shifts.map((shift) => ({ id: filter.id, shift })); + return shifts.map((shift) => ({ id: (filter as IFilterDTO).id, shift })); } /** @@ -74,7 +67,9 @@ const ShareModal = ({ * If any shift is invalid, the function returns undefined * @param shiftsString */ - function parseShiftsValid(shiftsString: string): SelectedShift[] | undefined { + function parseShiftsValid( + shiftsString: string + ): ISelectedFilterDTO[] | undefined { const shifts = shiftsString.split("&").map(parseShiftValid); return shifts.every(Boolean) ? shifts.flat() : undefined; } @@ -86,7 +81,7 @@ const ShareModal = ({ * @param eventString - The event string to be parsed */ function parseEventValid(eventString: string): number | undefined { - const event = getFilterByIdOrName(eventString); + const event = getFilterByIdOrName(eventString) as IFilterDTO; return event ? event.id : undefined; } @@ -118,7 +113,7 @@ const ShareModal = ({ * * @param shifts - The shift to be converted */ - function shiftsToStringArray(shifts: SelectedShift[]): string[] { + function shiftsToStringArray(shifts: ISelectedFilterDTO[]): string[] { const groupedShifts = groupBy(shifts, ({ id }) => id); return Object.entries(groupedShifts).map(([id, shifts]) => { @@ -145,18 +140,22 @@ const ShareModal = ({ return eventIds.map(eventToString); } - const valuesRaw = localStorage.getItem(isHome ? "checked" : "shifts"); + const valuesRaw = localStorage.getItem( + info.isEvents ? "checked" : "shifts" + ); if (!valuesRaw) return ""; try { const values = JSON.parse(valuesRaw); - const toStringArray = isHome ? eventsToStringArray : shiftsToStringArray; + const toStringArray = info.isEvents + ? eventsToStringArray + : shiftsToStringArray; return toStringArray(values)?.join("&") || ""; } catch (error) { return ""; } - }, [isHome, filters]); + }, [info.isEvents, filters]); function copyToClipboardHandle() { navigator.clipboard.writeText(code); @@ -177,16 +176,18 @@ const ShareModal = ({ const code = rawCode as string; - const parsedData = isHome ? parseEvents(code) : parseShiftsValid(code); + const parsedData = info.isEvents + ? parseEvents(code) + : parseShiftsValid(code); if (!parsedData) { playErrorImportAnimation(); return; } - handleFilters(parsedData); + info.handleFilters(parsedData); setChecked(parsedData); localStorage.setItem( - isHome ? "checked" : "shifts", + info.isEvents ? "checked" : "shifts", JSON.stringify(parsedData) ); playValidImportAnimation(); @@ -228,7 +229,7 @@ const ShareModal = ({ id="modal-modal-title" className="select-none text-xl font-medium" > - Share Your {isHome ? "Events" : "Schedule"}{" "} + Share Your {info.isEvents ? "Events" : "Schedule"}{" "} diff --git a/components/Sidebar/Sidebar.tsx b/components/Sidebar/Sidebar.tsx index 9fc266d5..7af05988 100644 --- a/components/Sidebar/Sidebar.tsx +++ b/components/Sidebar/Sidebar.tsx @@ -1,45 +1,30 @@ import { useState, useEffect } from "react"; - import Link from "next/link"; import Image from "next/image"; - import EventFilters from "../EventFilters"; import ScheduleFilters from "../ScheduleFilters"; import Settings from "../Settings"; import ExportButton from "../ExportButton"; import ClearSelectionButton from "../ClearSelectionButton"; import NavigationPane from "../NavigationPane"; - -import { IFilterDTO } from "../../dtos"; import ShareButton from "../ShareButton"; - -import { BeforeInstallPromptEvent, SelectedShift } from "../../types"; - +import { BeforeInstallPromptEvent } from "../../types"; import { useTheme } from "next-themes"; +import { useAppInfo } from "../../contexts/AppInfoProvider"; +import { ISelectedFilterDTO } from "../../dtos"; type SidebarProps = { - isHome?: boolean; isOpen?: boolean; setIsOpen?: (isOpen: boolean) => void; - filters?: IFilterDTO[]; - handleFilters?: any; - saveTheme: () => void; }; -const Sidebar = ({ - isHome, - isOpen, - setIsOpen, - filters, - handleFilters, - saveTheme, -}: SidebarProps) => { +const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => { const [isSettings, setIsSettings] = useState(false); const [clear, setClear] = useState(false); - const [checked, setChecked] = useState([]); + const [checked, setChecked] = useState([]); const [promptInstall, setPromptInstall] = useState(null); - const [image, setImage] = useState(""); + const [image, setImage] = useState("/calendarium-light.svg"); function clearSelection() { setClear(true); @@ -59,6 +44,7 @@ const Sidebar = ({ const sidebar = `lg:w-96 lg:block lg:translate-x-0 lg:h-full h-mobile lg:shadow-md lg:border-r dark:border-neutral-400/30 w-full absolute overflow-y-auto overflow-x-hidden lg:overflow-y-auto lg:rounded-r-3xl lg:py-8 pb-8 px-8 bg-white dark:bg-neutral-900 z-10 transition ease transform duration-300`; const { resolvedTheme } = useTheme(); + const info = useAppInfo(); useEffect(() => { setImage( @@ -111,43 +97,30 @@ const Sidebar = ({ {/* Clear Schedule button */} {/* Share Button */} - +
{/* Export button */} - +
{isSettings ? ( - ) : isHome ? ( + ) : info.isEvents ? ( ) : ( void; - filters: IFilterDTO[]; isOpen: boolean; setIsOpen: (isOpen: boolean) => void; - isHome: boolean; }; -const Themes = ({ - saveTheme, - filters, - isOpen, - setIsOpen, - isHome, -}: ThemesProps) => { - const [theme, setTheme] = useState("Modern"); - const [colors, setColors] = useState(defaultColors); - const [opacity, setOpacity] = useState(true); - const [openColor, setOpenColor] = useState(0); - const [customType, setCustomType] = useState("Subject"); +const Themes = ({ isOpen, setIsOpen }: ThemesProps) => { + const info = useAppInfo(); + const filters = info.filters as IFilterDTO[]; + + const { + setOpacity, + setSubjectColors, + setTheme, + theme, + subjectColors, + opacity, + saveThemeChanges, + } = useColorTheme(filters); + const [checkedFilters, setCheckedFilters] = useState([]); - const [subjectColors, setSubjectColors] = useState([]); const [checkedClasses, setCheckedClasses] = useState([]); + const [openColor, setOpenColor] = useState(0); + const [pendingThemeUpdate, setPendingThemeUpdate] = useState( + null + ); - const checkedThings = isHome ? checkedFilters : checkedClasses; + const checkedThings = info.isEvents ? checkedFilters : checkedClasses; function getSubjectColor(index: number) { return subjectColors.find((sc) => sc.filterId === checkedThings[index]) @@ -62,43 +65,15 @@ const Themes = ({ (sc) => sc.filterId === checkedThings[openColor] ).color = newColor; setSubjectColors(newSubjectColors); - localStorage.setItem("subjectColors", JSON.stringify(newSubjectColors)); - } - - function getBgColor(index: number) { - if (opacity) return reduceOpacity(colors[index + 1]); - else return colors[index + 1]; - } - - function getTextColor(index: number) { - if (opacity) return colors[index + 1]; - else return "white"; - } - - function updateColors(newColor: string) { - const newColors = [...colors]; - newColors[openColor + 1] = newColor; - setColors(newColors); - localStorage.setItem("colors", newColors.join(",")); } function updateTheme(newTheme: string) { setTheme(newTheme); - localStorage.setItem("theme", newTheme); - saveTheme(); - isOpen && newTheme !== "Custom" && setIsOpen(false); + setPendingThemeUpdate(newTheme); } function updateOpacity(updateOpacity: boolean) { setOpacity(updateOpacity); - localStorage.setItem("opacity", updateOpacity.toString()); - } - - function updateCustomType(newCustomType: string) { - setCustomType(newCustomType); - localStorage.setItem("customType", newCustomType); - setOpenColor(0); - saveTheme(); } function backToSubjectDefault() { @@ -109,64 +84,22 @@ const Themes = ({ (sc) => sc.filterId === filterId ); subjectColor.color = - defaultColors[filters.find((f) => f.id === filterId).groupId]; + DEFAULT_COLORS[filters.find((f) => f.id === filterId).groupId]; }); setSubjectColors(newSubjectColors); - localStorage.setItem("subjectColors", JSON.stringify(newSubjectColors)); - - setOpacity(true); - localStorage.setItem("opacity", "true"); - } - - function backToDefault() { - setColors(defaultColors); setOpacity(true); - localStorage.setItem("colors", defaultColors.join(",")); - localStorage.setItem("opacity", "true"); } - const getThemeSettings = useCallback(() => { - function initializeSubjectColors() { - const newSubjectColors: SubjectColor[] = []; - filters.forEach((f) => { - newSubjectColors.push({ - filterId: f.id, - color: defaultColors[f.groupId], - }); - }); - setSubjectColors(newSubjectColors); - localStorage.setItem("subjectColors", JSON.stringify(newSubjectColors)); - } - - const theme = localStorage.getItem("theme"); - const colors = localStorage.getItem("colors"); - const opacity = localStorage.getItem("opacity") === "true"; - const customType = localStorage.getItem("customType"); + const initializeVariables = useCallback(() => { const checkedFilters: number[] = JSON.parse(localStorage.getItem("checked")) ?? []; - const subjectColors: SubjectColor[] = JSON.parse( - localStorage.getItem("subjectColors") - ); - const checkedShifts: { id: number; shift: string }[] = JSON.parse( localStorage.getItem("shifts") ); - if (theme) setTheme(theme); - - if (colors) setColors(colors.split(",")); - - if (opacity) setOpacity(opacity); - - if (customType) setCustomType(customType); - if (checkedFilters) setCheckedFilters(checkedFilters); - if (subjectColors && subjectColors.length > 0) - setSubjectColors(subjectColors); - else initializeSubjectColors(); - if (checkedShifts) { const checkedClasses: number[] = checkedShifts.map( (s: { id: number; shift: string }) => s.id @@ -176,11 +109,28 @@ const Themes = ({ ); setCheckedClasses(uniqueCheckedClasses); } - }, [filters]); + }, []); + + const saveTheme = () => { + saveThemeChanges(); + info.fetchTheme(); + }; useEffect(() => { - getThemeSettings(); - }, [getThemeSettings]); + initializeVariables(); + }, [initializeVariables]); + + // make sure theme changes are saved and applied + useEffect(() => { + if (pendingThemeUpdate) { + saveThemeChanges(); + info.fetchTheme(); + if (isOpen && pendingThemeUpdate !== "Custom") { + setIsOpen(false); + } + setPendingThemeUpdate(null); + } + }, [info, isOpen, setIsOpen, pendingThemeUpdate, saveThemeChanges]); return ( <> @@ -204,97 +154,8 @@ const Themes = ({
{theme === "Custom" && ( -
-
- - -
- - {customType === "Year" ? ( -
-
-
-
- {colors.slice(1, 6).map((color, index) => ( - - ))} -
-
- updateColors(newColor)} - /> -
-
-
-
- Hex -
- updateColors(newColor)} - /> -
-
-
- - - Use setting - - - Opacity - - - -
-
- ) : checkedThings.length > 0 ? ( + <> + {checkedThings.length > 0 ? (
@@ -342,7 +203,7 @@ const Themes = ({ onChange={updateOpacity} className={`${ opacity ? "bg-cesium-900" : "bg-neutral-200" - } ${"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"}`} + } ${"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-cesium-900 focus:ring-offset-2"}`} > Use setting
) : ( -
+
{" "} Select at least one subject.
)} - {(customType === "Year" || checkedThings.length > 0) && ( + {checkedThings.length > 0 && (
+ )} ); diff --git a/contexts/AppInfoProvider.tsx b/contexts/AppInfoProvider.tsx new file mode 100644 index 00000000..b057d5f6 --- /dev/null +++ b/contexts/AppInfoProvider.tsx @@ -0,0 +1,29 @@ +import { createContext, useContext } from "react"; +import { IFilterDTO, ISelectedFilterDTO } from "../dtos"; + +interface AppInfoContextData { + isEvents: boolean; + filters: number[] | IFilterDTO[]; + handleFilters: (filters: number[] | ISelectedFilterDTO[]) => void; + fetchTheme: () => void; +} + +const AppContext = createContext(undefined); + +export function AppInfoProvider({ + children, + data, +}: { + children: React.ReactNode; + data: AppInfoContextData; +}) { + return {children}; +} + +export function useAppInfo() { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error("useAppInfo must be used within a AppInfoProvider"); + } + return context; +} diff --git a/dtos/index.ts b/dtos/index.ts index 6bfd9211..e019ffdb 100644 --- a/dtos/index.ts +++ b/dtos/index.ts @@ -6,6 +6,11 @@ export interface IFilterDTO { shifts?: string[]; } +export interface ISelectedFilterDTO { + id: number; + shift?: string; +} + export interface IEventDTO { title: string; place: string; diff --git a/hooks/useColorTheme.ts b/hooks/useColorTheme.ts new file mode 100644 index 00000000..58b112ec --- /dev/null +++ b/hooks/useColorTheme.ts @@ -0,0 +1,180 @@ +import { SetStateAction, useCallback, useState, useEffect } from "react"; +import { SubjectColor } from "../types"; +import { IFormatedShift } from "../pages/schedule"; +import { IEventDTO } from "../dtos"; +import { IFilterDTO } from "../dtos"; + +const DEFAULT_THEME = "Modern"; +export const DEFAULT_COLORS = [ + "#ed7950", // cesium + "#4BC0D9", // 1st year + "#7b54f0", // 2nd year + "#f0547b", // 3rd year + "#5ac77b", // 4th year + "#395B50", // 5th year + "#b70a0a", // uminho + "#3408fd", // sei + "#642580", // coderdojo + "#FF0000", // join + "#1B69EE", // jordi + "#FF499E", // codeweek + "#66B22E", // bugsbyte +]; + +export function reduceOpacity(hexColor) { + // Convert HEX color code to RGBA color code + let r = parseInt(hexColor.slice(1, 3), 16); + let g = parseInt(hexColor.slice(3, 5), 16); + let b = parseInt(hexColor.slice(5, 7), 16); + let a = 0.25; // 25% opacity + let rgbaColor = `rgba(${r}, ${g}, ${b}, ${a})`; + + return rgbaColor; +} + +function mergeColors(colors: string[]) { + let merged = [...colors]; + for (let i = 0; i < DEFAULT_COLORS.length; i++) { + if (merged[i] === undefined) merged[i] = DEFAULT_COLORS[i]; + } + return merged; +} + +function initializeSubjectColors(filters: IFilterDTO[]) { + const newSubjectColors: SubjectColor[] = []; + filters.forEach((f) => { + newSubjectColors.push({ + filterId: f.id, + color: DEFAULT_COLORS[f.groupId], + }); + }); + return newSubjectColors; +} + +const fetchTheme = ( + setTheme: (value: SetStateAction) => void, + setOpacity: (value: SetStateAction) => void, + setSubjectColors: (value: SetStateAction) => void, + filters: IFilterDTO[] +) => { + let themeLS = localStorage.getItem("theme"); + const colorsLS = localStorage.getItem("colors"); + const opacityLS = localStorage.getItem("opacity"); + const subjectColorsLS: SubjectColor[] = + JSON.parse(localStorage.getItem("subjectColors")) ?? []; + + // error proof checks + colorsLS && + colorsLS.split(",").length !== DEFAULT_COLORS.length && + localStorage.setItem("colors", mergeColors(colorsLS.split(",")).join(",")); + !themeLS && localStorage.setItem("theme", "Modern"); + subjectColorsLS.length <= 0 && + localStorage.setItem( + "subjectColors", + JSON.stringify(initializeSubjectColors(filters)) + ); + + if (themeLS !== "Modern" && themeLS !== "Classic" && themeLS !== "Custom") { + localStorage.setItem("theme", "Modern"); + themeLS = "Modern"; + } + + setTheme(themeLS); + opacityLS ? setOpacity(opacityLS === "true") : setOpacity(true); + subjectColorsLS && setSubjectColors(subjectColorsLS); +}; + +function getDefaultColor(event: IFormatedShift | IEventDTO): string { + if ((event as IFormatedShift).id !== undefined) { + return DEFAULT_COLORS[String((event as IFormatedShift).filterId)[0]]; + } + if ((event as IEventDTO).groupId !== undefined) { + return DEFAULT_COLORS[(event as IEventDTO).groupId]; + } +} + +// note: returns the default color if it was not found in the subjectColors array +function getSubjectColor( + event: IFormatedShift | IEventDTO, + subjectColors: SubjectColor[] +) { + const color = subjectColors.find( + (sc) => sc.filterId === event.filterId + )?.color; + return color ? color : getDefaultColor(event); +} + +function getBgColor( + event: IFormatedShift | IEventDTO, + theme: string, + opacity: boolean, + subjectColors: SubjectColor[] +) { + let color: string = "#000000"; + + if (theme === "Modern") color = reduceOpacity(getDefaultColor(event)); + else if (theme === "Classic") color = getDefaultColor(event); + else if (theme === "Custom") { + opacity + ? (color = reduceOpacity(getSubjectColor(event, subjectColors))) + : (color = getSubjectColor(event, subjectColors)); + } + + return color; +} + +function getTextColor( + event: IFormatedShift | IEventDTO, + theme: string, + opacity: boolean, + subjectColors: SubjectColor[] +) { + let color: string = "#000000"; + + if (theme === "Modern") color = getDefaultColor(event); + else if (theme === "Classic") color = "white"; + else if (theme === "Custom") { + opacity + ? (color = getSubjectColor(event, subjectColors)) + : (color = "white"); + } + + return color; +} + +export const useColorTheme = (filters: IFilterDTO[]) => { + const [theme, setTheme] = useState(DEFAULT_THEME); + const [opacity, setOpacity] = useState(true); + const [subjectColors, setSubjectColors] = useState([]); + + const fetchThemeCallBack = useCallback(() => { + fetchTheme(setTheme, setOpacity, setSubjectColors, filters); + }, [filters]); + + useEffect(() => { + fetchThemeCallBack(); + }, [fetchThemeCallBack]); + + const saveThemeChanges = useCallback(() => { + localStorage.setItem("theme", theme); + localStorage.setItem("opacity", opacity.toString()); + localStorage.setItem("subjectColors", JSON.stringify(subjectColors)); + }, [theme, opacity, subjectColors]); + + return { + saveThemeChanges, + fetchTheme: fetchThemeCallBack, + getBgColor: (event: IFormatedShift | IEventDTO) => + getBgColor(event, theme, opacity, subjectColors), + getTextColor: (event: IFormatedShift | IEventDTO) => + getTextColor(event, theme, opacity, subjectColors), + theme, + setTheme, + opacity, + setOpacity, + subjectColors, + setSubjectColors, + }; +}; + +export default useColorTheme; diff --git a/hooks/useWindowSize.ts b/hooks/useWindowSize.ts new file mode 100644 index 00000000..7a827c2c --- /dev/null +++ b/hooks/useWindowSize.ts @@ -0,0 +1,34 @@ +import { useState, useEffect } from "react"; + +export function useWindowSize() { + // initialize state with undefined width/height so server and client renders match + // learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ + const [windowSize, setWindowSize] = useState({ + width: undefined, + height: undefined, + }); + + useEffect(() => { + // only execute all the code below in client side + // handler to call on window resize + function handleResize() { + // set window width/height to state + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + + // add event listener + window.addEventListener("resize", handleResize); + + // call handler right away so state gets updated with initial window size + handleResize(); + + // remove event listener on cleanup + return () => window.removeEventListener("resize", handleResize); + }, []); // empty array ensures that effect is only run on mount + return windowSize; +} + +export default useWindowSize; diff --git a/pages/_document.tsx b/pages/_document.tsx index ad1abd94..3c6d248f 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -20,7 +20,7 @@ export default function Document() { {/* Splash Screen configuration for IOS devices */} - + ([]); // events fetched from the API const [events, setEvents] = useState([]); // events to be displayed - const [Filters, setFilters] = useState(filters); + const [Filters, setFilters] = useState(filters); const [selectedEvent, setSelectedEvent] = useState(events[0]); const [inspectEvent, setInspectEvent] = useState(false); @@ -96,15 +95,9 @@ export default function Home({ filters }) { setEvents(newEvents); }, []); - useEffect(() => { - handleData(); - }, [handleData]); - const handleFilters = useCallback( (myFilters: number[]) => { - const showNewEvents = (f) => { - const filters = Object.values(f); - + const showNewEvents = (filters: number[]) => { let newEvents = [...fetchedEvents]; if (filters.length > 0) { @@ -115,10 +108,8 @@ export default function Home({ filters }) { setEvents(newEvents); }; - - const newFilters = { ...myFilters }; - setFilters(newFilters); - showNewEvents(newFilters); + setFilters(myFilters); + showNewEvents(myFilters); }, [fetchedEvents] ); @@ -130,102 +121,13 @@ export default function Home({ filters }) { // THEMES - const [theme, setTheme] = useState("Modern"); - const [colors, setColors] = useState(defaultColors); - const [opacity, setOpacity] = useState(true); - const [subjectColors, setSubjectColors] = useState([]); - const [customType, setCustomType] = useState("Year"); - - // note: returns the default color if it was not found in the subjectColors array - function getSubjectColor(event: IEventDTO) { - const color = subjectColors.find( - (sc) => sc.filterId === event.filterId - )?.color; - return color ? color : defaultColors[event.groupId]; - } - - function getBgColor(event: IEventDTO) { - let color: string = "#000000"; - - if (theme === "Modern") color = reduceOpacity(defaultColors[event.groupId]); - else if (theme === "Classic") color = defaultColors[event.groupId]; - else if (theme === "Custom") { - if (customType === "Year") { - opacity - ? (color = reduceOpacity( - colors[event.groupId] ?? defaultColors[event.groupId] - )) - : (color = colors[event.groupId] ?? defaultColors[event.groupId]); - } else if (customType === "Subject") { - opacity - ? (color = reduceOpacity(getSubjectColor(event))) - : (color = getSubjectColor(event)); - } - } - - return color; - } - - function getTextColor(event: IEventDTO) { - let color: string = "#000000"; - - if (theme === "Modern") color = defaultColors[event.groupId]; - else if (theme === "Classic") color = "white"; - else if (theme === "Custom") { - if (customType === "Year") { - opacity - ? (color = colors[event.groupId] ?? defaultColors[event.groupId]) - : (color = "white"); - } else if (customType === "Subject") { - opacity ? (color = getSubjectColor(event)) : (color = "white"); - } - } - - return color; - } - - function saveTheme() { - let theme = localStorage.getItem("theme"); - const colors = localStorage.getItem("colors"); - const opacity = localStorage.getItem("opacity"); - const customType = localStorage.getItem("customType") ?? "Year"; - const subjectColors: SubjectColor[] = - JSON.parse(localStorage.getItem("subjectColors")) ?? []; - - // error proof checks - colors && - colors.split(",").length !== defaultColors.length && - localStorage.setItem("colors", mergeColors(colors.split(",")).join(",")); - !theme && localStorage.setItem("theme", "Modern"); - !customType && localStorage.setItem("customType", "Subject"); - - if (theme !== "Modern" && theme !== "Classic" && theme !== "Custom") { - localStorage.setItem("theme", "Modern"); - theme = "Modern"; - } + const { fetchTheme, getBgColor, getTextColor } = useColorTheme(filters); - setTheme(theme); - if (theme === "Custom") { - setCustomType(customType); - - switch (customType) { - case "Year": { - colors ? setColors(colors.split(",")) : setColors(defaultColors); - opacity ? setOpacity(opacity === "true") : setOpacity(true); - break; - } - case "Subject": { - opacity ? setOpacity(opacity === "true") : setOpacity(true); - subjectColors && setSubjectColors(subjectColors); - break; - } - } - } - } + // INITIALIZATION useEffect(() => { - saveTheme(); - }, []); + handleData(); + }, [handleData]); // RELATED TO react-big-calendar @@ -245,10 +147,10 @@ export default function Home({ filters }) { return ( Events | Calendarium diff --git a/pages/schedule.tsx b/pages/schedule.tsx index 7b18c96d..b5faf0b3 100644 --- a/pages/schedule.tsx +++ b/pages/schedule.tsx @@ -1,24 +1,16 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; - import Head from "next/head"; - +import { useTheme } from "next-themes"; import * as fs from "fs"; - +import moment from "moment"; import { Calendar, momentLocalizer } from "react-big-calendar"; import "react-big-calendar/lib/css/react-big-calendar.css"; - -import moment from "moment"; - import ShiftModal from "../components/ShiftModal"; import Layout from "../components/Layout"; -import { IFilterDTO, IShiftDTO } from "../dtos"; -import { reduceOpacity, defaultColors, mergeColors } from "../utils"; -import { SubjectColor } from "../types"; - +import { IFilterDTO, IShiftDTO, ISelectedFilterDTO } from "../dtos"; +import useColorTheme from "../hooks/useColorTheme"; import styles from "../styles/schedule.module.css"; -import { useTheme } from "next-themes"; - const localizer = momentLocalizer(moment); export interface IFormatedShift { @@ -34,139 +26,20 @@ export interface IFormatedShift { filterId: number; } -interface ISelectedFilter { - id: number; - shift?: string; -} - interface ISchedulesProps { filters: IFilterDTO[]; shifts: IShiftDTO[]; } export default function Schedule({ filters, shifts }: ISchedulesProps) { - const [events, setEvents] = useState([]); - const [selectedFilters, setSelectedFilters] = useState([]); - const [selectedShift, setSelectedShift] = useState(shifts[0]); - const [inspectShift, setInspectShift] = useState(false); - - const handleSelection = (shift) => { - setSelectedShift(shift); - setInspectShift(!inspectShift); - }; - - // THEMES - - const [colorTheme, setColorTheme] = useState("Modern"); - const [colors, setColors] = useState(defaultColors); - const [opacity, setOpacity] = useState(true); - const [subjectColors, setSubjectColors] = useState([]); - const [customType, setCustomType] = useState("Year"); - - function getDefaultColor(event: IFormatedShift) { - return defaultColors[String(event.filterId)[0]]; - } - - // note: returns the default color if it was not found in the subjectColors array - function getSubjectColor(event: IFormatedShift) { - const color = subjectColors.find( - (sc) => sc.filterId === event.filterId - )?.color; - return color ? color : getDefaultColor(event); - } - - function getBgColor(event: IFormatedShift) { - let color: string = "#000000"; - - if (colorTheme === "Modern") color = reduceOpacity(getDefaultColor(event)); - else if (colorTheme === "Classic") color = getDefaultColor(event); - else if (colorTheme === "Custom") { - if (customType === "Year") { - opacity - ? (color = reduceOpacity( - colors[String(event.filterId)[0]] ?? getDefaultColor(event) - )) - : (color = - colors[String(event.filterId)[0]] ?? getDefaultColor(event)); - } else if (customType === "Subject") { - opacity - ? (color = reduceOpacity(getSubjectColor(event))) - : (color = getSubjectColor(event)); - } - } - - return color; - } + // EVENT RELATED - function getTextColor(event: IFormatedShift) { - let color: string = "#000000"; - - if (colorTheme === "Modern") color = getDefaultColor(event); - else if (colorTheme === "Classic") color = "white"; - else if (colorTheme === "Custom") { - if (customType === "Year") { - opacity - ? (color = - colors[String(event.filterId)[0]] ?? getDefaultColor(event)) - : (color = "white"); - } else if (customType === "Subject") { - opacity ? (color = getSubjectColor(event)) : (color = "white"); - } - } - - return color; - } - - function saveTheme() { - let theme = localStorage.getItem("theme"); - const colors = localStorage.getItem("colors"); - const opacity = localStorage.getItem("opacity"); - const customType = localStorage.getItem("customType"); - const subjectColors: SubjectColor[] = - JSON.parse(localStorage.getItem("subjectColors")) ?? []; - - // error proof checks - colors && - colors.split(",").length !== defaultColors.length && - localStorage.setItem("colors", mergeColors(colors.split(",")).join(",")); - !theme && localStorage.setItem("theme", "Modern"); - !customType && localStorage.setItem("customType", "Subject"); - - if (theme !== "Modern" && theme !== "Classic" && theme !== "Custom") { - localStorage.setItem("theme", "Modern"); - theme = "Modern"; - } - - setColorTheme(theme); - if (theme === "Custom") { - setCustomType(customType); - - switch (customType) { - case "Year": { - colors ? setColors(colors.split(",")) : setColors(defaultColors); - opacity ? setOpacity(opacity === "true") : setOpacity(true); - break; - } - case "Subject": { - opacity ? setOpacity(opacity === "true") : setOpacity(true); - subjectColors && setSubjectColors(subjectColors); - break; - } - } - } - } - - const formats = useMemo( - () => ({ - dayFormat: (date, _, localizer) => localizer.format(date, "ddd"), - eventTimeRangeFormat: () => { - return ""; - }, - timeGutterFormat: (date, culture, localizer) => - localizer.format(date, "HH\\h", culture).replace(/^0+/, ""), - }), + const [events, setEvents] = useState([]); + const [selectedFilters, setSelectedFilters] = useState( [] ); + const [selectedShift, setSelectedShift] = useState(shifts[0]); + const [inspectShift, setInspectShift] = useState(false); const formatEvents = useCallback(() => { const filteredShifts = shifts.filter((shift) => { @@ -206,13 +79,38 @@ export default function Schedule({ filters, shifts }: ISchedulesProps) { setEvents(formatedEvents); }, [shifts, selectedFilters, filters]); + const handleFilters = useCallback((myFilters: ISelectedFilterDTO[]) => { + setSelectedFilters(myFilters); + }, []); + + const handleSelection = (shift) => { + setSelectedShift(shift); + setInspectShift(!inspectShift); + }; + + // THEMES + + const { fetchTheme, getBgColor, getTextColor } = useColorTheme(filters); + + // INITIALIZATION + useEffect(() => { formatEvents(); }, [selectedFilters, formatEvents]); - useEffect(() => { - saveTheme(); - }, []); + // RELATED TO react-big-calendar + + const formats = useMemo( + () => ({ + dayFormat: (date, _, localizer) => localizer.format(date, "ddd"), + eventTimeRangeFormat: () => { + return ""; + }, + timeGutterFormat: (date, culture, localizer) => + localizer.format(date, "HH\\h", culture).replace(/^0+/, ""), + }), + [] + ); const minDate = new Date(); minDate.setHours(8, 0, 0); @@ -220,18 +118,16 @@ export default function Schedule({ filters, shifts }: ISchedulesProps) { const maxDate = new Date(); maxDate.setHours(20, 0, 0); - const { resolvedTheme } = useTheme(); + // RELATED TO APPEARANCE - const handleFilters = useCallback((myFilters) => { - setSelectedFilters(myFilters); - }, []); + const { resolvedTheme } = useTheme(); return ( Schedule | Calendarium diff --git a/types/index.ts b/types/index.ts index bf4eb450..d9e0cc53 100644 --- a/types/index.ts +++ b/types/index.ts @@ -9,11 +9,6 @@ export type CheckBoxProps = { shifts?: string[]; }; -export type SelectedShift = { - id: number; - shift: string; -}; - export interface BeforeInstallPromptEvent extends Event { readonly platforms: Array; diff --git a/utils/index.ts b/utils/index.ts index 7e513113..4b68ba19 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,9 +1,7 @@ import moment from "moment-timezone"; -import { useEffect, useState } from "react"; - -import { IEventDTO } from "../dtos"; import { sheets_v4 } from "googleapis"; +import { IEventDTO } from "../dtos"; // EVENTS @@ -96,72 +94,3 @@ export async function getEvents( ); // filter out invalid/incomplete events } } - -// UI - -export function reduceOpacity(hexColor) { - // Convert HEX color code to RGBA color code - let r = parseInt(hexColor.slice(1, 3), 16); - let g = parseInt(hexColor.slice(3, 5), 16); - let b = parseInt(hexColor.slice(5, 7), 16); - let a = 0.25; // 25% opacity - let rgbaColor = `rgba(${r}, ${g}, ${b}, ${a})`; - - return rgbaColor; -} - -export const defaultColors = [ - "#ed7950", // cesium - "#4BC0D9", // 1st year - "#7b54f0", // 2nd year - "#f0547b", // 3rd year - "#5ac77b", // 4th year - "#395B50", // 5th year - "#b70a0a", // uminho - "#3408fd", // sei - "#642580", // coderdojo - "#FF0000", // join - "#1B69EE", // jordi - "#FF499E", // codeweek - "#66B22E", // bugsbyte -]; - -export function mergeColors(colors: string[]) { - let merged = [...colors]; - for (let i = 0; i < defaultColors.length; i++) { - if (merged[i] === undefined) merged[i] = defaultColors[i]; - } - return merged; -} - -// hook -export function useWindowSize() { - // initialize state with undefined width/height so server and client renders match - // learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ - const [windowSize, setWindowSize] = useState({ - width: undefined, - height: undefined, - }); - - useEffect(() => { - // only execute all the code below in client side - // handler to call on window resize - function handleResize() { - // set window width/height to state - setWindowSize({ - width: window.innerWidth, - height: window.innerHeight, - }); - } - - // add event listener - window.addEventListener("resize", handleResize); - - // call handler right away so state gets updated with initial window size - handleResize(); - - // remove event listener on cleanup - return () => window.removeEventListener("resize", handleResize); - }, []); // empty array ensures that effect is only run on mount - return windowSize; -}