diff --git a/package.json b/package.json
index 9d0d6d3ce2..50646899a1 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,11 @@
"private": true,
"dependencies": {
"@giscus/react": "^2.0.3",
+ "@types/lodash": "^4.14.182",
"@types/react": "^18.0.6",
"@types/react-dom": "^18.0.2",
+ "date-fns": "^2.28.0",
+ "lodash": "^4.17.21",
"node-sass": "^7.0.1",
"plop": "^3.0.5",
"react": "^18.0.0",
@@ -51,8 +54,8 @@
]
},
"devDependencies": {
- "typescript": "^4.6.4",
+ "puppeteer": "^13.7.0",
"react-snap": "^1.23.0",
- "puppeteer": "^13.7.0"
+ "typescript": "^4.6.4"
}
}
diff --git a/src/meta/play-meta.js b/src/meta/play-meta.js
index 4f87daadc0..0712a90fda 100644
--- a/src/meta/play-meta.js
+++ b/src/meta/play-meta.js
@@ -16,6 +16,7 @@ QuoteGenerator,
PasswordGenerator,
WhyTypescript,
NetlifyCardGame,
+Calendar,
FunQuiz,
//import play here
} from "plays";
@@ -242,6 +243,19 @@ export const plays = [
language: 'js',
featured: true,
}, {
+ id: 'pl-calendar',
+ name: 'Calendar',
+ description: 'Simple calendar app to manage events',
+ component: () => {return },
+ path: '/plays/calendar',
+ level: 'Intermediate',
+ tags: 'JSX,Hooks,Typescript',
+ github: 'vincentBCP',
+ cover: '',
+ blog: '',
+ video: '',
+ language: 'ts'
+ }, {
id: 'pl-fun-quiz',
name: 'Fun Quiz',
description: 'Its a Fun Quiz app which lets player to choose desirable category to answer 20 unique question with 4 options and pick the correct one.',
diff --git a/src/plays/calendar/Calendar.scss b/src/plays/calendar/Calendar.scss
new file mode 100644
index 0000000000..f5c574c686
--- /dev/null
+++ b/src/plays/calendar/Calendar.scss
@@ -0,0 +1,373 @@
+$calendar-play-box-shadow-dark:rgba(0,0,0,0.1);
+$calendar-play-box-shadow: 0px 0px 10px 5px $calendar-play-box-shadow-dark;
+
+$calendar-play-blue-500: #3182CE;
+$calendar-play-blue-400: #4299E1;
+$calendar-play-blue-300: #63B3ED;
+
+$calendar-play-red-500: #E53E3E;
+$calendar-play-red-200: #FEB2B2;
+$calendar-play-red-100: #FED7D7;
+
+.calendar-play {
+ background-color: white;
+ padding: 30px;
+}
+
+.calendar-play-navigation {
+ display: flex;
+ align-items: center;
+ margin-bottom: 30px;
+ user-select: none;
+
+ button {
+ background-color: transparent;
+ border: 1px solid lightgray;
+ border-radius: 5px;
+ font-size: 14px;
+ padding: 10px 15px;
+ font-weight: 500;
+ margin-right: 20px;
+ }
+
+ .calendar-play-navigation-arrow {
+ width: 35px;
+ height: 35px;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 50%;
+ font-size: 28px;
+ cursor: pointer;
+ padding-top: 3px;
+ }
+
+ .calendar-play-navigation-current-date {
+ font-size: 24px;
+ margin-left: 20px;
+ }
+
+ button,
+ .calendar-play-navigation-arrow {
+ &:hover {
+ background-color: rgba(0,0,0,0.05);
+ }
+
+ &:active {
+ background-color: darkgray;
+ }
+ }
+}
+
+.calendar-play-body {
+ display: grid;
+ grid-template-columns: repeat(7, minmax(0, 1fr));
+ border-left: 1px solid lightgray;
+ border-bottom: 1px solid lightgray;
+}
+
+.calendar-play-day-tile {
+ height: 150px;
+ border-right: 1px solid lightgray;
+ border-top: 1px solid lightgray;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding-top: 5px;
+
+ .calendar-play-week {
+ font-size: 12px;
+ }
+
+ .calendar-play-day {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 25px;
+ width: 100%;
+ font-size: 12px;
+ }
+
+ .calendar-play-week {
+ text-transform: uppercase;
+ font-weight: 500;
+ color: gray;
+ }
+
+ &.today {
+ .calendar-play-day {
+ width: 25px;
+ font-weight: 700;
+ color: white;
+ background-color: $calendar-play-blue-500;
+ border-radius: 50%;
+ }
+ }
+}
+
+.calendar-play-modal {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ padding-top: 120px;
+ z-index: 999;
+ background-color: rgba(0,0,0,0.5);
+
+ .calendar-play-modal-content {
+ width: 500px;
+ margin: auto;
+ background-color: white;
+ padding: 30px 30px 30px 30px;
+ border-radius: 5px;
+ box-shadow: $calendar-play-box-shadow;
+
+ & > div:first-of-type {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20px;
+
+ span:last-of-type {
+ cursor: pointer;
+ margin-left: auto;
+ }
+ }
+
+ @media (max-width: 768px) {
+ max-width: 90%;
+ }
+ }
+}
+
+.calendar-play-event-form {
+ input[name='title'] {
+ width: 100%;
+ border: none;
+ border-bottom: 1px solid lightgray;
+ font-size: 18px;
+ outline: none;
+ padding-bottom: 5px;
+ margin-bottom: 20px;
+ }
+
+ p {
+ font-size: 14px;
+ color: gray;
+ }
+
+ & > div:first-of-type {
+ display: flex;
+ gap: 30px;
+
+ div {
+ width: 50%;
+
+ label {
+ display: block;
+ color: gray;
+ font-size: 14px;
+ }
+
+ input {
+ width: 100%;
+ outline: none;
+ border: none;
+ border-bottom: 1px solid lightgray;
+ }
+ }
+ }
+
+ & > div:last-of-type {
+ display: flex;
+ justify-content: flex-end;
+ gap: 15px;
+ margin-top: 40px;
+
+ button {
+ min-width: 80px;
+ padding: 10px;
+ border: none;
+ outline: none;
+ border-radius: 5px;
+ font-weight: 500;
+ transition-duration: 0.3s;
+ border: 1px solid transparent;
+
+ &.delete {
+ background-color: transparent;
+ margin-right: auto;
+ color: $calendar-play-red-500;
+
+ &:hover {
+ border: 1px solid $calendar-play-red-500;
+ }
+
+ &:active {
+ background-color: $calendar-play-red-100;
+ }
+ }
+
+ &.close {
+ background-color: transparent;
+
+ &:hover {
+ background-color: rgba(0,0,0,0.05);
+ }
+
+ &:active {
+ background-color: rgba(0,0,0,0.2);
+ }
+ }
+
+ &.save {
+ color: white;
+ background-color: $calendar-play-blue-500;
+
+ &:hover {
+ background-color: $calendar-play-blue-400;
+ }
+
+ &:active {
+ background-color: $calendar-play-blue-300;
+ }
+ }
+ }
+ }
+}
+
+.calendar-play-event-info {
+ p:first-of-type {
+ font-size: 18px;
+ }
+
+ div {
+ text-align: right;
+
+ button {
+ min-width: 80px;
+ padding: 10px;
+ border: none;
+ outline: none;
+ border-radius: 5px;
+ font-weight: 500;
+ transition-duration: 0.3s;
+
+ &:hover {
+ background-color: rgba(0,0,0,0.1);
+ }
+
+ &:active {
+ background-color: rgba(0,0,0,0.2);
+ }
+ }
+ }
+}
+
+.calendar-play-events {
+ margin-top: 5px;
+ width: 100%;
+}
+
+.calendar-play-events-more {
+ padding: 0 10px;
+ display: block;
+ cursor: pointer;
+ position: relative;
+
+ &:hover {
+ background-color: rgba(0,0,0,0.05);
+ }
+
+ & > span {
+ font-weight: 600;
+ font-size: 12px;
+ white-space: nowrap;
+ }
+
+ .calendar-play-events-more-popup {
+ display: none;
+ position: absolute;
+ left: -200px;
+ top: -200%;
+ width: 200px;
+ background-color: white;
+ padding: 20px 10px;
+ border-radius: 5px;
+ box-shadow: $calendar-play-box-shadow;
+ overflow: auto;
+ max-height: 200px;
+
+ & > div:first-of-type {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 10px;
+
+ span {
+ &:first-of-type {
+ font-size: 14px;
+ color: gray;
+ text-transform: uppercase;
+ }
+
+ &:last-of-type {
+ font-size: 20px;
+ }
+ }
+ }
+ }
+
+ &:hover {
+ .calendar-play-events-more-popup {
+ display: block;
+ }
+ }
+
+ @media (max-width: 425px) {
+ span {
+ font-size: 10px;
+ }
+ }
+}
+
+.calendar-play-event {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ overflow: hidden;
+ cursor: pointer;
+ padding: 3px 10px;
+ transition-duration: 0.3s;
+
+ &:hover {
+ background-color: rgba(0,0,0,0.05);
+ }
+
+ div {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: $calendar-play-blue-500;
+ flex-shrink: 0;
+ }
+
+ span {
+ font-size: 12px;
+ white-space: nowrap;
+
+ &:last-of-type {
+ font-weight: 500;
+ flex-grow: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ @media (max-width: 425px) {
+ span {
+ font-size: 10px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/plays/calendar/Calendar.tsx b/src/plays/calendar/Calendar.tsx
new file mode 100644
index 0000000000..d2e2039467
--- /dev/null
+++ b/src/plays/calendar/Calendar.tsx
@@ -0,0 +1,34 @@
+import * as React from 'react';
+import { getPlayById } from 'meta/play-meta-util';
+
+import PlayHeader from 'common/playlists/PlayHeader';
+import CalendarGrid from './CalendarGrid'
+import { ContextProvider } from './Context';
+
+import './Calendar.scss'
+import ModalContainer from './ModalContainer';
+
+function Calendar(props:any) {
+ // Do not remove the below lines.
+ // The following code is to fetch the current play from the URL
+ const { id } = props;
+ const play = getPlayById(id);
+
+ // Your Code Start below.
+
+ return (
+ <>
+
+ >
+ );
+}
+
+export default Calendar;
\ No newline at end of file
diff --git a/src/plays/calendar/CalendarDayTile.tsx b/src/plays/calendar/CalendarDayTile.tsx
new file mode 100644
index 0000000000..d836f86229
--- /dev/null
+++ b/src/plays/calendar/CalendarDayTile.tsx
@@ -0,0 +1,43 @@
+import React, { useContext } from 'react'
+import { format } from 'date-fns'
+import CalendarEvents from './CalendarEvents'
+import CalendarEventForm from './CalendarEventForm'
+import { Context } from './Context'
+
+interface Props {
+ date: Date,
+ showWeek?: boolean,
+ isToday?: boolean
+}
+
+const CalendarDayTile = ({ date, showWeek, isToday }: Props) => {
+ const context = useContext(Context)
+ const { showModal, hideModal } = context
+
+ const handleClick = () => {
+ showModal(
+ ,
+ format(date, 'ccc, MMMM dd')
+ )
+ }
+
+ return (
+
+ {showWeek && (
+ {format(date, "EEE")}
+ )}
+
+ {format(date, date.getDate() === 1 && !isToday ? "MMM d" : "d")}
+
+
+
+ );
+}
+
+export default CalendarDayTile
\ No newline at end of file
diff --git a/src/plays/calendar/CalendarEvent.tsx b/src/plays/calendar/CalendarEvent.tsx
new file mode 100644
index 0000000000..2a5ed36b4f
--- /dev/null
+++ b/src/plays/calendar/CalendarEvent.tsx
@@ -0,0 +1,38 @@
+import React, { useContext } from 'react'
+import { format } from 'date-fns'
+import CalendarEventForm from './CalendarEventForm'
+import { Context } from './Context'
+import EventType from './EventType'
+
+interface Props {
+ event: EventType
+}
+
+const CalendarEvent = ({ event }: Props) => {
+ const context = useContext(Context)
+ const { showModal, hideModal } = context
+
+ const handleClick = () => {
+ showModal(
+ ,
+ format(new Date(event.date), 'ccc, MMMM dd')
+ )
+ }
+
+ return (
+
+
+
{format(new Date(`1990-01-01 ${event.startTime}`), 'h:mm aaa')}
+
{event.title}
+
+ )
+}
+
+export default CalendarEvent
\ No newline at end of file
diff --git a/src/plays/calendar/CalendarEventForm.tsx b/src/plays/calendar/CalendarEventForm.tsx
new file mode 100644
index 0000000000..233ee7fc6e
--- /dev/null
+++ b/src/plays/calendar/CalendarEventForm.tsx
@@ -0,0 +1,154 @@
+import { isBefore } from 'date-fns'
+import { format } from 'date-fns/esm'
+import { isEqual } from 'lodash'
+import React, { useState, useContext, useEffect } from 'react'
+import CalendarEventInfo from './CalendarEventInfo'
+import { Context } from './Context'
+import EventType from './EventType'
+
+interface Props {
+ date: Date,
+ event?: EventType,
+ onCancel: VoidFunction
+}
+
+const CalendarEventForm = ({ date, event, onCancel }: Props) => {
+ const [calendarEvent, setCalendarEvent] = useState()
+ const [data, setData] = useState()
+ const [editable, setEditable] = useState(false)
+ const context = useContext(Context)
+ const { addEvent, updateEvent, deleteEvent } = context
+
+ useEffect(() => {
+ if (event) {
+ setData({...event})
+ setCalendarEvent({...event})
+ setEditable(false)
+ return
+ }
+
+ setData({
+ date: format(date, 'yyyy-MM-dd'),
+ title: '',
+ startTime: '',
+ endTime: ''
+ })
+ setEditable(true)
+ }, [date, event])
+
+ const handleSave = () => {
+ if (!data.title) {
+ alert('Please provide title')
+ return
+ }
+
+ if (!data.startTime) {
+ alert('Please provide start time')
+ return
+ }
+
+ if (!data.endTime) {
+ alert('Please provide end time')
+ return
+ }
+
+ const start = new Date(`1990-01-01 ${data.startTime}`)
+ const end = new Date(`1990-01-01 ${data.endTime}`)
+ if (isEqual(start, end) || isBefore(end, start)) {
+ alert('Invalid time values')
+ return
+ }
+
+ if (event) {
+ updateEvent(data)
+ setCalendarEvent({...data})
+ setEditable(false)
+ return
+ }
+
+ addEvent(data)
+ onCancel()
+ }
+
+ const handleDelete = () => {
+ deleteEvent(event)
+ onCancel()
+ }
+
+ const handleCancel = () => {
+ if (event) {
+ setEditable(false)
+ return
+ }
+
+ onCancel()
+ }
+
+ const handleEdit = () => {
+ setData({...calendarEvent})
+ setEditable(true)
+ }
+
+ if (!data) return null
+
+ if (calendarEvent && !editable) {
+ return (
+
+ )
+ }
+
+ return (
+ ev.stopPropagation()}
+ >
+
setData({ ...data, title: ev.target.value })}
+ />
+
+
+ {Boolean(event) && (
+
+ )}
+
+
+
+
+ )
+}
+
+export default CalendarEventForm
\ No newline at end of file
diff --git a/src/plays/calendar/CalendarEventInfo.tsx b/src/plays/calendar/CalendarEventInfo.tsx
new file mode 100644
index 0000000000..0301cd6a97
--- /dev/null
+++ b/src/plays/calendar/CalendarEventInfo.tsx
@@ -0,0 +1,24 @@
+import { format } from 'date-fns'
+import React from 'react'
+import EventType from './EventType'
+
+interface Props {
+ event: EventType,
+ onEdit: VoidFunction
+}
+
+const CalendarEventInfo = ({ event, onEdit }: Props) => {
+ return (
+
+
{event.title}
+
{format(new Date(`1990-01-01 ${event.startTime}`), 'h:mm a')} - {format(new Date(`1990-01-01 ${event.endTime}`), 'h:mm a')}
+
+
+
+
+ )
+}
+
+export default CalendarEventInfo
\ No newline at end of file
diff --git a/src/plays/calendar/CalendarEvents.tsx b/src/plays/calendar/CalendarEvents.tsx
new file mode 100644
index 0000000000..826fa67d1c
--- /dev/null
+++ b/src/plays/calendar/CalendarEvents.tsx
@@ -0,0 +1,41 @@
+import React, { useState, useContext, useEffect } from 'react'
+import CalendarEvent from './CalendarEvent'
+import CalendarEventsMore from './CalendarEventsMore'
+import { Context } from './Context'
+import EventType from './EventType'
+
+interface Props {
+ date: Date
+}
+
+const MAX_VISIBLE_ITEMS = 3
+
+const CalendarEvents = ({ date }: Props) => {
+ const [events, setEvents] = useState([])
+ const context = useContext(Context)
+ const { getEvents } = context
+
+ useEffect(() => {
+ setEvents(getEvents(date))
+ }, [date, getEvents])
+
+ return (
+ ev.stopPropagation()}>
+ {events
+ .filter((e, index) => index < MAX_VISIBLE_ITEMS)
+ .map(event => (
+
+ ))}
+ {events.length > MAX_VISIBLE_ITEMS && (
+ index >= MAX_VISIBLE_ITEMS
+ )}
+ />
+ )}
+
+ );
+}
+
+export default CalendarEvents
\ No newline at end of file
diff --git a/src/plays/calendar/CalendarEventsMore.tsx b/src/plays/calendar/CalendarEventsMore.tsx
new file mode 100644
index 0000000000..f92dbb78ab
--- /dev/null
+++ b/src/plays/calendar/CalendarEventsMore.tsx
@@ -0,0 +1,30 @@
+import React from 'react'
+import { format } from 'date-fns'
+import CalendarEvent from './CalendarEvent'
+import EventType from './EventType'
+
+interface Props {
+ date: Date,
+ events: EventType[]
+}
+
+const CalendarEventsMore = ({ date, events }: Props) => {
+ return (
+
+
{events.length} more
+
+
+ {format(date, "ccc")}
+ {format(date, "dd")}
+
+
+ {events.map(event => (
+
+ ))}
+
+
+
+ );
+}
+
+export default CalendarEventsMore
\ No newline at end of file
diff --git a/src/plays/calendar/CalendarGrid.tsx b/src/plays/calendar/CalendarGrid.tsx
new file mode 100644
index 0000000000..5acfab9195
--- /dev/null
+++ b/src/plays/calendar/CalendarGrid.tsx
@@ -0,0 +1,56 @@
+import React, { useState } from 'react'
+import CalendarNavigation from './CalendarNavigation'
+import CalendarDayTile from './CalendarDayTile'
+import { endOfMonth, format, startOfMonth, startOfWeek } from 'date-fns'
+import { addDays, endOfWeek } from 'date-fns/esm'
+
+const WEEK_STARTS_ON = 0 // Sunday
+
+const CalendarGrid = () => {
+ const [currentDate, setCurrentDate] = useState(new Date())
+
+ const generateTiles = () => {
+ const startDate = startOfWeek(startOfMonth(currentDate), { weekStartsOn: WEEK_STARTS_ON })
+ const endDate = endOfWeek(endOfMonth(currentDate), { weekStartsOn: WEEK_STARTS_ON })
+
+ let curDate = startDate
+
+ const tiles: React.ReactNode[] = []
+
+ do {
+ tiles.push(
+
+ )
+
+ curDate = addDays(curDate, 1)
+ } while(format(curDate, 'yyyy-MM-dd') !== format(endDate, 'yyyy-MM-dd'))
+
+ tiles.push(
+
+ )
+
+ return tiles
+ }
+
+ return (
+
+
setCurrentDate(date)}
+ />
+
+ { generateTiles() }
+
+
+ )
+}
+
+export default CalendarGrid
\ No newline at end of file
diff --git a/src/plays/calendar/CalendarNavigation.tsx b/src/plays/calendar/CalendarNavigation.tsx
new file mode 100644
index 0000000000..06a60dea44
--- /dev/null
+++ b/src/plays/calendar/CalendarNavigation.tsx
@@ -0,0 +1,49 @@
+import { addMonths, format } from 'date-fns'
+import React from 'react'
+
+interface Props {
+ currentDate: Date,
+ onDateChange: (date: Date) => void
+}
+
+const CalendarNavigation = ({ currentDate, onDateChange }: Props) => {
+ const navigateTo = (direction: number) => {
+ /*
+ -1 = prev
+ 0 = today
+ 1 = next
+ */
+
+ if (direction === 0) {
+ onDateChange(new Date())
+ return
+ }
+
+ onDateChange(addMonths(currentDate, direction))
+ }
+
+ return (
+
+
+ navigateTo(-1)}
+ >
+ <
+
+ navigateTo(1)}
+ >
+ >
+
+
+ {format(currentDate, 'MMMM yyyy')}
+
+
+ )
+}
+
+export default CalendarNavigation
\ No newline at end of file
diff --git a/src/plays/calendar/Context.tsx b/src/plays/calendar/Context.tsx
new file mode 100644
index 0000000000..d765771600
--- /dev/null
+++ b/src/plays/calendar/Context.tsx
@@ -0,0 +1,85 @@
+import { format } from "date-fns";
+import React, { useCallback, useState } from "react";
+import { orderBy } from 'lodash'
+import { getDummyEvents } from "./utils";
+import EventType from "./EventType";
+
+export const Context = React.createContext({
+ modalTitle: '',
+ modalContent: undefined,
+ getEvents: (date: Date) => {},
+ addEvent: (event: EventType) => {},
+ updateEvent: (event: EventType) => {},
+ deleteEvent: (event: EventType) => {},
+ showModal: (content: React.ReactNode, title?: '') => {},
+ hideModal: () => {}
+})
+
+export const ContextProvider = ({ children }: any) => {
+ const [events, setEvents] = useState(getDummyEvents())
+ const [modalTitle, setModalTitle] = useState('')
+ const [modalContent, setModalContent] = useState(undefined)
+
+ const getEvents = useCallback((date: Date) => {
+ return orderBy(events.filter(e => e.date === format(date, 'yyyy-MM-dd')), ['startTime'])
+ }, [events])
+
+ const addEvent = (event: EventType) => {
+ event.id = (new Date()).getTime().toString()
+ setEvents(oldValue => ([...oldValue, event]))
+ }
+
+ const updateEvent = (event: EventType) => {
+ setEvents(oldValue => {
+ const newEvents = [...oldValue]
+ const index = newEvents.findIndex(e => e.id === event.id)
+
+ if (index === -1) return newEvents
+
+ newEvents[index] = event
+
+ return newEvents
+ })
+ }
+
+ const deleteEvent = (event: EventType) => {
+ setEvents(oldValue => {
+ const newEvents = [...oldValue]
+ const index = newEvents.findIndex(e => e.id === event.id)
+
+ if (index === -1) return newEvents
+
+ newEvents.splice(index, 1)
+
+ return newEvents
+ })
+ }
+
+ const showModal = (content: React.ReactNode, title?: '') => {
+ setModalTitle(title || '')
+ setModalContent(content)
+ }
+
+ const hideModal = () => {
+ setModalTitle('')
+ setModalContent(undefined)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
diff --git a/src/plays/calendar/EventType.ts b/src/plays/calendar/EventType.ts
new file mode 100644
index 0000000000..9c81d2e4ba
--- /dev/null
+++ b/src/plays/calendar/EventType.ts
@@ -0,0 +1,9 @@
+type EventType = {
+ id: string,
+ date: string,
+ title: string,
+ startTime: string,
+ endTime: string
+}
+
+export default EventType
\ No newline at end of file
diff --git a/src/plays/calendar/ModalContainer.tsx b/src/plays/calendar/ModalContainer.tsx
new file mode 100644
index 0000000000..e8e3b033ea
--- /dev/null
+++ b/src/plays/calendar/ModalContainer.tsx
@@ -0,0 +1,28 @@
+import React, { useContext } from 'react'
+import { Context } from './Context'
+
+const ModalContainer = () => {
+ const context = useContext(Context)
+ const { modalContent, modalTitle, hideModal } = context
+
+ if (!modalContent) return null
+
+ return (
+
+
ev.stopPropagation()}
+ >
+
+ {Boolean(modalTitle) && (
+ {modalTitle}
+ )}
+ ✕
+
+ {modalContent}
+
+
+ );
+}
+
+export default ModalContainer
\ No newline at end of file
diff --git a/src/plays/calendar/Readme.md b/src/plays/calendar/Readme.md
new file mode 100644
index 0000000000..97561fba67
--- /dev/null
+++ b/src/plays/calendar/Readme.md
@@ -0,0 +1,12 @@
+# Calendar
+
+A simple calendar to add, update, and delete events.
+
+## What will you learn
+
+- How to use `useState`, and `useContext` to manage component state.
+- How to use `useEffect`, and `useCallback` to implement component behaviour.
+- How to use `Context` to manage application state.
+- How to use `date-fns` to manipulate date using its cool functions.
+- How to pass `props` from parent component to child component.
+- How to slice your app into smaller components for re-usability and easier management.
\ No newline at end of file
diff --git a/src/plays/calendar/cover.png b/src/plays/calendar/cover.png
new file mode 100644
index 0000000000..ff9f846272
Binary files /dev/null and b/src/plays/calendar/cover.png differ
diff --git a/src/plays/calendar/utils.tsx b/src/plays/calendar/utils.tsx
new file mode 100644
index 0000000000..26c12f8bae
--- /dev/null
+++ b/src/plays/calendar/utils.tsx
@@ -0,0 +1,85 @@
+import {
+ addDays,
+ endOfMonth,
+ endOfWeek,
+ format,
+ getWeekOfMonth,
+ isWeekend,
+ startOfMonth,
+ startOfWeek,
+} from "date-fns";
+import EventType from "./EventType";
+
+export const getDummyEvents = (): EventType[] => {
+ // create fake events
+
+ const startDate = startOfWeek(startOfMonth(new Date()), { weekStartsOn: 0 });
+ const endDate = endOfWeek(endOfMonth(new Date()), { weekStartsOn: 0 });
+
+ let curDate = startDate;
+
+ let events: EventType[] = [];
+
+ const addEvent = (date: Date) => {
+ // add event only if date is Mon - Fri
+ if (isWeekend(date)) return;
+
+ events.push({
+ id: format(date, "yyyy-MM-dd"),
+ date: format(date, "yyyy-MM-dd"),
+ title: "Daily stand up",
+ startTime: "09:30",
+ endTime: "10:00",
+ });
+
+ if (getWeekOfMonth(date) % 2 === 0 && date.getDay() === 2) {
+ events.push({
+ id: `${format(date, "yyyy-MM-dd")}-key`,
+ date: format(date, "yyyy-MM-dd"),
+ title: "Sprint Planning",
+ startTime: "15:00",
+ endTime: "17:00",
+ });
+ }
+ };
+
+ do {
+ addEvent(curDate);
+ curDate = addDays(curDate, 1);
+ } while (format(curDate, "yyyy-MM-dd") !== format(endDate, "yyyy-MM-dd"));
+
+ addEvent(curDate);
+
+ events = events.concat([
+ {
+ id: "1",
+ date: format(new Date(), "yyyy-MM") + "-13",
+ title: "Lorem ipsum dolor sit amet",
+ startTime: "07:30",
+ endTime: "08:30",
+ },
+ {
+ id: "2",
+ date: format(new Date(), "yyyy-MM") + "-13",
+ title: "Lorem ipsum dolor sit amet",
+ startTime: "10:00",
+ endTime: "11:00",
+ },
+ {
+ id: "3",
+ date: format(new Date(), "yyyy-MM") + "-13",
+ title: "Lorem ipsum dolor sit amet",
+ startTime: "12:30",
+ endTime: "13:30",
+ },
+ {
+ id: "4",
+ date: format(new Date(), "yyyy-MM") + "-13",
+ title: "Lorem ipsum dolor sit amet",
+ startTime: "14:00",
+ endTime: "14:30",
+ },
+ ]);
+
+ return events;
+};
diff --git a/src/plays/index.js b/src/plays/index.js
index dee1abcc87..89543a1073 100644
--- a/src/plays/index.js
+++ b/src/plays/index.js
@@ -18,5 +18,6 @@ export { default as AnalogClock } from 'plays/analog-clock/AnalogClock';
export { default as PasswordGenerator } from 'plays/password-generator/PasswordGenerator';
export { default as WhyTypescript } from 'plays/why-typescript/WhyTypescript';
export { default as NetlifyCardGame } from 'plays/memory-game/NetlifyCardGame';
+export { default as Calendar } from 'plays/calendar/Calendar';
export { default as FunQuiz } from 'plays/fun-quiz/FunQuiz';
//add export here