loading: {loading ? "loading..." : "loaded"}
@@ -327,14 +263,13 @@ export const CalendarWithData = () => {
}
const CalendarWrapper = styled.div`
- border: 1px solid black;
+ height: 100%;
`
const Pre = styled.pre`
text-align: left;
`
-const CalendarTitle = styled.h2``
const CalendarSubtitle = styled.div``
const CalendarTimezone = styled.div``
diff --git a/src/components/Calendar/WeekView.tsx b/src/components/Calendar/WeekView.tsx
index b833346..d014c4b 100644
--- a/src/components/Calendar/WeekView.tsx
+++ b/src/components/Calendar/WeekView.tsx
@@ -258,7 +258,6 @@ const WeekViewBody: React.FC = (props) => {
const colCount = group.length
return Array.from(col).map((ev, eventIndex) => {
const { data: event, startDate } = ev
-
let topPosition =
startDate.hour() + startDate.minute() / 60
topPosition *= 100
@@ -267,6 +266,7 @@ const WeekViewBody: React.FC = (props) => {
const duration = (event.durationHours * 100) / 24
const isRecurrent = !!event.rrule
+ const toggled = event.toggled
// console.log("[WeekViewEventItem]", {
// event,
// startDate,
@@ -278,6 +278,7 @@ const WeekViewBody: React.FC = (props) => {
return (
`
- display: flex;
+ display: ${(props) => (props.toggled ? "flex" : "none")};
line-height: 1;
position: absolute;
top: ${(p) => (p.topPosition ? `${p.topPosition}%` : 0)};
diff --git a/src/components/Calendar/graphql/listCalendarEventsQuery.graphql b/src/components/Calendar/graphql/listCalendarEventsQuery.graphql
new file mode 100644
index 0000000..d8d7fa0
--- /dev/null
+++ b/src/components/Calendar/graphql/listCalendarEventsQuery.graphql
@@ -0,0 +1,34 @@
+query listCalendarEventsQuery(
+ $filters: ListCalendarEventsInput
+ $first: Float
+ $after: String
+ $before: String
+) {
+ listCalendarEvents(
+ filters: $filters
+ first: $first
+ after: $after
+ before: $before
+ ) {
+ edges {
+ node {
+ _id
+ title
+ isAllDay
+ durationHours
+ startDateUtc
+ endDateUtc
+ rrule
+ exceptionsDatesUtc
+ subject {
+ name
+ }
+ }
+ cursor
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+}
diff --git a/src/components/Calendar/graphql/listSubjectsQuery.graphql b/src/components/Calendar/graphql/listSubjectsQuery.graphql
new file mode 100644
index 0000000..64ae0c6
--- /dev/null
+++ b/src/components/Calendar/graphql/listSubjectsQuery.graphql
@@ -0,0 +1,25 @@
+query listSubjectsQuery(
+ $filters: ListSubjectInput
+ $first: Float
+ $after: String
+ $before: String
+) {
+ listSubjects(
+ filters: $filters
+ first: $first
+ after: $after
+ before: $before
+ ) {
+ edges {
+ node {
+ _id
+ name
+ }
+ cursor
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+}
diff --git a/src/components/CalendarSidebar.tsx b/src/components/CalendarSidebar.tsx
new file mode 100644
index 0000000..37a80b4
--- /dev/null
+++ b/src/components/CalendarSidebar.tsx
@@ -0,0 +1,69 @@
+import * as React from "react"
+import styled from "styled-components"
+// import { Link } from "react-router"
+
+// Hooks
+import { useSchoolCalendar } from "../pages/CalendarPage/SchoolCalendarContext"
+
+// Components
+import { CalendarTitle } from "./CalendarTitle"
+import { Logo } from "./Logo"
+import { VerticalAccordion } from "./VerticalAccordion"
+import { ItemToggle } from "./ItemToggle"
+
+// Types
+import { ICareer } from "../types/Career"
+import { ISubject } from "../types/Subject"
+
+// Helpers
+import { deepClone } from "../helpers/deepClone"
+import { mapEdges } from "../helpers/mapEdges"
+
+export const CalendarSidebar: React.FC = () => {
+ const { careers, school, setSchool } = useSchoolCalendar()
+
+ const toggleSubject = (careerIndex: number, subjectIndex: number) => {
+ const updatedSchool = deepClone(school)
+ const updatedCareerEdges = updatedSchool?.careersConnection?.edges
+ const updatedSubjectEdges =
+ updatedCareerEdges[careerIndex]?.node?.subjectsConnection?.edges
+ const updatedSubject = updatedSubjectEdges[subjectIndex]?.node
+ updatedSubject.toggled = !updatedSubject?.toggled
+
+ setSchool(updatedSchool)
+ }
+
+ return (
+
+
+
+ {careers?.map((career, cIndex) => {
+ const { name, subjectsConnection }: ICareer = career
+ const subjects: ISubject[] = mapEdges(subjectsConnection?.edges)
+
+ return (
+
+ {subjects?.map((subject, sIndex) => (
+ {
+ toggleSubject(cIndex, sIndex)
+ }}
+ />
+ ))}
+
+ )
+ })}
+
+ )
+}
+
+// Styled components
+
+const SidebarWrapper = styled.div`
+ height: 100vh;
+ background-color: #fff;
+ grid-row: span 2;
+ padding: 25px 10px;
+`
diff --git a/src/components/CalendarTitle.tsx b/src/components/CalendarTitle.tsx
new file mode 100644
index 0000000..6e4c8d8
--- /dev/null
+++ b/src/components/CalendarTitle.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import styled from "styled-components"
+
+interface ICalendarTitleProps {
+ title: string
+}
+
+export const CalendarTitle: React.FC = (props) => {
+ const { title } = props
+
+ return (
+
+
+ {title}
+
+ )
+}
+
+// Styled components
+
+const CalendarTitleWrapper = styled.div`
+ align-items: center;
+ display: flex;
+ margin-bottom: 50px;
+ margin-top: 50px;
+`
+
+const AvatarImage = styled.div`
+ background-color: #ccc;
+ border-radius: 50%;
+ display: inline-block;
+ flex-basis: 70px;
+ flex-shrink: 0;
+ height: 70px;
+ margin-right: 15px;
+`
diff --git a/src/components/ItemToggle.tsx b/src/components/ItemToggle.tsx
new file mode 100644
index 0000000..b266017
--- /dev/null
+++ b/src/components/ItemToggle.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+import styled from "styled-components"
+
+interface IItemToggleProps {
+ text: string
+ toggled: boolean
+ setToggled: () => void
+}
+
+export const ItemToggle: React.FC = (props) => {
+ const { text, toggled, setToggled } = props
+
+ return (
+ {
+ setToggled()
+ }}
+ >
+
+ {text}
+
+ )
+}
+
+// Local components
+
+interface ICheckboxProps {
+ toggled: boolean
+}
+
+const Checkbox: React.FC = (props) => {
+ const { toggled } = props
+ return (
+
+ {toggled ? : null}
+
+ )
+}
+
+// Styled Components
+
+const ToggleWrapper = styled.div`
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+`
+
+const Text = styled.p`
+ text-align: right;
+`
+
+interface ICheckboxWrapperProps {
+ toggled: boolean
+}
+
+const CheckboxWrapper = styled.div`
+ align-items: center;
+ background-color: #CCC;
+ /* background-color: ${(props) => (props.toggled ? "#222" : "#CCC")}; */
+ border-radius: 5px;
+ box-shadow: ${(props) =>
+ props.toggled ? "inset 0 0 0 2px #222" : undefined};
+ display: flex;
+ flex-basis: 20px;
+ flex-shrink: 0;
+ justify-content: center;
+ height: 20px;
+`
+
+const CheckboxMarker = styled.div`
+ background-color: #222;
+ border-radius: 2px;
+ height: 12px;
+ width: 12px;
+`
diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx
new file mode 100644
index 0000000..1dee7f9
--- /dev/null
+++ b/src/components/Logo.tsx
@@ -0,0 +1,13 @@
+import * as React from "react"
+import LOGO from "../assets/logo.svg"
+
+interface ILogoProps {
+ height?: number
+}
+
+export const Logo: React.FC = (props) => {
+ const { height } = props
+ return (
+
+ )
+}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
new file mode 100644
index 0000000..049f3ad
--- /dev/null
+++ b/src/components/Navbar.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+import styled from "styled-components"
+
+// Components
+import { Button } from "../components/Button"
+
+export const Navbar: React.FC = () => {
+ return (
+
+
+
+
+ )
+}
+
+const NavbarWrapper = styled.nav`
+ background-color: #fff;
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: center;
+ padding-right: 20px;
+`
diff --git a/src/components/VerticalAccordion.tsx b/src/components/VerticalAccordion.tsx
new file mode 100644
index 0000000..5dd0e5a
--- /dev/null
+++ b/src/components/VerticalAccordion.tsx
@@ -0,0 +1,70 @@
+import * as React from "react"
+import styled from "styled-components"
+import { FiChevronDown } from "react-icons/fi"
+
+interface IVerticalAccordionProps {
+ title: string
+ children: JSX.Element[] | JSX.Element
+}
+
+export const VerticalAccordion: React.FC = (props) => {
+ const { title, children } = props
+
+ const [visible, setVisible] = React.useState(false)
+
+ const toggleAccordion = () => {
+ setVisible(!visible)
+ }
+
+ return (
+
+
+ {title}
+
+
+
+
+ {children}
+
+ )
+}
+
+// Styled components
+
+interface IVisibilityProps {
+ visible: boolean
+}
+
+const AccordionWrapper = styled.div`
+ border-radius: 5px;
+ padding: 5px 15px;
+ transition: all 0.15s linear;
+ &:hover {
+ box-shadow: 0 1px 4px 0 #666;
+ }
+`
+
+const Arrow = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transform: rotate(${(props) => (props.visible ? "180deg" : "0deg")});
+ transform-origin: center center;
+`
+
+const Dropdown = styled.div`
+ display: ${(props) => (props.visible ? undefined : "none")};
+ /* user-select: none; */
+`
+
+const DropdownToggle = styled.div`
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ p {
+ margin-bottom: 10px;
+ margin-top: 10px;
+ }
+`
diff --git a/src/helpers/addStateVariablesToSchool.ts b/src/helpers/addStateVariablesToSchool.ts
new file mode 100644
index 0000000..1be6172
--- /dev/null
+++ b/src/helpers/addStateVariablesToSchool.ts
@@ -0,0 +1,21 @@
+// Helpers
+import { deepClone } from "./deepClone"
+
+// Types
+import { ISchool } from "../types/School"
+
+export const addStateVariablesToSchool = (schoolRawData: ISchool): ISchool => {
+ const updatedSchool = deepClone(schoolRawData)
+
+ if (updatedSchool?.careersConnection) {
+ for (let careerEdge of updatedSchool.careersConnection?.edges) {
+ if (careerEdge.node?.subjectsConnection) {
+ for (let subjectEdge of careerEdge.node.subjectsConnection.edges) {
+ subjectEdge.node.toggled = true
+ }
+ }
+ }
+ }
+
+ return updatedSchool
+}
diff --git a/src/helpers/deepClone.ts b/src/helpers/deepClone.ts
new file mode 100644
index 0000000..4b33d8b
--- /dev/null
+++ b/src/helpers/deepClone.ts
@@ -0,0 +1,4 @@
+export const deepClone = (object: T): T => {
+ if (!object) return object
+ return JSON.parse(JSON.stringify(object))
+}
diff --git a/src/helpers/mapEdges.tsx b/src/helpers/mapEdges.tsx
new file mode 100644
index 0000000..df559d4
--- /dev/null
+++ b/src/helpers/mapEdges.tsx
@@ -0,0 +1,5 @@
+import { IEdge } from "../types/Connection"
+
+export const mapEdges = (edges: IEdge[]): T[] => {
+ return edges?.map((edge) => edge?.node)
+}
diff --git a/src/layouts/CalendarLayout.tsx b/src/layouts/CalendarLayout.tsx
new file mode 100644
index 0000000..e1713b0
--- /dev/null
+++ b/src/layouts/CalendarLayout.tsx
@@ -0,0 +1,28 @@
+import * as React from "react"
+import styled from "styled-components"
+
+// Components
+import { Navbar } from "../components/Navbar"
+import { CalendarSidebar } from "../components/CalendarSidebar"
+
+interface ICalendarLayoutProps {
+ children: JSX.Element[] | JSX.Element
+}
+
+export const CalendarLayout: React.FC = (props) => {
+ const { children } = props
+
+ return (
+
+
+
+ {children}
+
+ )
+}
+
+const LayoutWrapper = styled.div`
+ display: grid;
+ grid-template-columns: 250px auto;
+ grid-template-rows: var(--navbar-height) auto;
+`
diff --git a/src/pages/CalendarPage/CalendarPage.tsx b/src/pages/CalendarPage/CalendarPage.tsx
index b761ad9..0b05855 100644
--- a/src/pages/CalendarPage/CalendarPage.tsx
+++ b/src/pages/CalendarPage/CalendarPage.tsx
@@ -1,9 +1,65 @@
+import * as React from "react"
import { CalendarWithData } from "../../components/Calendar"
+import { useParams } from "react-router"
+import { useQuery } from "@apollo/client"
+
+// Helpers
+import { addStateVariablesToSchool } from "../../helpers/addStateVariablesToSchool"
+import { mapEdges } from "../../helpers/mapEdges"
+
+// Layout
+import { CalendarLayout } from "../../layouts/CalendarLayout"
+
+// Context
+import { SchoolCalendarContext } from "./SchoolCalendarContext"
+
+// Queries
+import GET_SCHOOL_QUERY from "./graphql/getSchoolQuery.graphql"
+
+// Types
+import { ICalendarEvent } from "../../types/CalendarEvent"
+import { ICareer } from "../../types/Career"
+import { ISubject } from "../../types/Subject"
export function Page() {
+ const { schoolShortName } = useParams()
+ const { loading, error, data } = useQuery(GET_SCHOOL_QUERY, {
+ variables: { shortName: schoolShortName },
+ })
+
+ // TODO: Don't know if it's a good practice to have the entire state in one giant object
+
+ /* Preprocessing step to add state variables to school object */
+ const schoolRawData = data?.school
+ const schoolInitialValue = addStateVariablesToSchool(schoolRawData)
+
+ /* Set school as state for Context */
+ const [school, setSchool] = React.useState(schoolInitialValue)
+
+ /* Set related variables for faster access */
+ const careers: ICareer[] = mapEdges(school?.careersConnection?.edges) || []
+ let subjects: ISubject[] = []
+ for (let career of careers) {
+ subjects = subjects.concat(mapEdges(career?.subjectsConnection?.edges))
+ }
+ let calendarEvents: ICalendarEvent[] = []
+ for (let subject of subjects) {
+ const mappedEvents = mapEdges(subject?.calendarEventsConnection?.edges)
+ for (let event of mappedEvents) {
+ event.toggled = subject.toggled
+ }
+ calendarEvents = calendarEvents.concat(mappedEvents)
+ }
+
return (
-
+
+
+
+
+
)
}
diff --git a/src/pages/CalendarPage/SchoolCalendarContext.tsx b/src/pages/CalendarPage/SchoolCalendarContext.tsx
new file mode 100644
index 0000000..143549b
--- /dev/null
+++ b/src/pages/CalendarPage/SchoolCalendarContext.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+
+// Types
+import { ICalendarEvent } from "../../types/CalendarEvent"
+import { ICareer } from "../../types/Career"
+import { ISchool } from "../../types/School"
+interface IDefaultValue {
+ calendarEvents?: ICalendarEvent[]
+ careers?: ICareer[]
+ school?: ISchool
+ setSchool?: React.Dispatch
+}
+
+const defaultValue: IDefaultValue = {
+ calendarEvents: undefined,
+ careers: undefined,
+ school: undefined,
+ setSchool: undefined,
+}
+
+export const SchoolCalendarContext = React.createContext(defaultValue)
+export const useSchoolCalendar = () => {
+ return React.useContext(SchoolCalendarContext)
+}
diff --git a/src/pages/CalendarPage/graphql/getSchoolQuery.graphql b/src/pages/CalendarPage/graphql/getSchoolQuery.graphql
new file mode 100644
index 0000000..c29e4db
--- /dev/null
+++ b/src/pages/CalendarPage/graphql/getSchoolQuery.graphql
@@ -0,0 +1,32 @@
+query getSchoolQuery($shortName: String!) {
+ school(shortName: $shortName) {
+ name
+ careersConnection {
+ edges {
+ node {
+ name
+ subjectsConnection {
+ edges {
+ node {
+ name
+ calendarEventsConnection {
+ edges {
+ node {
+ title
+ isAllDay
+ durationHours
+ startDateUtc
+ endDateUtc
+ rrule
+ exceptionsDatesUtc
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/types/CalendarEvent.ts b/src/types/CalendarEvent.ts
new file mode 100644
index 0000000..770ade6
--- /dev/null
+++ b/src/types/CalendarEvent.ts
@@ -0,0 +1,10 @@
+export interface ICalendarEvent {
+ title?: string
+ isAllDay?: boolean
+ durationHours?: number
+ startDateUtc?: Date
+ endDateUtc?: Date
+ rrule?: string
+ exceptionsDatesUtc?: any // TODO: Better typing
+ toggled?: boolean
+}
diff --git a/src/types/Career.ts b/src/types/Career.ts
new file mode 100644
index 0000000..8d9fc02
--- /dev/null
+++ b/src/types/Career.ts
@@ -0,0 +1,7 @@
+import { IConnection } from "./Connection"
+import { ISubject } from "./Subject"
+
+export interface ICareer {
+ name?: string
+ subjectsConnection?: IConnection
+}
diff --git a/src/types/Connection.ts b/src/types/Connection.ts
new file mode 100644
index 0000000..dad4861
--- /dev/null
+++ b/src/types/Connection.ts
@@ -0,0 +1,15 @@
+export interface IEdge {
+ node?: NodeType
+ cursor?: string
+}
+
+interface IPageInfo {
+ hasNextPage?: boolean
+ endCursor?: string
+}
+
+export interface IConnection {
+ edges?: IEdge[]
+ pageInfo?: IPageInfo
+ totalCount?: number
+}
diff --git a/src/types/School.ts b/src/types/School.ts
new file mode 100644
index 0000000..2db9d60
--- /dev/null
+++ b/src/types/School.ts
@@ -0,0 +1,7 @@
+import { IConnection } from "./Connection"
+import { ICareer } from "./Career"
+
+export interface ISchool {
+ name?: string
+ careersConnection?: IConnection
+}
diff --git a/src/types/Subject.ts b/src/types/Subject.ts
new file mode 100644
index 0000000..bc74ed0
--- /dev/null
+++ b/src/types/Subject.ts
@@ -0,0 +1,9 @@
+import { IConnection } from "./Connection"
+import { ICalendarEvent } from "./CalendarEvent"
+
+export interface ISubject {
+ _id?: string
+ name?: string
+ toggled?: boolean
+ calendarEventsConnection?: IConnection
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e40525a
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "commonjs",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": false,
+ "forceConsistentCasingInFileNames": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react"
+ },
+ "include": ["src"]
+}
diff --git a/yarn.lock b/yarn.lock
index 4f013bd..d51c78d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11702,6 +11702,11 @@ react-hotkeys@2.0.0:
dependencies:
prop-types "^15.6.1"
+react-icons@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.2.0.tgz#6dda80c8a8f338ff96a1851424d63083282630d0"
+ integrity sha512-rmzEDFt+AVXRzD7zDE21gcxyBizD/3NqjbX6cmViAgdqfJ2UiLer8927/QhhrXQV7dEj/1EGuOTPp7JnLYVJKQ==
+
react-inspector@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.0.tgz#45a325e15f33e595be5356ca2d3ceffb7d6b8c3a"
@@ -13768,6 +13773,11 @@ typescript-plugin-styled-components@^1.4.4:
resolved "https://registry.yarnpkg.com/typescript-plugin-styled-components/-/typescript-plugin-styled-components-1.4.4.tgz#fff26d516ff213dffe1a5627fe76f23ae3ee6ada"
integrity sha512-w5S5lSpzRFM+61KNNpGtlF46DuTJTyzfWM4g6ic9m189ILEoU3sgoTNHNS2MxQhXsGtQZwAlINKG+Dwy0euwUg==
+typescript@^4.3.2:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
+ integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==
+
unbox-primitive@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f"