diff --git a/.gitignore b/.gitignore index 4d29575..92a61ff 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ .env.development.local .env.test.local .env.production.local +.env* npm-debug.log* yarn-debug.log* diff --git a/src/App.css b/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.js b/src/App.js index 765395c..a86c1d0 100644 --- a/src/App.js +++ b/src/App.js @@ -6,7 +6,7 @@ import { Hour } from "./Hour"; import { Modal } from "./Modal"; import { reducer } from "./Reducer"; import { dayGenerator, hourGenerator, dateGenerator } from "./Generator"; -import { addEvent as addEventApi, editEvent as editEventApi, deleteEvent as deleteEventApi } from "./Events"; +import { getEvents as getEventsApi, addEvent as addEventApi, editEvent as editEventApi, deleteEvent as deleteEventApi } from "./api"; import { Dropdown } from "./Dropdown"; // inspired by https://www.youtube.com/watch?v=m9OSBJaQTlM @@ -24,8 +24,8 @@ import { Dropdown } from "./Dropdown"; // - See if there's an API for all dates of holidays and auto populate? // - Add reminder option and alert if you're in the range of time set on event // - Create "Are you sure?" dialog when deleting an Event -// - Link and write functions for storage of events in DynamoDB -// - Make tests (with Jest?) +// - Add auth register/login, allow users to only see their own events +// - Create S3 bucket function App() { @@ -74,13 +74,13 @@ function App() { const [state, dispatch] = useReducer(reducer, initialState); const initial = { - id: null, - title: "", - timeFrom: null, - timeTo: null, - description: "", - allDay: false, - color: "#0057ba", + EventID: '', + Title: "", + TimeFrom: '', + TimeTo: '', + Description: "", + AllDay: false, + Color: "#0057ba", // repeat: { // daily: false, // weekly: false, @@ -92,7 +92,7 @@ function App() { }; const [tempEvent, setTempEvent] = useState(initial); - const [events, setEvents] = useState(localStorage.getItem("events") ? JSON.parse(localStorage.getItem("events")) : []); + const [events, setEvents] = useState([]); const [cachedTab, setCachedTab] = useState(null); const modalRef = useRef(null); @@ -102,7 +102,48 @@ function App() { const selectedDate = useRef(0); + // move these into redux? + const [isLoaded, setIsLoaded] = useState(false); + const [cooldown, setCooldown] = useState(false); + const [error, setError] = useState(''); + // #region Render functions + + const request = async () => { + try { + const result = await getEventsApi(); + setEvents(result); + } catch (err) { + setError(`Error getting events: ${err}`); + }; + }; + + const getEvents = (type) => { + setIsLoaded(false); + + // should probably remove sync for exploit purposes, but good for testing + if (type === 'sync') { + if (!cooldown) { + request(); + setCooldown(true); + + setTimeout(() => { + setCooldown(false); + }, 5000); // can be abused with page refresh! + } else { + console.log("too fast!"); + }; + } else { + !localStorage.getItem("events") ? request() : setEvents(JSON.parse(localStorage.getItem("events"))); + }; + + setIsLoaded(true); + }; + + useEffect(() => { + getEvents(); + }, []); + // UPDATE STATE WITH NEW WEEK/MONTH/YEAR VALUES FOR FUNCTIONS TO READ useEffect(() => { @@ -157,6 +198,7 @@ function App() { openModal(e.target.id, e.target.name)} + aria-label={data.dayString} > {data?.events?.map((e) => { - if (e.allDay) return ""; + if (e.AllDay) return ""; - const [fromHour, fromMins] = e.timeFrom.split(":").map(Number); - const [toHour, toMins] = e.timeTo.split(":").map(Number); + const [fromHour, fromMins] = e.TimeFrom.split(":").map(Number); + const [toHour, toMins] = e.TimeTo.split(":").map(Number); return ( - {e.title} + {e.Title} ); })} @@ -276,64 +318,63 @@ function App() { setToMonth(e.currentTarget.id)} + aria-label={`${month} ${state.year}`} > - + )} + ); @@ -407,6 +448,18 @@ function App() { }, }); }; + + // HANDLE FILTER MENU CLICKS + + const handleClickCapture = (e) => { + if (e.target.name === 'sync') { + getEvents('sync'); + } else if (e.target.name === 'today') { + setToday(); + } else { + setFilter(e.target.name); + }; + }; // #endregion // #region Event functions @@ -415,7 +468,7 @@ function App() { const openModal = (id, type) => { if (!type) return; - const [date, hour, clicked] = id.split("_"); // get info from passed id + const [, hour, clicked] = id.split("_"); // get info from passed id const hoursExist = hour ? true : false; // if split is successful, hour should exist const dateNow = new Date(); @@ -433,21 +486,19 @@ function App() { const isOne = clicked === "1"; // did they click the top half of the hour (00-30)? const parsedHour = parseInt(hour); - const index = events.findIndex((e) => e.date === date); - setTempEvent(() => { return type === "edit-event" - ? events[index].events.find((e) => e.id === id) + ? events.find((e) => e.EventID === id) : { ...initial, // if the time is below 10, add a 0 | if they clicked the top half of the hour (00-30), make it the start of the hour - timeFrom: hoursExist + TimeFrom: hoursExist ? `${belowTen ? "0" : ""}${hour}:${isOne ? "00" : "30"}` : `${time}`, // if the time is below 10 or is the top half of 9, add a 0 | if they clicked the bottom half of the hour (30-60), make it the next hour - timeTo: hoursExist + TimeTo: hoursExist ? `${parsedHour + 1 < 10 ? "0" : parsedHour === 9 && isOne @@ -458,7 +509,7 @@ function App() { }; }); - selectedDate.current = id; + selectedDate.current = id; // store the element's id to pass onto addEvent/editEvent/deleteEvent setCachedTab(document.activeElement); // captures the last focused element to jump back to after the modal is closed dispatch({ type: "CHANGE_MODAL", payload: type }); modalRef.current.openModal(); @@ -466,49 +517,46 @@ function App() { // SAVING A NEW EVENT - const addEvent = (e, id) => { + const addEvent = async (e, id) => { e.preventDefault(); - const result = addEventApi(tempEvent, id); - - setEvents(() => { - return events.find((e) => e.date === result.date) - ? events.map((e) => (e.date !== result.date ? e : result)) - : [...events, result]; - }); - - modalRef.current.closeModal(); + try { + const result = await addEventApi(tempEvent, id); + setEvents([...events, result]); + closeModal(); + } catch (err) { + setError(`Error adding event: ${err}`); + }; }; // SAVING AN EDITED EVENT - const editEvent = (e, id) => { + const editEvent = async (e, id) => { e.preventDefault(); - const result = editEventApi(tempEvent, id); - - setEvents( - events.map((e) => { - return e.date !== result.date ? e : result; - }) - ); - - modalRef.current.closeModal(); + try { + await editEventApi(tempEvent, id); + setEvents(events.map((e) => e.EventID !== id ? e : tempEvent)); + closeModal(); + } catch (err) { + setError(`Error editing event: ${err}`); + }; }; // DELETING AN EVENT - const deleteEvent = (id) => { - const date = id.split("_")[0]; - - const result = deleteEventApi(date, id); - - setEvents( - events.map((e) => { - return e.date !== date ? e : { ...e, events: result }; - }) - ); + const deleteEvent = async (id) => { + try { + await deleteEventApi(id); + setEvents(events.filter((e) => e.EventID !== id)); + closeModal(); + } catch (err) { + setError(`Error deleting event: ${err}`); + }; + }; + const closeModal = () => { + setError(''); modalRef.current.closeModal(); }; @@ -520,179 +568,180 @@ function App() { }; // #endregion - return ( - <> - -
- - cycle("left")}> - ← - - cycle("right")}> - → - - {state.dateDisplay} - - - - e.target.name === "today" - ? setToday() - : setFilter(e.target.name) - } - > - + if (!isLoaded) { + return

Loading...

+ } else { + return ( + <> + +
+ + cycle("left")}> + ← + + cycle("right")}> + → + + {state.dateDisplay} + + + handleClickCapture(e)} + > + + + {/* */} - {/* */} - -
-
- {state.filter === "week" && ( - <> - - - {weekdays.map((day, i) => - - {day.slice(0, 3)}{day.slice(3, -1)} - - )} - - - {[...Array(24)].map((_, i) => ( -
  • {i}
  • - ))} -
    - - {[...Array(7)].map((_, i) => { - return displayHours(state.newDay + i); - })} - -
    - - )} - {state.filter === "month" && ( - - {weekdays.map((day, i) => - - {day.slice(0, 3)}{day.slice(3, -1)} - - )} - {[...Array(state.paddingDays + state.monthLength)].map((_, i) => { - return displayDays(i); - })} - - )} - {state.filter === "year" && ( - - {months.map((month, i) => { - return displayMonths(month, i); - })} - - )} -
    -
    - -
    - state.modalType === "add-event" - ? addEvent(e, selectedDate.current) - : editEvent(e, selectedDate.current) - } +
    +
    + {state.filter === "week" && ( + <> + + + {weekdays.map((day, i) => + + {day.slice(0, 3)}{day.slice(3)} + + )} + + + {[...Array(24)].map((_, i) => ( +
  • {i}
  • + ))} +
    + + {[...Array(7)].map((_, i) => { + return displayHours(state.newDay + i); + })} + +
    + + )} + {state.filter === "month" && ( + + {weekdays.map((day, i) => + + {day.slice(0, 3)}{day.slice(3)} + + )} + {[...Array(state.paddingDays + state.monthLength)].map((_, i) => { + return displayDays(i); + })} + + )} + {state.filter === "year" && ( + + {months.map((month, i) => { + return displayMonths(month, i); + })} + + )} +
    +
    + - -

    {state.modalType === "add-event" ? "New" : "Edit"} Event

    - modalRef.current.closeModal()} - > - X - -
    -
    -
    - - setTempEvent({ ...tempEvent, color: e.target.value }) - } - value={tempEvent.color} - ref={firstTab} - /> - - setTempEvent({ ...tempEvent, title: e.target.value }) - } - placeholder="Event Title" - value={tempEvent.title} - required - /> - <label> - <Checkbox - type="checkbox" + <form + onSubmit={(e) => + state.modalType === "add-event" + ? addEvent(e, selectedDate.current) + : editEvent(e, selectedDate.current) + } + > + <ModalHeader> + <H2>{state.modalType === "add-event" ? "New" : "Edit"} Event</H2> + <Cancel + type="button" + aria-label="Cancel" + ref={lastTab} + onClick={() => closeModal()} + > + X + </Cancel> + </ModalHeader> + <Fieldset> + <div> + <Color + type="color" onChange={(e) => - setTempEvent({ ...tempEvent, allDay: e.target.checked }) + setTempEvent({ ...tempEvent, Color: e.target.value }) } - checked={tempEvent.allDay} + value={tempEvent.Color} + ref={firstTab} /> - All day - </label> - </div> - <div> - <span>🕒 </span> - <Input - type="time" - onChange={(e) => - setTempEvent({ ...tempEvent, timeFrom: e.target.value }) - } - placeholder="Event Time From" - value={tempEvent.timeFrom} - disabled={tempEvent.allDay === true} - required={tempEvent.allDay !== true} - /> - <span> to </span> - <Input - type="time" + <Title + onChange={(e) => + setTempEvent({ ...tempEvent, Title: e.target.value }) + } + placeholder="Event Title" + value={tempEvent.Title} + required + /> + <label> + <Checkbox + type="checkbox" + onChange={(e) => + setTempEvent({ ...tempEvent, AllDay: e.target.checked }) + } + checked={tempEvent.AllDay} + /> + All day + </label> + </div> + <div> + <span>🕒 </span> + <Input + type="time" + onChange={(e) => + setTempEvent({ ...tempEvent, TimeFrom: e.target.value }) + } + aria-label="Event Time From" + value={tempEvent.TimeFrom} + disabled={tempEvent.AllDay === true} + required={tempEvent.AllDay !== true} + /> + <span> to </span> + <Input + type="time" + onChange={(e) => + setTempEvent({ ...tempEvent, TimeTo: e.target.value }) + } + aria-label="Event Time To" + value={tempEvent.TimeTo} + disabled={tempEvent.AllDay === true} + required={tempEvent.AllDay !== true} + /> + </div> + <Description onChange={(e) => - setTempEvent({ ...tempEvent, timeTo: e.target.value }) + setTempEvent({ ...tempEvent, Description: e.target.value }) } - placeholder="Event Time To" - value={tempEvent.timeTo} - disabled={tempEvent.allDay === true} - required={tempEvent.allDay !== true} + placeholder="Event Description" + value={tempEvent.Description} /> - </div> - <Description - onChange={(e) => - setTempEvent({ ...tempEvent, description: e.target.value }) - } - placeholder="Event Description" - value={tempEvent.description} - /> - <Buttons> - {state.modalType === "edit-event" && ( - <Delete - type="button" - onClick={() => deleteEvent(selectedDate.current)} - > - Delete - </Delete> - )} - <ModalButton type="submit">Save</ModalButton> - </Buttons> - </Fieldset> - </form> - </Modal> - </> - ); + <Buttons> + {state.modalType === "edit-event" && ( + <Delete + type="button" + onClick={() => deleteEvent(selectedDate.current)} + > + Delete + </Delete> + )} + <ModalButton type="submit">Save</ModalButton> + </Buttons> + </Fieldset> + </form> + {error && <p>{error}</p>} + </Modal> + </> + ); + }; } export default App; diff --git a/src/App.test.js b/src/App.test.js index 9382b9a..4ba707a 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,8 +1,309 @@ -import { render, screen } from "@testing-library/react"; -import App from "./App"; +import { render, screen, waitFor, waitForElementToBeRemoved, within } from "@testing-library/react"; +import userEvent from '@testing-library/user-event'; +import App from './App'; + +// KNOWN ISSUE: Event expects current time, so test may occasionally fail if performed between when time ticks to next minute + +const now = new Date(); +const month = now.getMonth() + 1; // get month without zero index +const year = now.getFullYear(); +const hours = now.getHours(); +const minutes = now.getMinutes(); +const time = `${hours < 10 ? `0${hours}` : hours}:${minutes < 10? `0${minutes}` : minutes}`; // get time with trailing zeroes +const date = `${year}-${month < 10 ? `0${month}` : month}-01`; // first date of month string for week filter + +const mockEvent = { + EventID: `${date}_1`, + Title: "test", + TimeFrom: time, + TimeTo: time, + Description: "", + AllDay: false, + Color: "#0057ba", +}; + +const mockGetEvents = jest.fn(); +const mockAddEvent = jest.fn(); +const mockEditEvent = jest.fn(); +const mockDeleteEvent = jest.fn(); + +jest.mock('./api', () => ({ + getEvents: () => mockGetEvents.mockReturnValue([]), + addEvent: (item, date) => mockAddEvent(item, date), + editEvent: (item, id) => mockEditEvent(item, id), + deleteEvent: (id) => mockDeleteEvent(id), +})); + +const helper = async () => { + const eventButton = screen.getByText('1'); + userEvent.click(eventButton); + + expect(await screen.findByText('New Event')).toBeInTheDocument(); + + const input = screen.getByPlaceholderText('Event Title'); + const submitButton = screen.getByRole('button', { name: 'Save' }); + + userEvent.type(input, 'Test event'); + userEvent.click(submitButton); + + await waitFor(() => { + expect(mockAddEvent).toHaveBeenCalledWith({ + ...mockEvent, + Title: "Test event", + EventID: "" + }, date); + }); + + await waitForElementToBeRemoved(() => screen.queryByText(/New Event/i)); + + expect(screen.getByRole('button', { name: 'Edit test' })).toBeInTheDocument(); +}; + +beforeEach(() => { + window.localStorage.clear(); + mockAddEvent.mockReturnValue(mockEvent); +}); + +// -------------------------------------------------------------------------- + +describe('App', () => { + it('renders correctly', async () => { + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + }); + + it('should filter by week when clicking Week', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + // end of setup + + const weekButton = screen.getByText('Week'); + + userEvent.click(weekButton); + + expect(await screen.findAllByLabelText('1am')).toBeTruthy(); + }); + + it('should filter by year when clicking Year', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + // end of setup + + const yearButton = screen.getByText('Year'); + + userEvent.click(yearButton); + + expect(screen.getByLabelText(`January ${year}`)).toBeInTheDocument(); + }); + + it('should cycle months when clicking an arrow button', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + // end of setup + + const currentMonth = now.getMonth().toLocaleString('default', { month: 'long' }); + + expect(await screen.findByText(currentMonth)).toBeInTheDocument(); + + const testDate = new Date(); + testDate.setMonth(now.getMonth() - 1); + const previousMonth = testDate.toLocaleString('default', { month: 'long' }); + const cycleButton = screen.getByLabelText('Go to Previous Month'); + userEvent.click(cycleButton); + + expect(await screen.findByText(`${previousMonth} ${testDate.getFullYear()}`)).toBeInTheDocument(); + }); + + it('should display Modal when clicking on a Day', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + // end of setup + + const eventButton = screen.getByText('1'); + userEvent.click(eventButton); + + expect(await screen.findByText('New Event')).toBeInTheDocument(); + }); + + it('should add an Event when clicking Save on Modal', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + // end of setup + + // helper() + const eventButton = screen.getByText('1'); + userEvent.click(eventButton); + + expect(await screen.findByText('New Event')).toBeInTheDocument(); + + const input = screen.getByPlaceholderText('Event Title'); + const submitButton = screen.getByRole('button', { name: 'Save' }); + + userEvent.type(input, 'Test event'); + userEvent.click(submitButton); + + await waitFor(() => { + expect(mockAddEvent).toHaveBeenCalledWith({ + ...mockEvent, + Title: "Test event", + EventID: "" + }, date); + }); + + await waitForElementToBeRemoved(() => screen.queryByText(/New Event/i)); + + expect(screen.getByRole('button', { name: 'Edit test' })).toBeInTheDocument(); + }); + + it('should show Edit Modal when clicking on an Event', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + await helper(); + + // end of setup + + const editButton = screen.getByRole('button', { name: 'Edit test' }); + + userEvent.click(editButton); + + expect(await screen.findByText('Edit Event')).toBeInTheDocument(); + }); + + it('should save edits when clicking Save on Modal', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + await helper(); + + const editButton = screen.getByRole('button', { name: 'Edit test' }); + + userEvent.click(editButton); + + expect(await screen.findByText('Edit Event')).toBeInTheDocument(); + + // end of setup + + const input = screen.getByPlaceholderText('Event Title'); + const submitButton = screen.getByRole('button', { name: 'Save' }); + + userEvent.clear(input); + userEvent.type(input, 'test-new'); + userEvent.click(submitButton); + + expect(mockEditEvent).toHaveBeenCalledWith({...mockEvent, Title: 'test-new'}, mockEvent.EventID); + + await waitForElementToBeRemoved(() => screen.queryByText(/Edit Event/i)); + + expect(await screen.findByText('test-new')).toBeInTheDocument(); + }); + + it('should delete an Event when clicking Delete on Modal', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + await helper(); + + const editButton = screen.getByRole('button', { name: 'Edit test' }); + + userEvent.click(editButton); + + expect(await screen.findByText('Edit Event')).toBeInTheDocument(); + + // end of setup + + const deleteButton = screen.getByRole('button', { name: 'Delete' }); + + userEvent.click(deleteButton); + + expect(mockDeleteEvent).toHaveBeenCalledWith(mockEvent.EventID); + + await waitForElementToBeRemoved(() => screen.queryByText(/Edit Event/i)); + + expect(screen.queryByRole('button', { name: 'Edit test' })).not.toBeInTheDocument(); + }); +}); + +it('should display the correct month selected from Year filter', async () => { + // setup -test("renders learn react link", () => { render(<App />); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + const yearButton = screen.getByText('Year'); + + userEvent.click(yearButton); + + expect(screen.getByLabelText(`January ${year}`)).toBeInTheDocument(); + + // end of setup + + const monthButton = screen.getByRole('button', { name: `February ${year}` }); + userEvent.click(monthButton); + + expect(screen.getByText(`February ${year}`)).toBeInTheDocument(); }); + +it('should auto-fill correct time when clicking on hour in Hour filter', async () => { + // setup + + render(<App />); + + expect(await screen.findByText('Mon')).toBeInTheDocument(); + + const weekButton = screen.getByText('Week'); + + userEvent.click(weekButton); + + expect(await screen.findAllByLabelText('1am')).toBeTruthy(); + + // end of setup + + const dayString = now.toDateString(); + + expect(screen.getByLabelText(dayString)).toBeInTheDocument(); + + const parentDiv = screen.getByLabelText(dayString); + const hourDiv = within(parentDiv).getByLabelText("1am"); + const childButton = within(hourDiv).getByRole('button', { name: `30 to 60 minutes` }); + userEvent.click(childButton); + + expect(await screen.findByText('New Event')).toBeInTheDocument(); + + const timeFrom = screen.getByLabelText('Event Time From'); + const timeTo = screen.getByLabelText('Event Time To'); + + expect(timeFrom.value).toBe("01:30"); + expect(timeTo.value).toBe("02:00"); +}); \ No newline at end of file diff --git a/src/Day.js b/src/Day.js index 467e682..9f68886 100644 --- a/src/Day.js +++ b/src/Day.js @@ -20,29 +20,29 @@ export const Day = ({ date, isPadding, isCurrentDay, events, dayString, children return ( <ListItem ref={i === 0 ? firstTab : null} - key={`${origin}_${e.id}`} - color={e.color} + key={`${origin}_${e.EventID}`} + color={e.Color} $origin={origin} > <Event - id={`${e.id}`} + id={`${e.EventID}`} name="edit-event" - aria-label={`Edit ${e.title}`} + aria-label={`Edit ${e.Title}`} $origin={origin} > {origin === "list" && ( <Times> - {e.allDay ? ( + {e.AllDay ? ( <Time>All day</Time> ) : ( <> - <Time>{e.timeFrom}</Time> - <Time>{e.timeTo}</Time> + <Time>{e.TimeFrom}</Time> + <Time>{e.TimeTo}</Time> </> )} </Times> )} - {e.title} + {e.Title} </Event> </ListItem> ); diff --git a/src/Dropdown.js b/src/Dropdown.js index 680c910..16d37c6 100644 --- a/src/Dropdown.js +++ b/src/Dropdown.js @@ -16,31 +16,56 @@ export const Dropdown = () => { clearTimeout(timeout); }; - // media query values for which buttons should disappear and appear in the dropdown + // media query values for which buttons should disappear and appear in the dropdown (commented numbers are without sync) let todayPx = 493; - let weekPx = 576; - let monthPx = 683; - let yearPx = 683; + let weekPx = 605; // 576 + let monthPx = 690; // 683 + let yearPx = 767; // 683 + let syncPx = 787; return ( <> - <Outside mediaQ={todayPx} aria-label="Jump to Today" name="today"> + <Outside + mediaQ={todayPx} + aria-label="Jump to Today" + name="today" + > Today </Outside> - <Divider>|</Divider> - <Outside mediaQ={weekPx} aria-label="Filter by Week" name="week"> + <Divider aria-hidden={true}>|</Divider> + <Outside + mediaQ={weekPx} + aria-label="Filter by Week" + name="week" + > Week </Outside> - <Outside mediaQ={monthPx} aria-label="Filter by Month" name="month"> + <Outside + mediaQ={monthPx} + aria-label="Filter by Month" + name="month" + > Month </Outside> - <Outside mediaQ={yearPx} aria-label="Filter by Year" name="year"> + <Outside + mediaQ={yearPx} + aria-label="Filter by Year" + name="year" + > Year </Outside> + <Divider mediaQ={weekPx} aria-hidden={true}>|</Divider> + <Outside + mediaQ={syncPx} + aria-label="Filter by Year" + name="sync" + > + Sync + </Outside> {open && <Backdrop mediaQ={open} onClick={() => setOpen(false)} />} <Header onBlur={() => onBlurHandler()} onFocus={() => onFocusHandler()}> <Button - mediaQ={yearPx} + mediaQ={syncPx} onClick={() => setOpen(!open)} aria-label={!open ? `Open "All Filters"` : `Close "All Filters"`} > @@ -48,18 +73,41 @@ export const Dropdown = () => { </Button> {open && ( <List> - <Inside mediaQ={todayPx} aria-label="Jump to Today" name="today"> + <Inside + mediaQ={todayPx} + aria-label="Jump to Today" + name="today" + > Today </Inside> - <Inside mediaQ={weekPx} aria-label="Filter by Week" name="week"> + <Inside + mediaQ={weekPx} + aria-label="Filter by Week" + name="week" + > Week </Inside> - <Inside mediaQ={monthPx} aria-label="Filter by Month" name="month"> + <Inside + mediaQ={monthPx} + aria-label="Filter by Month" + name="month" + > Month </Inside> - <Inside mediaQ={yearPx} aria-label="Filter by Year" name="year"> + <Inside + mediaQ={yearPx} + aria-label="Filter by Year" + name="year" + > Year </Inside> + <Inside + mediaQ={syncPx} + aria-label="Filter by Year" + name="sync" + > + Sync + </Inside> </List> )} </Header> @@ -80,11 +128,11 @@ const Outside = styled.button` &:hover { background-color: lightgrey; - } + }; @media only screen and (max-width: ${(props) => props.mediaQ}px) { display: none; - } ; + }; `; const Button = styled.button` @@ -103,16 +151,20 @@ const Button = styled.button` &:hover, &:active { background-color: lightgrey; - } + }; @media only screen and (max-width: ${(props) => props.mediaQ}px) { display: block; - } ; + }; `; const Divider = styled.span` font-size: 1.5em; margin: 0em 0.5em; + + @media only screen and (max-width: ${(props) => props.mediaQ}px) { + display: none; + }; `; const List = styled.div` @@ -136,7 +188,7 @@ const Inside = styled(Outside)` @media only screen and (max-width: ${(props) => props.mediaQ}px) { display: block; - } ; + }; `; const Backdrop = styled.div` diff --git a/src/Events.js b/src/Events.js deleted file mode 100644 index 9566be1..0000000 --- a/src/Events.js +++ /dev/null @@ -1,42 +0,0 @@ -import { v4 as uuidv4 } from "uuid"; - -// SAVING A NEW EVENT - -export const addEvent = (item, id) => { - const events = JSON.parse(localStorage.getItem("events")); - - const date = id.split("_")[0]; - const dateEvents = events?.find((e) => e.date === date)?.events; // grab date's events - - if (!dateEvents) { - // if date doesn't exist, add new event and list explicitly - return { date, events: [{ ...item, id: `${date}_${uuidv4()}` }] }; - } - - return { - date, - events: [...dateEvents, { ...item, id: `${date}_${uuidv4()}` }], - }; -}; - -// SAVING AN EDITED EVENT - -export const editEvent = (item, id) => { - const events = JSON.parse(localStorage.getItem("events")); - - const date = id.split("_")[0]; - const dateEvents = events.find((e) => e.date === date).events; // grab events of the day - const eventIndex = dateEvents.findIndex((e) => e.id === id); // grab index of event - - dateEvents[eventIndex] = { ...item, id }; // replace event with new/updated one - - return { date: date, events: dateEvents }; -}; - -// DELETING AN EVENT - -export const deleteEvent = (date, id) => { - const events = JSON.parse(localStorage.getItem("events")); - - return events.find((e) => e.date === date).events.filter((e) => e.id !== id); -}; diff --git a/src/Generator.js b/src/Generator.js index 1eb0c84..f5c3797 100644 --- a/src/Generator.js +++ b/src/Generator.js @@ -38,8 +38,12 @@ export const dayGenerator = (day, month, year, paddingDays, prevMonthLength, eve day = day + 1 - paddingDays; // remove the extra/padding days to count from 1+ }; - const dateString = `${day}-${month}-${year}`; // used as id, to track what day/month/year the button is on click - const eventsForDay = events?.find((event) => event.date === dateString); // find/grab all events for the current day + // add zeroes to the beginning of numbers for database specificity and week display + let zeroDay = day < 10 ? `0${day}` : day; + let zeroMonth = month < 10 ? `0${month}` : month; + + const dateString = `${year}-${zeroMonth}-${zeroDay}`; // used as id, to track what day/month/year the button is on click + const eventsForDay = events?.filter((e) => e.EventID.includes(dateString)); // find/grab all events for the current day const isCurrentDay = `${day}/${month}/${year}` === currentDay ? true : false; const dayString = new Date(year, month - 1, day).toDateString(); // used for aria-label and title on events list @@ -48,7 +52,7 @@ export const dayGenerator = (day, month, year, paddingDays, prevMonthLength, eve isCurrentDay, isPadding, date: dateString, - events: eventsForDay?.events, + events: eventsForDay, dayString, }; }; @@ -71,21 +75,25 @@ export const hourGenerator = (day, month, year, prevMonthLength, monthLength, ev month = month === 12 ? 0 : month + 1; // if current month is December, set the next month's days to January, otherwise count up one }; - let actualDay = day; // preserve original day (before day/month display edits) for dateString + // add zeroes to the beginning of numbers for database specificity and week display + let zeroDay = day < 10 ? `0${day}` : day; + let zeroMonth = month < 10 ? `0${month}` : month; if (day === 1) { // display first day with month number to help avoid confusion, append 0 under 10 for consistency - day = `01/${month < 10 ? `0` : ``}${month === 0 ? month + 1 : month}`; + day = `01/${zeroMonth === 0 ? zeroMonth + 1 : zeroMonth}`; }; - const dateString = `${actualDay}-${month}-${year}`; // used as id, to track what day/month/year the button is on click - const eventsForDay = events?.find((event) => event.date === dateString); // find/grab all events for the current day + const dateString = `${year}-${zeroMonth}-${zeroDay}`; // used as id, to track what day/month/year the button is on click + const eventsForDay = events?.filter((e) => e.EventID.includes(dateString)); // find/grab all events for the current day const isCurrentDay = `${day}/${month}/${year}` === currentDay ? true : false; + const dayString = new Date(year, month - 1, day).toDateString(); // used for aria-label and title on events list return { day, isCurrentDay, date: dateString, - events: eventsForDay?.events, + events: eventsForDay, + dayString }; }; diff --git a/src/Hour.js b/src/Hour.js index 4e8baf2..59095ec 100644 --- a/src/Hour.js +++ b/src/Hour.js @@ -2,16 +2,34 @@ import React from "react"; import styled from "styled-components"; export const Hour = ({ hour, index, date, isPadding, isCurrentDay }) => { + + const aria = () => { + if (index === 0 || index === 12) { + return `12${index === 12 ? 'pm' : 'am'}`; + } else { + return `${index < 12 ? `${index}am` : `${index - 12}pm`}`; + }; + }; + return ( <HourContainer isPadding={isPadding} isCurrentDay={isCurrentDay} hour={hour} + aria-label={aria()} > - <StyledHour id={`${date}_${index}_1`} name="add-event"> + <StyledHour + id={`${date}_${index}_1`} + name="add-event" + aria-label="0 to 30 minutes" + > {""} </StyledHour> - <StyledHour2 id={`${date}_${index}_2`} name="add-event"> + <StyledHour2 + id={`${date}_${index}_2`} + name="add-event" + aria-label="30 to 60 minutes" + > {""} </StyledHour2> </HourContainer> diff --git a/src/Modal.js b/src/Modal.js index c5a4681..1e7f794 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -1,15 +1,9 @@ -import React, { - useState, - useEffect, - forwardRef, - useImperativeHandle, -} from "react"; +import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react"; import styled from "styled-components"; import { Portal } from "./Portal"; -export const Modal = forwardRef( - ({ cache, firstTab, lastTab, zIndex = 3, children }, ref) => { +export const Modal = forwardRef(({ cache, firstTab, lastTab, zIndex = 3, children }, ref) => { const [display, setDisplay] = useState(false); useImperativeHandle(ref, () => { diff --git a/src/Portal.js b/src/Portal.js index 1b549de..4b276eb 100644 --- a/src/Portal.js +++ b/src/Portal.js @@ -1,8 +1,12 @@ import React, { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; -const portalRoot = - typeof document !== `undefined` ? document.getElementById("portal") : null; +let portalRoot = document.getElementById("portal"); +if (!portalRoot) { + portalRoot = document.createElement('div'); + portalRoot.setAttribute('id', 'portal'); + document.body.appendChild(portalRoot); +}; export const Portal = ({ children }) => { const elRef = useRef(null); diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000..23961bf --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,52 @@ +import { v4 as uuidv4 } from "uuid"; + +const uri = `${process.env.REACT_APP_INVOKE_URL}/events`; +const user = process.env.REACT_APP_USER; + +// GETTING ALL EVENTS + +export const getEvents = async () => { + const res = await fetch(`${uri}/${user}/`); + return await res.json(); +}; + +// SAVING A NEW EVENT + +export const addEvent = async (item, date) => { + const id = `${date.split("_")[0]}_${uuidv4()}`; + + const event = { ...item, EventID: id }; + + await fetch(`${uri}/${user}/${id}`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ Username: user, ...event }) + }) + .then(response => response.json()); + + return event; +}; + +// SAVING AN EDITED EVENT + +export const editEvent = async (item, id) => { + await fetch(`${uri}/${user}/${id}`, { + method: 'PATCH', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ Username: user, ...item }) + }); +}; + +// DELETING AN EVENT + +export const deleteEvent = async (id) => { + await fetch(`${uri}/${user}/${id}`, { + method: 'DELETE' + }); +}; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 4a1df4d..0000000 --- a/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", - monospace; -} diff --git a/src/index.js b/src/index.js index 770ee7d..8524d86 100644 --- a/src/index.js +++ b/src/index.js @@ -1,14 +1,11 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - <React.StrictMode> - <App /> - </React.StrictMode> + <App /> ); // If you want to start measuring performance in your app, pass a function diff --git a/src/logo.svg b/src/logo.svg deleted file mode 100644 index 9dfc1c0..0000000 --- a/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> \ No newline at end of file