From 2d8a5f6cf3c7a607d5de9cf44ec9a0f2438b6171 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 10:48:04 -0700 Subject: [PATCH 001/355] Adding dashboard route --- backend/pnpm-lock.yaml | 15 +++------------ frontend/src/App.tsx | 13 ++++++++++++- .../src/components/Dashboard/DashboardIndex.tsx | 7 +++++++ 3 files changed, 22 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/Dashboard/DashboardIndex.tsx diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 3bf47f9..692d006 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -10,8 +10,8 @@ specifiers: '@types/nodemailer': ^6.4.4 '@types/parse': ^2.18.16 babel-cli: ^6.26.0 - date-fns: ^2.28.0 cors: ^2.8.5 + date-fns: ^2.28.0 dotenv: ^16.0.1 express: ^4.18.1 googleapis: ^105.0.0 @@ -24,8 +24,8 @@ specifiers: typescript: ^4.7.4 dependencies: - date-fns: 2.28.0 cors: 2.8.5 + date-fns: 2.28.0 dotenv: 16.0.1 express: 4.18.1 googleapis: 105.0.0 @@ -2616,6 +2616,7 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true optional: true + /cors/2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -4748,7 +4749,6 @@ packages: /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - dev: true /object-copy/0.1.0: resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} @@ -4804,15 +4804,6 @@ packages: dev: true optional: true - /object-assign/4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false - - /object-inspect/1.12.2: - resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} - dev: false - /on-finished/2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9eedd3e..4847d8f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom"; +import DashboardIndex from "./components/Dashboard/DashboardIndex"; import DifficultyIndex from "./components/Difficulty/DifficultyIndex"; import Home from "./components/Home"; import HubIndex from "./components/Hub/HubIndex"; @@ -18,6 +19,16 @@ function App() { } /> } /> } /> + + + + } + > + } /> + { + return
DashboardIndex
; +}; + +export default DashboardIndex; From 81f4e157970b3ae9ae340d62bfa387eb6fe43ecc Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 10:50:56 -0700 Subject: [PATCH 002/355] Adding initial dashboard header --- frontend/src/components/Dashboard/DashboardIndex.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index 236ccfa..e28289e 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -1,7 +1,17 @@ +import { Button, Container, Flex, Heading, VStack } from "@chakra-ui/react"; import React from "react"; const DashboardIndex = () => { - return
DashboardIndex
; + return ( + + + + Learning Dashboard + + + + + ); }; export default DashboardIndex; From 2dfaaf4adfde89815ba8e03c79d9a261e2f0daaa Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 10:58:04 -0700 Subject: [PATCH 003/355] Adding grid --- .../components/Dashboard/DashboardIndex.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index e28289e..ac2cdba 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -1,5 +1,13 @@ -import { Button, Container, Flex, Heading, VStack } from "@chakra-ui/react"; +import { + Button, + Container, + Flex, + Grid, + Heading, + VStack, +} from "@chakra-ui/react"; import React from "react"; +import CourseCard from "./CourseCard"; const DashboardIndex = () => { return ( @@ -9,6 +17,16 @@ const DashboardIndex = () => { Learning Dashboard + + + + + + ); From 927588c0f88981b5f77581351484159bf0671c96 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 10:58:17 -0700 Subject: [PATCH 004/355] Adding course dashboard card --- .../src/components/Dashboard/CourseCard.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 frontend/src/components/Dashboard/CourseCard.tsx diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx new file mode 100644 index 0000000..7cd0d71 --- /dev/null +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -0,0 +1,58 @@ +import { ChevronDownIcon, HamburgerIcon } from "@chakra-ui/icons"; +import { + Box, + Button, + Flex, + Heading, + IconButton, + Image, + Menu, + MenuButton, + MenuItem, + MenuList, + Text, +} from "@chakra-ui/react"; +import React from "react"; +import useThemeColor from "../../hooks/useThemeColor"; + +const CourseCard = () => { + const { backgroundColor, borderColor } = useThemeColor(); + return ( + + + + + + React Course + + + } + > + + Download + Create a Copy + Mark as Draft + Delete + Attend a Workshop + + + + + + ); +}; + +export default CourseCard; From 464e37a443ca8e691d241ec5ae0d9321c92ae7e9 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 10:59:15 -0700 Subject: [PATCH 005/355] Adding correct menu items --- frontend/src/components/Dashboard/CourseCard.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index 7cd0d71..95c2fc4 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -24,6 +24,7 @@ const CourseCard = () => { borderWidth={1} borderRadius={4} w="100%" + cursor={"pointer"} > @@ -42,11 +43,8 @@ const CourseCard = () => { icon={} > - Download - Create a Copy - Mark as Draft - Delete - Attend a Workshop + Share course + Delete course From a4e88cf8f2978a083d7cdcd8cfbc0b2d43e79ec5 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 11:08:19 -0700 Subject: [PATCH 006/355] Adding progress detail to popover --- .../components/Dashboard/DashboardIndex.tsx | 2 + frontend/src/components/Popover/Popover.tsx | 50 +++++++++++++++++++ .../src/components/Popover/PopoverPortal.tsx | 7 +++ 3 files changed, 59 insertions(+) create mode 100644 frontend/src/components/Popover/Popover.tsx create mode 100644 frontend/src/components/Popover/PopoverPortal.tsx diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index ac2cdba..63d051a 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -7,6 +7,7 @@ import { VStack, } from "@chakra-ui/react"; import React from "react"; +import Popover from "../Popover/Popover"; import CourseCard from "./CourseCard"; const DashboardIndex = () => { @@ -18,6 +19,7 @@ const DashboardIndex = () => { + { + const { backgroundColor, borderColor } = useThemeColor(); + return ( + + + React Course + + + Beginner + + 50% + + + + Intermediate + + 70% + + + + Advanced + + 20% + + + + ); +}; + +export default Popover; diff --git a/frontend/src/components/Popover/PopoverPortal.tsx b/frontend/src/components/Popover/PopoverPortal.tsx new file mode 100644 index 0000000..4d23e1f --- /dev/null +++ b/frontend/src/components/Popover/PopoverPortal.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const PopoverPortal = () => { + return
PopoverPortal
; +}; + +export default PopoverPortal; From 51e83263628f5f5ca4f225e6401b7a3a642cf6c9 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 11:11:03 -0700 Subject: [PATCH 007/355] Adding props to popover --- .../components/Dashboard/DashboardIndex.tsx | 7 ++++- frontend/src/components/Popover/Popover.tsx | 28 +++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index 63d051a..1b025b5 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -19,7 +19,12 @@ const DashboardIndex = () => { - + { +interface Props { + courseName: string; + beginnerProgress: number; + intermediateProgress: number; + advancedProgress: number; +} + +const Popover = ({ + courseName, + beginnerProgress, + intermediateProgress, + advancedProgress, +}: Props) => { const { backgroundColor, borderColor } = useThemeColor(); return ( { gap={4} > - React Course + {courseName} Beginner - - 50% + + {beginnerProgress}% Intermediate - - 70% + + {intermediateProgress}% Advanced - - 20% + + {advancedProgress}% From 9af71385806544472a26750f6f089184aa5cddf6 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 11:17:31 -0700 Subject: [PATCH 008/355] Adding portal component --- frontend/src/components/Popover/PopoverPortal.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Popover/PopoverPortal.tsx b/frontend/src/components/Popover/PopoverPortal.tsx index 4d23e1f..c03d99f 100644 --- a/frontend/src/components/Popover/PopoverPortal.tsx +++ b/frontend/src/components/Popover/PopoverPortal.tsx @@ -1,7 +1,11 @@ import React from "react"; +import ReactDom from "react-dom"; -const PopoverPortal = () => { - return
PopoverPortal
; +interface Props { + children: JSX.Element; +} +const PopoverPortal = ({ children }: Props) => { + return ReactDom.createPortal(children, document.body); }; export default PopoverPortal; From 0df2e402e661425391bfcf4d998e0450c37cf656 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 11:31:24 -0700 Subject: [PATCH 009/355] Adding tooltip logic --- frontend/src/components/Popover/Tooltip.tsx | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 frontend/src/components/Popover/Tooltip.tsx diff --git a/frontend/src/components/Popover/Tooltip.tsx b/frontend/src/components/Popover/Tooltip.tsx new file mode 100644 index 0000000..29f7800 --- /dev/null +++ b/frontend/src/components/Popover/Tooltip.tsx @@ -0,0 +1,36 @@ +import { Box } from "@chakra-ui/react"; +import React, { useState } from "react"; +import PopoverPortal from "./PopoverPortal"; + +interface Props { + children: React.ReactElement; + render: React.ReactElement; +} + +const Tooltip = ({ children, render }: Props) => { + const [show, setShow] = useState(false); + + const handleMouseOver = () => { + setShow(true); + }; + + const handleMouseOut = () => { + setShow(false); + }; + + return ( + <> + {React.cloneElement(children, { + onMouseOver: handleMouseOver, + onMouseOut: handleMouseOut, + })} + + + {render} + + + + ); +}; + +export default Tooltip; From 85750c625021f78b87f78a6963b8f9f10048e3be Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 11:31:33 -0700 Subject: [PATCH 010/355] Reanming interface --- frontend/src/components/Popover/PopoverPortal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Popover/PopoverPortal.tsx b/frontend/src/components/Popover/PopoverPortal.tsx index c03d99f..caccf1a 100644 --- a/frontend/src/components/Popover/PopoverPortal.tsx +++ b/frontend/src/components/Popover/PopoverPortal.tsx @@ -2,7 +2,7 @@ import React from "react"; import ReactDom from "react-dom"; interface Props { - children: JSX.Element; + children: React.ReactNode; } const PopoverPortal = ({ children }: Props) => { return ReactDom.createPortal(children, document.body); From 5c0698335fa308ff89eee4daf672e85e9bcb5161 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 11:31:41 -0700 Subject: [PATCH 011/355] Adding test tooltip --- .../components/Dashboard/DashboardIndex.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index 1b025b5..2dc0836 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -8,6 +8,7 @@ import { } from "@chakra-ui/react"; import React from "react"; import Popover from "../Popover/Popover"; +import Tooltip from "../Popover/Tooltip"; import CourseCard from "./CourseCard"; const DashboardIndex = () => { @@ -19,12 +20,18 @@ const DashboardIndex = () => { - + + } + > +
hi
+
Date: Tue, 12 Jul 2022 11:55:13 -0700 Subject: [PATCH 012/355] Adding position logic to tooltip --- frontend/src/components/Popover/Tooltip.tsx | 26 ++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Popover/Tooltip.tsx b/frontend/src/components/Popover/Tooltip.tsx index 29f7800..1f6c76c 100644 --- a/frontend/src/components/Popover/Tooltip.tsx +++ b/frontend/src/components/Popover/Tooltip.tsx @@ -1,5 +1,5 @@ import { Box } from "@chakra-ui/react"; -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import PopoverPortal from "./PopoverPortal"; interface Props { @@ -7,11 +7,24 @@ interface Props { render: React.ReactElement; } +const getPoint = (element: HTMLElement, ref: HTMLElement) => { + const pt = { x: 0, y: 0 }; + const rectangle = element.getBoundingClientRect(); + const refRectangle = ref.getBoundingClientRect(); + pt.x = rectangle.left + (rectangle.width - refRectangle.width) / 2; + pt.y = rectangle.top - refRectangle.height / 2; + return pt; +}; + const Tooltip = ({ children, render }: Props) => { const [show, setShow] = useState(false); - const handleMouseOver = () => { + const posRef = useRef({ x: 0, y: 0 }); + const tooltipRef = useRef(); + + const handleMouseOver = (e: React.MouseEvent) => { setShow(true); + posRef.current = getPoint(e.currentTarget, tooltipRef.current); }; const handleMouseOut = () => { @@ -25,7 +38,14 @@ const Tooltip = ({ children, render }: Props) => { onMouseOut: handleMouseOut, })} - + {render} From 3440288724515b11c0aea537bd3ab11640767ec1 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 11:55:25 -0700 Subject: [PATCH 013/355] Restyling popover --- frontend/src/components/Popover/Popover.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/Popover/Popover.tsx b/frontend/src/components/Popover/Popover.tsx index c7f03bf..c56f864 100644 --- a/frontend/src/components/Popover/Popover.tsx +++ b/frontend/src/components/Popover/Popover.tsx @@ -29,31 +29,31 @@ const Popover = ({ backgroundColor={backgroundColor} borderColor={borderColor} borderWidth={1} + borderRadius={4} w={"20vw"} p={4} flexDir="column" gap={4} + boxShadow={"0px 10px 15px -3px rgba(0,0,0,0.3)"} > {courseName} Beginner - - {beginnerProgress}% - + Intermediate - - {intermediateProgress}% - + Advanced - - {advancedProgress}% - + ); From dc91b5e1471eca128a0c8683564b1d2fa3dc7da0 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 11:55:37 -0700 Subject: [PATCH 014/355] Adding test popover to one of the courses --- .../components/Dashboard/DashboardIndex.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index 2dc0836..ff2fc7f 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -19,25 +19,25 @@ const DashboardIndex = () => { Learning Dashboard - - - } - > -
hi
-
- + + } + > +
+ +
+
From c3fa42fc04a729f5810c18657db24b155fff25d6 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 14:41:06 -0700 Subject: [PATCH 015/355] Fixing typescript errors --- frontend/src/components/Popover/Tooltip.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Popover/Tooltip.tsx b/frontend/src/components/Popover/Tooltip.tsx index 1f6c76c..eece653 100644 --- a/frontend/src/components/Popover/Tooltip.tsx +++ b/frontend/src/components/Popover/Tooltip.tsx @@ -12,7 +12,7 @@ const getPoint = (element: HTMLElement, ref: HTMLElement) => { const rectangle = element.getBoundingClientRect(); const refRectangle = ref.getBoundingClientRect(); pt.x = rectangle.left + (rectangle.width - refRectangle.width) / 2; - pt.y = rectangle.top - refRectangle.height / 2; + pt.y = rectangle.top - refRectangle.height / 1.2; return pt; }; @@ -20,10 +20,11 @@ const Tooltip = ({ children, render }: Props) => { const [show, setShow] = useState(false); const posRef = useRef({ x: 0, y: 0 }); - const tooltipRef = useRef(); + const tooltipRef = useRef(null); const handleMouseOver = (e: React.MouseEvent) => { setShow(true); + if (!tooltipRef.current) return; posRef.current = getPoint(e.currentTarget, tooltipRef.current); }; From 6f718f1b666ab5fa29d775b55ff566aadc41cebc Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 14:41:15 -0700 Subject: [PATCH 016/355] Adding tooltip to the image --- .../components/Dashboard/DashboardIndex.tsx | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index ff2fc7f..fced630 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -1,14 +1,5 @@ -import { - Button, - Container, - Flex, - Grid, - Heading, - VStack, -} from "@chakra-ui/react"; +import { Button, Container, Flex, Grid, Heading } from "@chakra-ui/react"; import React from "react"; -import Popover from "../Popover/Popover"; -import Tooltip from "../Popover/Tooltip"; import CourseCard from "./CourseCard"; const DashboardIndex = () => { @@ -24,20 +15,7 @@ const DashboardIndex = () => { gap="1em" mt={10} > - - } - > -
- -
-
+
From 74eab9e21cf78d5b64498a6798dc387691276777 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 14:41:27 -0700 Subject: [PATCH 017/355] Adding tooltip to the image --- frontend/src/components/Dashboard/CourseCard.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index 95c2fc4..6552b84 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -14,6 +14,8 @@ import { } from "@chakra-ui/react"; import React from "react"; import useThemeColor from "../../hooks/useThemeColor"; +import Popover from "../Popover/Popover"; +import Tooltip from "../Popover/Tooltip"; const CourseCard = () => { const { backgroundColor, borderColor } = useThemeColor(); @@ -27,7 +29,18 @@ const CourseCard = () => { cursor={"pointer"} > - + + } + > + + Date: Tue, 12 Jul 2022 14:46:09 -0700 Subject: [PATCH 018/355] Adding props to course card --- .../src/components/Dashboard/CourseCard.tsx | 30 +++++++++++++------ .../components/Dashboard/DashboardIndex.tsx | 24 +++++++++++++-- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index 6552b84..dfffced 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -1,7 +1,6 @@ import { ChevronDownIcon, HamburgerIcon } from "@chakra-ui/icons"; import { Box, - Button, Flex, Heading, IconButton, @@ -10,14 +9,27 @@ import { MenuButton, MenuItem, MenuList, - Text, } from "@chakra-ui/react"; import React from "react"; import useThemeColor from "../../hooks/useThemeColor"; import Popover from "../Popover/Popover"; import Tooltip from "../Popover/Tooltip"; -const CourseCard = () => { +interface Props { + title: string; + src: string; + beginnerProgress: number; + intermediateProgress: number; + advancedProgress: number; +} + +const CourseCard = ({ + title, + src, + beginnerProgress, + intermediateProgress, + advancedProgress, +}: Props) => { const { backgroundColor, borderColor } = useThemeColor(); return ( { } > - + { alignItems={"center"} > - React Course + {title} { gap="1em" mt={10} > - - - + + + From bf2b0e5834c420613b6673978a4dfd3f64749c16 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 14:53:05 -0700 Subject: [PATCH 019/355] Adding link between pages --- .../src/components/Dashboard/CourseCard.tsx | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index dfffced..05637bc 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -1,6 +1,7 @@ import { ChevronDownIcon, HamburgerIcon } from "@chakra-ui/icons"; import { Box, + Button, Flex, Heading, IconButton, @@ -11,6 +12,7 @@ import { MenuList, } from "@chakra-ui/react"; import React from "react"; +import { Link } from "react-router-dom"; import useThemeColor from "../../hooks/useThemeColor"; import Popover from "../Popover/Popover"; import Tooltip from "../Popover/Tooltip"; @@ -38,7 +40,6 @@ const CourseCard = ({ borderWidth={1} borderRadius={4} w="100%" - cursor={"pointer"} > } > - + {title} - - } - > - - Share course - Delete course - - + + + + + + } + > + + Share course + Delete course + + + From 463ed0619f6f34d1f3cdf505383551dbb0b5c229 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 14:56:35 -0700 Subject: [PATCH 020/355] Fixing style issue --- frontend/src/components/Popover/Tooltip.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Popover/Tooltip.tsx b/frontend/src/components/Popover/Tooltip.tsx index eece653..6d81e94 100644 --- a/frontend/src/components/Popover/Tooltip.tsx +++ b/frontend/src/components/Popover/Tooltip.tsx @@ -12,7 +12,7 @@ const getPoint = (element: HTMLElement, ref: HTMLElement) => { const rectangle = element.getBoundingClientRect(); const refRectangle = ref.getBoundingClientRect(); pt.x = rectangle.left + (rectangle.width - refRectangle.width) / 2; - pt.y = rectangle.top - refRectangle.height / 1.2; + pt.y = rectangle.top - refRectangle.height + 10; return pt; }; @@ -43,7 +43,7 @@ const Tooltip = ({ children, render }: Props) => { position="fixed" top={posRef.current.y} left={posRef.current.x} - opacity={show ? 1 : 0} + visibility={show ? "visible" : "hidden"} zIndex={999} ref={tooltipRef} > From 52330e341d45b26ea6ad241ba0f6184a0fcf31ed Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 15:00:57 -0700 Subject: [PATCH 021/355] Adding new course route --- frontend/src/App.tsx | 2 ++ frontend/src/components/Dashboard/DashboardIndex.tsx | 7 ++++++- frontend/src/components/NewCourse/NewCourseIndex.tsx | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/NewCourse/NewCourseIndex.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4847d8f..c95dbcb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import Navbar from "./components/Navbar"; import LoginIndex from "./components/Registration/LoginIndex"; import RegisterIndex from "./components/Registration/RegisterIndex"; import RequireAuth from "./components/Registration/RequireAuth"; +import NewCourseIndex from "./components/NewCourse/NewCourseIndex"; function App() { return ( @@ -28,6 +29,7 @@ function App() { } > } /> + } /> { + const navigate = useNavigate(); + return ( Learning Dashboard - + { + return
NewCourseIndex
; +}; + +export default NewCourseIndex; From e917a3f527ff8f60a8a09247ab1bb03b4f5964e0 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 15:14:56 -0700 Subject: [PATCH 022/355] Adding form componet --- .../components/NewCourse/NewCourseIndex.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/NewCourse/NewCourseIndex.tsx b/frontend/src/components/NewCourse/NewCourseIndex.tsx index 3d19e36..ab48050 100644 --- a/frontend/src/components/NewCourse/NewCourseIndex.tsx +++ b/frontend/src/components/NewCourse/NewCourseIndex.tsx @@ -1,7 +1,35 @@ +import { + Button, + Container, + Flex, + FormControl, + FormLabel, + Heading, + Input, +} from "@chakra-ui/react"; import React from "react"; +import Banner from "../Hub/Banner"; const NewCourseIndex = () => { - return
NewCourseIndex
; + return ( + <> + + + + Let{"'"}s learn something new! + + + + What do you want to learn? + + + + + + + ); }; export default NewCourseIndex; From 4f25b73c00890a5b22b31251f6d23c78c6abb5ab Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 12 Jul 2022 15:29:23 -0700 Subject: [PATCH 023/355] adding course route in backend --- backend/app.ts | 2 ++ backend/src/routes/course.ts | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 backend/src/routes/course.ts diff --git a/backend/app.ts b/backend/app.ts index 60fb0dd..6822f45 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -6,6 +6,7 @@ import auth from "./src/routes/auth"; import { NotFoundError } from "./src/utils/errors"; import debug from "./src/routes/debug"; import cors from "cors"; +import course from "./src/routes/course"; dotenv.config(); @@ -19,6 +20,7 @@ app.use(cors()); app.use("/auth", auth); app.use("/debug", debug); +app.use("/course", course); app.get("/", async (req, res) => { const testObject = new Parse.Object("test"); diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts new file mode 100644 index 0000000..8b3e632 --- /dev/null +++ b/backend/src/routes/course.ts @@ -0,0 +1,5 @@ +import express from "express"; + +const course = express.Router(); + +export default course; From 60c534f7ffd0b1135d993c401dabecf05cfc4af5 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 09:53:36 -0700 Subject: [PATCH 024/355] Adding more fields to backend user type --- backend/src/types/course.d.ts | 6 ++++++ backend/src/types/global.d.ts | 1 + backend/src/types/user.d.ts | 5 +++-- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 backend/src/types/course.d.ts diff --git a/backend/src/types/course.d.ts b/backend/src/types/course.d.ts new file mode 100644 index 0000000..0053ba9 --- /dev/null +++ b/backend/src/types/course.d.ts @@ -0,0 +1,6 @@ +import { DbObject } from "./global"; + +export interface ICourse extends DbObject { + name: string; + resources: any[]; +} diff --git a/backend/src/types/global.d.ts b/backend/src/types/global.d.ts index 02c54c2..3b192c6 100644 --- a/backend/src/types/global.d.ts +++ b/backend/src/types/global.d.ts @@ -2,4 +2,5 @@ export interface DbObject { objectId: string; updatedAt: string; createdAt: string; + ACL: object; } diff --git a/backend/src/types/user.d.ts b/backend/src/types/user.d.ts index a6b7a1e..f10ba13 100644 --- a/backend/src/types/user.d.ts +++ b/backend/src/types/user.d.ts @@ -1,7 +1,8 @@ import { DbObject } from "./global"; -export interface IUser extends DbObject { +export interface IUser extends Partial { username: string; email: string; - emailVerified: boolean; + password: string; + emailVerified?: boolean; } From a267b6ef5bf0153fa94ef0dbc3be0466d6856791 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 09:53:51 -0700 Subject: [PATCH 025/355] Adding type to user registration --- backend/src/routes/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 361ff69..0d6b7e2 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -29,7 +29,7 @@ auth.post("/register", async (req, res, next) => { return; } - const user = new Parse.User({ username, password, email }); + const user = new Parse.User({ username, password, email }); try { await user.signUp(); From 5cfa99400d8ad5d9b0f3631ed5175e3cb903905c Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 09:54:10 -0700 Subject: [PATCH 026/355] creating functions to get resources based in a course name --- backend/src/course/course.ts | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 backend/src/course/course.ts diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts new file mode 100644 index 0000000..549a68b --- /dev/null +++ b/backend/src/course/course.ts @@ -0,0 +1,40 @@ +import { getExternalRanking } from "../rating/ranking"; +import { getVideoDetailByIds, getVideosByQuery } from "../rating/youtube"; + +const VIDEOS_PER_QUERY = 100; + +export const createCourse = (name: string) => { + const Course: Parse.Object = new Parse.Object("Course"); + Course.set("name", name); + return Course; +}; + +export const generateResources = async (name: string) => { + const rankedBeginner = await getTop3ByDifficulty(name, "beginner"); + const rankedIntermediate = await getTop3ByDifficulty(name, "intermediate"); + const rankedAdvanced = await getTop3ByDifficulty(name, "advanced"); +}; + +const getTop3ByDifficulty = async (query: string, difficulty: string) => { + const rankedVideos = await getRankedVideos(`${difficulty} ${query} tutorial`); + const splicedRankedVideos = rankedVideos.splice(0, 3); + return splicedRankedVideos; +}; + +const getRankedVideos = async (query: string) => { + const videos = await getVideosByQuery(query, VIDEOS_PER_QUERY); + + const ids = []; + for (const video of videos.data.items) { + ids.push(video.id.videoId); + } + + const videosDetailed = await getVideoDetailByIds(ids, VIDEOS_PER_QUERY); + + const rankedVideos = getExternalRanking(videosDetailed.data.items); + const sortedRankedVideos = rankedVideos.sort( + (a, b) => b.final_score - a.final_score + ); + + return sortedRankedVideos; +}; From ea5de153fa749bb126440e114fa5f15c9ed1781c Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 09:54:17 -0700 Subject: [PATCH 027/355] adding new course route --- backend/src/routes/course.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 8b3e632..1d6aaf2 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -1,5 +1,19 @@ import express from "express"; +import { createCourse } from "../course/course"; +import { ICourse } from "../types/course"; +import { BadRequestError } from "../utils/errors"; const course = express.Router(); +course.post("/new", (req, res, next) => { + const { name } = req.body; + + if (!name) { + next(new BadRequestError("Missing attributes")); + return; + } + + const course = createCourse(name); +}); + export default course; From 6dcca6a37314feeeacd3bd4675c2463343f1b9e4 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 11:06:06 -0700 Subject: [PATCH 028/355] Adding resource type --- backend/src/types/resource.d.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 backend/src/types/resource.d.ts diff --git a/backend/src/types/resource.d.ts b/backend/src/types/resource.d.ts new file mode 100644 index 0000000..b7402b2 --- /dev/null +++ b/backend/src/types/resource.d.ts @@ -0,0 +1,25 @@ +import { ICourse } from "./course"; +import Parse from "parse/node"; + +export type IResource = + | { + type: "video"; + status: "not started" | "in progress" | "completed"; + level: 1 | 2 | 3; + feedback: number; + title: string; + description: string; + url: string; + thumbnail: string; + channel: string; + course: Parse.Object; + } + | { + type: "website"; + status: "not started" | "in progress" | "completed"; + level: 1 | 2 | 3; + feedback: number; + title: string; + url: string; + course: Parse.Object; + }; From 18b1cbd79c2f5664242c49cab2ee97a4fe4f398d Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 11:06:29 -0700 Subject: [PATCH 029/355] createResource and saveResource function --- backend/src/course/course.ts | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index 549a68b..e543489 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -1,5 +1,8 @@ import { getExternalRanking } from "../rating/ranking"; import { getVideoDetailByIds, getVideosByQuery } from "../rating/youtube"; +import Parse from "parse/node"; +import { IResource } from "../types/resource"; +import { IWeightedYoutubeVideo } from "../types/youtube"; const VIDEOS_PER_QUERY = 100; @@ -9,10 +12,49 @@ export const createCourse = (name: string) => { return Course; }; +export const createResource = (resource: IResource) => { + const Resource: Parse.Object = new Parse.Object("Resource"); + + Object.keys(resource).forEach((resourceAttribute) => { + Resource.set(resourceAttribute, resource[resourceAttribute]); + }); + + return Resource; +}; + +export const saveResources = async ( + resources: IWeightedYoutubeVideo[], + course: Parse.Object, + level: 1 | 2 | 3 +) => { + for (const resource of resources) { + const video = createResource({ + type: "video", + level, + status: "not started", + title: resource.snippet.title, + description: resource.snippet.description, + url: `https://youtube.com/video/${resource.id}`, + thumbnail: resource.snippet.thumbnails.default.url, + channel: resource.snippet.channelTitle, + feedback: 0, + course, + }); + + await video.save(); + } +}; + export const generateResources = async (name: string) => { const rankedBeginner = await getTop3ByDifficulty(name, "beginner"); const rankedIntermediate = await getTop3ByDifficulty(name, "intermediate"); const rankedAdvanced = await getTop3ByDifficulty(name, "advanced"); + + return { + beginner: rankedBeginner, + intermediate: rankedIntermediate, + advanced: rankedAdvanced, + }; }; const getTop3ByDifficulty = async (query: string, difficulty: string) => { From f190240b1aaba95b6e20222644a2025d90821eef Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 11:06:48 -0700 Subject: [PATCH 030/355] saving resources for different difficulties --- backend/src/routes/course.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 1d6aaf2..7658371 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -1,11 +1,16 @@ import express from "express"; -import { createCourse } from "../course/course"; +import { + createCourse, + createResource, + generateResources, + saveResources, +} from "../course/course"; import { ICourse } from "../types/course"; import { BadRequestError } from "../utils/errors"; const course = express.Router(); -course.post("/new", (req, res, next) => { +course.post("/new", async (req, res, next) => { const { name } = req.body; if (!name) { @@ -14,6 +19,20 @@ course.post("/new", (req, res, next) => { } const course = createCourse(name); + const { beginner, intermediate, advanced } = await generateResources(name); + + try { + // Save course first + await course.save(); + + await saveResources(beginner, course, 1); + await saveResources(intermediate, course, 2); + await saveResources(advanced, course, 3); + + res.status(201).send("Generated Course!"); + } catch (error) { + next(new BadRequestError(error)); + } }); export default course; From 565a27d1655c73e73b788dfe24751d4921dfe47d Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 11:12:38 -0700 Subject: [PATCH 031/355] Removing intermediate courses --- backend/src/course/course.ts | 4 +--- backend/src/routes/course.ts | 5 ++--- backend/src/types/resource.d.ts | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index e543489..df9190f 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -25,7 +25,7 @@ export const createResource = (resource: IResource) => { export const saveResources = async ( resources: IWeightedYoutubeVideo[], course: Parse.Object, - level: 1 | 2 | 3 + level: 1 | 2 ) => { for (const resource of resources) { const video = createResource({ @@ -47,12 +47,10 @@ export const saveResources = async ( export const generateResources = async (name: string) => { const rankedBeginner = await getTop3ByDifficulty(name, "beginner"); - const rankedIntermediate = await getTop3ByDifficulty(name, "intermediate"); const rankedAdvanced = await getTop3ByDifficulty(name, "advanced"); return { beginner: rankedBeginner, - intermediate: rankedIntermediate, advanced: rankedAdvanced, }; }; diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 7658371..b27dce0 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -19,15 +19,14 @@ course.post("/new", async (req, res, next) => { } const course = createCourse(name); - const { beginner, intermediate, advanced } = await generateResources(name); + const { beginner, advanced } = await generateResources(name); try { // Save course first await course.save(); await saveResources(beginner, course, 1); - await saveResources(intermediate, course, 2); - await saveResources(advanced, course, 3); + await saveResources(advanced, course, 2); res.status(201).send("Generated Course!"); } catch (error) { diff --git a/backend/src/types/resource.d.ts b/backend/src/types/resource.d.ts index b7402b2..e8b2772 100644 --- a/backend/src/types/resource.d.ts +++ b/backend/src/types/resource.d.ts @@ -5,7 +5,7 @@ export type IResource = | { type: "video"; status: "not started" | "in progress" | "completed"; - level: 1 | 2 | 3; + level: 1 | 2; feedback: number; title: string; description: string; @@ -17,7 +17,7 @@ export type IResource = | { type: "website"; status: "not started" | "in progress" | "completed"; - level: 1 | 2 | 3; + level: 1 | 2; feedback: number; title: string; url: string; From 18afe2afaa3b53d3e91f193b79774b6c46da7428 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 13:46:18 -0700 Subject: [PATCH 032/355] Adding formik to new course form --- .../components/NewCourse/NewCourseIndex.tsx | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/NewCourse/NewCourseIndex.tsx b/frontend/src/components/NewCourse/NewCourseIndex.tsx index ab48050..13dbab6 100644 --- a/frontend/src/components/NewCourse/NewCourseIndex.tsx +++ b/frontend/src/components/NewCourse/NewCourseIndex.tsx @@ -7,10 +7,19 @@ import { Heading, Input, } from "@chakra-ui/react"; +import { Field, Form, Formik } from "formik"; import React from "react"; import Banner from "../Hub/Banner"; +interface LearnForm { + name: string; +} + const NewCourseIndex = () => { + const handleSubmit = (values: LearnForm) => { + console.log(values); + }; + return ( <> @@ -18,15 +27,21 @@ const NewCourseIndex = () => { Let{"'"}s learn something new! - - - What do you want to learn? - - - - + + {() => ( +
+ + + What do you want to learn? + + + + +
+ )} +
); From fa7f2c64f2abe934e55d28e002436d9fdab3086a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 13:49:15 -0700 Subject: [PATCH 033/355] Adding form error validation and helper --- .../components/NewCourse/NewCourseIndex.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/NewCourse/NewCourseIndex.tsx b/frontend/src/components/NewCourse/NewCourseIndex.tsx index 13dbab6..f490ef8 100644 --- a/frontend/src/components/NewCourse/NewCourseIndex.tsx +++ b/frontend/src/components/NewCourse/NewCourseIndex.tsx @@ -3,6 +3,8 @@ import { Container, Flex, FormControl, + FormErrorMessage, + FormHelperText, FormLabel, Heading, Input, @@ -10,11 +12,16 @@ import { import { Field, Form, Formik } from "formik"; import React from "react"; import Banner from "../Hub/Banner"; +import * as Yup from "yup"; interface LearnForm { name: string; } +const schema = Yup.object({ + name: Yup.string().required("Topic of the course is required"), +}); + const NewCourseIndex = () => { const handleSubmit = (values: LearnForm) => { console.log(values); @@ -27,13 +34,22 @@ const NewCourseIndex = () => { Let{"'"}s learn something new! - - {() => ( + + {({ errors, touched }) => (
- + What do you want to learn? + + Write the topic you would like to learn (E.g. React, NextJs, + Node) + + {errors.name} From fa03a82a7576f4abd79850fa2fcabc016e793e6c Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 14:11:22 -0700 Subject: [PATCH 036/355] Adding types from backend --- frontend/src/types/course.d.ts | 6 ++++++ frontend/src/types/resource.d.ts | 25 +++++++++++++++++++++++++ frontend/src/types/youtube.d.ts | 21 +++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 frontend/src/types/course.d.ts create mode 100644 frontend/src/types/resource.d.ts create mode 100644 frontend/src/types/youtube.d.ts diff --git a/frontend/src/types/course.d.ts b/frontend/src/types/course.d.ts new file mode 100644 index 0000000..0053ba9 --- /dev/null +++ b/frontend/src/types/course.d.ts @@ -0,0 +1,6 @@ +import { DbObject } from "./global"; + +export interface ICourse extends DbObject { + name: string; + resources: any[]; +} diff --git a/frontend/src/types/resource.d.ts b/frontend/src/types/resource.d.ts new file mode 100644 index 0000000..e8b2772 --- /dev/null +++ b/frontend/src/types/resource.d.ts @@ -0,0 +1,25 @@ +import { ICourse } from "./course"; +import Parse from "parse/node"; + +export type IResource = + | { + type: "video"; + status: "not started" | "in progress" | "completed"; + level: 1 | 2; + feedback: number; + title: string; + description: string; + url: string; + thumbnail: string; + channel: string; + course: Parse.Object; + } + | { + type: "website"; + status: "not started" | "in progress" | "completed"; + level: 1 | 2; + feedback: number; + title: string; + url: string; + course: Parse.Object; + }; diff --git a/frontend/src/types/youtube.d.ts b/frontend/src/types/youtube.d.ts new file mode 100644 index 0000000..0febe51 --- /dev/null +++ b/frontend/src/types/youtube.d.ts @@ -0,0 +1,21 @@ +import { youtube_v3 } from "googleapis"; + +export interface IExternalRankingScore { + date: number; + dateXViews: number; + dateXLikes: number; + useOfChapters: number; +} + +export interface IRawYoutubeVideo extends youtube_v3.Schema$Video { + raw_score: IExternalRankingScore; +} + +export interface INormalizedYoutubeVideo extends IRawYoutubeVideo { + normalized_score: IExternalRankingScore; +} + +export interface IWeightedYoutubeVideo extends INormalizedYoutubeVideo { + weighted_score: IExternalRankingScore; + final_score: number; +} From b3a92827d848e239de70f2f64d09c68ed733a52b Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 13 Jul 2022 14:11:34 -0700 Subject: [PATCH 037/355] Adding toasts --- backend/src/routes/course.ts | 4 +-- .../components/NewCourse/NewCourseIndex.tsx | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index b27dce0..6510d6c 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -23,12 +23,12 @@ course.post("/new", async (req, res, next) => { try { // Save course first - await course.save(); + const courseData = await course.save(); await saveResources(beginner, course, 1); await saveResources(advanced, course, 2); - res.status(201).send("Generated Course!"); + res.status(201).send(courseData); } catch (error) { next(new BadRequestError(error)); } diff --git a/frontend/src/components/NewCourse/NewCourseIndex.tsx b/frontend/src/components/NewCourse/NewCourseIndex.tsx index 508fc2d..b60e21c 100644 --- a/frontend/src/components/NewCourse/NewCourseIndex.tsx +++ b/frontend/src/components/NewCourse/NewCourseIndex.tsx @@ -8,14 +8,18 @@ import { FormLabel, Heading, Input, + useToast, } from "@chakra-ui/react"; import { Field, Form, Formik } from "formik"; import React from "react"; import Banner from "../Hub/Banner"; import * as Yup from "yup"; import { useMutation } from "react-query"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import { baseURL } from "../../utils/constants"; +import { ErrorType } from "../../types/requests"; +import { useNavigate } from "react-router-dom"; +import { ICourse } from "../../../../types/course"; interface LearnForm { name: string; @@ -26,21 +30,35 @@ const schema = Yup.object({ }); const NewCourseIndex = () => { + const toast = useToast(); + const navigate = useNavigate(); + const handleSubmit = (values: LearnForm) => { createCourse.mutate(values.name); }; const createCourse = useMutation( async (name: string) => { - const res = await axios.post(`${baseURL}/course/new`, { name }); + const res = await axios.post(`${baseURL}/course/new`, { name }); return res.data; }, { - onSuccess: () => { - alert("done"); + onSuccess: (course) => { + toast({ + title: "Course created!", + description: "Your custom course was created successfully!", + status: "success", + }); + navigate(`/course/difficulty/${course.objectId}`); }, - onError: () => { - alert("error"); + onError: (error: AxiosError) => { + toast({ + title: "An error ocurred", + description: error.response?.data.message, + status: "error", + duration: 5000, + isClosable: true, + }); }, } ); From 1d469126fdfe83bb34bb729474631ee0fd577400 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 13:24:09 -0700 Subject: [PATCH 038/355] Adding getConfig function to generate the auth headers --- frontend/src/utils/auth.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/auth.ts b/frontend/src/utils/auth.ts index 5c47aef..5976c83 100644 --- a/frontend/src/utils/auth.ts +++ b/frontend/src/utils/auth.ts @@ -5,10 +5,10 @@ export const persistUser = (user: IUser) => { localStorage.setItem("user", JSON.stringify(user)); }; -export const getCachedUser = (): IUser | null => { +export const getCachedUser = (): { user: IUser } | null => { const userString = localStorage.getItem("user"); if (!userString) return null; - return JSON.parse(userString) as IUser; + return JSON.parse(userString); }; export const deleteCachedUser = () => { @@ -30,9 +30,15 @@ export const useSession = () => { setIsFetching(false); return; } - setUser(cachedUser); + setUser(cachedUser.user); setIsFetching(false); }; return { user, fetchUser, isFetching }; }; + +export const getConfig = (token: string) => { + return { + headers: { Authorization: token }, + }; +}; From d0a4adf055d31612b41d7346ee517d127d9f7e61 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 13:24:24 -0700 Subject: [PATCH 039/355] Adding auth headers to create course post --- frontend/src/components/NewCourse/NewCourseIndex.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/NewCourse/NewCourseIndex.tsx b/frontend/src/components/NewCourse/NewCourseIndex.tsx index b60e21c..6476da7 100644 --- a/frontend/src/components/NewCourse/NewCourseIndex.tsx +++ b/frontend/src/components/NewCourse/NewCourseIndex.tsx @@ -20,6 +20,7 @@ import { baseURL } from "../../utils/constants"; import { ErrorType } from "../../types/requests"; import { useNavigate } from "react-router-dom"; import { ICourse } from "../../../../types/course"; +import { getConfig, useSession } from "../../utils/auth"; interface LearnForm { name: string; @@ -32,6 +33,7 @@ const schema = Yup.object({ const NewCourseIndex = () => { const toast = useToast(); const navigate = useNavigate(); + const { user } = useSession(); const handleSubmit = (values: LearnForm) => { createCourse.mutate(values.name); @@ -39,7 +41,13 @@ const NewCourseIndex = () => { const createCourse = useMutation( async (name: string) => { - const res = await axios.post(`${baseURL}/course/new`, { name }); + if (!user) throw new Error("User is not logged in"); + + const res = await axios.post( + `${baseURL}/course/new`, + { name }, + getConfig(user.sessionToken) + ); return res.data; }, { From 1a9cc6daf0260940fca16c506d23b4c8c4eb10eb Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 13:24:36 -0700 Subject: [PATCH 040/355] Adding User Typescript type --- backend/src/types/resource.d.ts | 2 ++ backend/src/types/user.d.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/backend/src/types/resource.d.ts b/backend/src/types/resource.d.ts index e8b2772..7e9cf16 100644 --- a/backend/src/types/resource.d.ts +++ b/backend/src/types/resource.d.ts @@ -13,6 +13,7 @@ export type IResource = thumbnail: string; channel: string; course: Parse.Object; + user: Parse.User; } | { type: "website"; @@ -22,4 +23,5 @@ export type IResource = title: string; url: string; course: Parse.Object; + user: Parse.User; }; diff --git a/backend/src/types/user.d.ts b/backend/src/types/user.d.ts index f10ba13..1cc68eb 100644 --- a/backend/src/types/user.d.ts +++ b/backend/src/types/user.d.ts @@ -1,4 +1,6 @@ +import { Request } from "express"; import { DbObject } from "./global"; +import Parse from "parse/node"; export interface IUser extends Partial { username: string; @@ -6,3 +8,5 @@ export interface IUser extends Partial { password: string; emailVerified?: boolean; } + +export type RequestWUser = Request & { user: Parse.User }; From d9ff822e58be2d2c2a661e55fb69cada0b7d9bef Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 13:24:58 -0700 Subject: [PATCH 041/355] Adding user middleware --- backend/src/middleware/getAuthUser.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 backend/src/middleware/getAuthUser.ts diff --git a/backend/src/middleware/getAuthUser.ts b/backend/src/middleware/getAuthUser.ts new file mode 100644 index 0000000..d8e1afa --- /dev/null +++ b/backend/src/middleware/getAuthUser.ts @@ -0,0 +1,24 @@ +import Parse from "parse/node"; +import { Express, Request, Response, NextFunction } from "express"; +import { BadRequestError } from "../utils/errors"; +import { IUser, RequestWUser } from "../types/user"; + +export const getAuthUser = async ( + req: RequestWUser, + res: Response, + next: NextFunction +) => { + const token = req.headers.authorization; + if (!token) { + next(new BadRequestError("Missing authorization header")); + return; + } + try { + Parse.User.enableUnsafeCurrentUser(); + const user = await Parse.User.become(token); + req.user = user; + next(); + } catch (error) { + next(new BadRequestError("Invalid token")); + } +}; From 6c743f885f418ca93c900bcb63c13ed295b8bd27 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 13:25:54 -0700 Subject: [PATCH 042/355] Adding middleware to course route --- backend/src/course/course.ts | 10 ++++++++-- backend/src/routes/course.ts | 15 ++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index df9190f..c1fdec1 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -6,9 +6,13 @@ import { IWeightedYoutubeVideo } from "../types/youtube"; const VIDEOS_PER_QUERY = 100; -export const createCourse = (name: string) => { +export const createCourse = ( + name: string, + user: Parse.Object +) => { const Course: Parse.Object = new Parse.Object("Course"); Course.set("name", name); + Course.set("user", user); return Course; }; @@ -25,7 +29,8 @@ export const createResource = (resource: IResource) => { export const saveResources = async ( resources: IWeightedYoutubeVideo[], course: Parse.Object, - level: 1 | 2 + level: 1 | 2, + user: Parse.User ) => { for (const resource of resources) { const video = createResource({ @@ -39,6 +44,7 @@ export const saveResources = async ( channel: resource.snippet.channelTitle, feedback: 0, course, + user, }); await video.save(); diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 6510d6c..e4033c7 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -5,28 +5,33 @@ import { generateResources, saveResources, } from "../course/course"; +import { getAuthUser } from "../middleware/getAuthUser"; import { ICourse } from "../types/course"; +import { RequestWUser } from "../types/user"; import { BadRequestError } from "../utils/errors"; const course = express.Router(); -course.post("/new", async (req, res, next) => { +course.use(getAuthUser); + +course.post("/new", async (req: RequestWUser, res, next) => { const { name } = req.body; + const { user } = req; - if (!name) { + if (!name || !user) { next(new BadRequestError("Missing attributes")); return; } - const course = createCourse(name); + const course = createCourse(name, user); const { beginner, advanced } = await generateResources(name); try { // Save course first const courseData = await course.save(); - await saveResources(beginner, course, 1); - await saveResources(advanced, course, 2); + await saveResources(beginner, course, 1, user); + await saveResources(advanced, course, 2, user); res.status(201).send(courseData); } catch (error) { From e07bb55ff500d381c176894fe42c56f874285b91 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 14:02:50 -0700 Subject: [PATCH 043/355] Adding get courses function --- backend/src/course/course.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index c1fdec1..0702168 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -6,6 +6,17 @@ import { IWeightedYoutubeVideo } from "../types/youtube"; const VIDEOS_PER_QUERY = 100; +export const getUserCourses = async (user: Parse.Object) => { + const Course = Parse.Object.extend("Course"); + const query = new Parse.Query(Course); + + query.equalTo("user", user); + + const courses = await query.findAll(); + + return courses; +}; + export const createCourse = ( name: string, user: Parse.Object From e3bcf2fb76f696ae072e57d2270de642fd4ef5fa Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 14:06:35 -0700 Subject: [PATCH 044/355] Adding get my courses route --- backend/src/routes/course.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index e4033c7..2e7a4bc 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -3,6 +3,7 @@ import { createCourse, createResource, generateResources, + getUserCourses, saveResources, } from "../course/course"; import { getAuthUser } from "../middleware/getAuthUser"; @@ -39,4 +40,11 @@ course.post("/new", async (req: RequestWUser, res, next) => { } }); +course.get("/me", async (req: RequestWUser, res, next) => { + const { user } = req; + + const courses = await getUserCourses(user); + res.send(courses); +}); + export default course; From 938e137fcafa17e9ae74df7a3c29a5584d8c7f6d Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 14:13:19 -0700 Subject: [PATCH 045/355] Showing cards in react --- .../components/Dashboard/DashboardIndex.tsx | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index 7e141ce..ff0defa 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -1,10 +1,33 @@ import { Button, Container, Flex, Grid, Heading } from "@chakra-ui/react"; import React from "react"; +import { useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; +import { getConfig, useSession } from "../../utils/auth"; import CourseCard from "./CourseCard"; +import axios from "axios"; +import { baseURL } from "../../utils/constants"; +import { ICourse } from "../../types/course"; const DashboardIndex = () => { const navigate = useNavigate(); + const { isFetching, user } = useSession(); + + const { data: courses, isLoading } = useQuery( + "courses", + async () => { + if (!user) throw new Error(); + + const res = await axios.get( + `${baseURL}/course/me`, + getConfig(user?.sessionToken) + ); + + return res.data; + }, + { + enabled: !isFetching, + } + ); return ( @@ -20,27 +43,17 @@ const DashboardIndex = () => { gap="1em" mt={10} > - - - + {courses && + courses.map((course) => ( + + ))} From 9e9ddf2e6bfdae79b1a2b5d537566062979d1d23 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 14:15:20 -0700 Subject: [PATCH 046/355] Adding objectId as link between pages --- frontend/src/components/Dashboard/CourseCard.tsx | 4 +++- frontend/src/components/Dashboard/DashboardIndex.tsx | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index 05637bc..f653c2f 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -18,6 +18,7 @@ import Popover from "../Popover/Popover"; import Tooltip from "../Popover/Tooltip"; interface Props { + link: string; title: string; src: string; beginnerProgress: number; @@ -27,6 +28,7 @@ interface Props { const CourseCard = ({ title, + link, src, beginnerProgress, intermediateProgress, @@ -63,7 +65,7 @@ const CourseCard = ({ {title} - + diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index ff0defa..22687c6 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -12,7 +12,7 @@ const DashboardIndex = () => { const navigate = useNavigate(); const { isFetching, user } = useSession(); - const { data: courses, isLoading } = useQuery( + const { data: courses } = useQuery( "courses", async () => { if (!user) throw new Error(); @@ -47,6 +47,7 @@ const DashboardIndex = () => { courses.map((course) => ( Date: Thu, 14 Jul 2022 14:29:45 -0700 Subject: [PATCH 047/355] Adding link between category and --- .../src/components/Dashboard/CourseCard.tsx | 9 +++++++-- .../components/Difficulty/DifficultyCard.tsx | 4 +++- .../components/Difficulty/DifficultyIndex.tsx | 18 ++++++++++-------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index f653c2f..3a9620b 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -12,7 +12,7 @@ import { MenuList, } from "@chakra-ui/react"; import React from "react"; -import { Link } from "react-router-dom"; +import { createSearchParams, Link } from "react-router-dom"; import useThemeColor from "../../hooks/useThemeColor"; import Popover from "../Popover/Popover"; import Tooltip from "../Popover/Tooltip"; @@ -65,7 +65,12 @@ const CourseCard = ({ {title} - + diff --git a/frontend/src/components/Difficulty/DifficultyCard.tsx b/frontend/src/components/Difficulty/DifficultyCard.tsx index 159cf0c..61d89eb 100644 --- a/frontend/src/components/Difficulty/DifficultyCard.tsx +++ b/frontend/src/components/Difficulty/DifficultyCard.tsx @@ -18,6 +18,7 @@ interface Props { title: string; progress: number; src: string; + courseId: string; started?: boolean; phrase: string; } @@ -25,6 +26,7 @@ interface Props { const DifficultyCard = ({ title, progress, + courseId, src, started = false, phrase, @@ -71,7 +73,7 @@ const DifficultyCard = ({ scrollWithOffset(el)} > diff --git a/frontend/src/components/Difficulty/DifficultyIndex.tsx b/frontend/src/components/Difficulty/DifficultyIndex.tsx index b0fd7d7..d363306 100644 --- a/frontend/src/components/Difficulty/DifficultyIndex.tsx +++ b/frontend/src/components/Difficulty/DifficultyIndex.tsx @@ -1,12 +1,19 @@ import { Box, Heading } from "@chakra-ui/react"; import React from "react"; +import { useQuery } from "react-query"; +import { useParams, useSearchParams } from "react-router-dom"; +import { ICourse } from "../../types/course"; import DifficultyCard from "./DifficultyCard"; const DifficultyIndex = () => { + const [searchParams] = useSearchParams(); + const { id } = useParams(); + const name = searchParams.get("name"); + return ( <> - React + {name} { progress={20} src="https://images.unsplash.com/photo-1633356122102-3fe601e05bd2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80" phrase="You can do it!" - started - /> - From c708c75e1f9067730782addcaf56f67a7b48ed0a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 14:36:34 -0700 Subject: [PATCH 048/355] Adding resources route --- backend/app.ts | 2 ++ backend/src/routes/resources.ts | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 backend/src/routes/resources.ts diff --git a/backend/app.ts b/backend/app.ts index 6822f45..dac2070 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -7,6 +7,7 @@ import { NotFoundError } from "./src/utils/errors"; import debug from "./src/routes/debug"; import cors from "cors"; import course from "./src/routes/course"; +import resources from "./src/routes/resources"; dotenv.config(); @@ -21,6 +22,7 @@ app.use(cors()); app.use("/auth", auth); app.use("/debug", debug); app.use("/course", course); +app.use("/resources", resources); app.get("/", async (req, res) => { const testObject = new Parse.Object("test"); diff --git a/backend/src/routes/resources.ts b/backend/src/routes/resources.ts new file mode 100644 index 0000000..3af1c5a --- /dev/null +++ b/backend/src/routes/resources.ts @@ -0,0 +1,5 @@ +import express from "express"; + +const resources = express.Router(); + +export default resources; From e787f69d38094bd68a67d838ac5034b8dcfa474a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 14:39:38 -0700 Subject: [PATCH 049/355] Creater resources.ts and moved some functions there --- backend/src/course/course.ts | 11 +---------- backend/src/resources/resources.ts | 26 ++++++++++++++++++++++++++ backend/src/routes/course.ts | 1 - 3 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 backend/src/resources/resources.ts diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index 0702168..dd7ad42 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -3,6 +3,7 @@ import { getVideoDetailByIds, getVideosByQuery } from "../rating/youtube"; import Parse from "parse/node"; import { IResource } from "../types/resource"; import { IWeightedYoutubeVideo } from "../types/youtube"; +import { createResource } from "../resources/resources"; const VIDEOS_PER_QUERY = 100; @@ -27,16 +28,6 @@ export const createCourse = ( return Course; }; -export const createResource = (resource: IResource) => { - const Resource: Parse.Object = new Parse.Object("Resource"); - - Object.keys(resource).forEach((resourceAttribute) => { - Resource.set(resourceAttribute, resource[resourceAttribute]); - }); - - return Resource; -}; - export const saveResources = async ( resources: IWeightedYoutubeVideo[], course: Parse.Object, diff --git a/backend/src/resources/resources.ts b/backend/src/resources/resources.ts new file mode 100644 index 0000000..3a179c3 --- /dev/null +++ b/backend/src/resources/resources.ts @@ -0,0 +1,26 @@ +import { IResource } from "../types/resource"; + +export const createResource = (resource: IResource) => { + const Resource: Parse.Object = new Parse.Object("Resource"); + + Object.keys(resource).forEach((resourceAttribute) => { + Resource.set(resourceAttribute, resource[resourceAttribute]); + }); + + return Resource; +}; + +export const getResourcesFromCourse = async (courseId: string) => { + const Resource = Parse.Object.extend("Resource"); + const Course = Parse.Object.extend("Course"); + + const query = new Parse.Query(Resource); + + const course = new Course(); + course.id = courseId; + + query.equalTo("course", course); + + const resources = await query.findAll(); + return resources; +}; diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 2e7a4bc..22f9d94 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -1,7 +1,6 @@ import express from "express"; import { createCourse, - createResource, generateResources, getUserCourses, saveResources, From 5d6fd4e7aac90bc091f6ac0e3749fbaf423833a9 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 14 Jul 2022 14:43:48 -0700 Subject: [PATCH 050/355] adding get resources by courseId --- backend/src/resources/resources.ts | 1 + backend/src/routes/resources.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/backend/src/resources/resources.ts b/backend/src/resources/resources.ts index 3a179c3..69600b4 100644 --- a/backend/src/resources/resources.ts +++ b/backend/src/resources/resources.ts @@ -1,4 +1,5 @@ import { IResource } from "../types/resource"; +import Parse from "parse/node"; export const createResource = (resource: IResource) => { const Resource: Parse.Object = new Parse.Object("Resource"); diff --git a/backend/src/routes/resources.ts b/backend/src/routes/resources.ts index 3af1c5a..9e0cdc5 100644 --- a/backend/src/routes/resources.ts +++ b/backend/src/routes/resources.ts @@ -1,5 +1,16 @@ import express from "express"; +import { getAuthUser } from "../middleware/getAuthUser"; +import { getResourcesFromCourse } from "../resources/resources"; +import { RequestWUser } from "../types/user"; const resources = express.Router(); +resources.use(getAuthUser); + +resources.get("/byCourse/:courseId", async (req: RequestWUser, res, next) => { + const { courseId } = req.params; + const courses = await getResourcesFromCourse(courseId); + res.send(courses); +}); + export default resources; From 538ddfcb9ee9a9837bf05dcaf6970a362f0428cd Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 09:48:24 -0700 Subject: [PATCH 051/355] adding objectId to resource type --- frontend/src/components/Hub/ResourceGroup.tsx | 31 +++++++------------ frontend/src/types/resource.d.ts | 2 ++ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/Hub/ResourceGroup.tsx b/frontend/src/components/Hub/ResourceGroup.tsx index 584d0da..2174b35 100644 --- a/frontend/src/components/Hub/ResourceGroup.tsx +++ b/frontend/src/components/Hub/ResourceGroup.tsx @@ -1,14 +1,16 @@ import { Box, Grid, Heading } from "@chakra-ui/react"; import React from "react"; +import { IResource } from "../../types/resource"; import DocsCard from "./DocsCard"; import VideoCard from "./VideoCard"; interface Props { title: string; kind: "video" | "documentation" | "both"; + data: IResource[]; } -const ResourceGroup = ({ title, kind }: Props) => { +const ResourceGroup = ({ title, kind, data }: Props) => { return ( @@ -19,28 +21,17 @@ const ResourceGroup = ({ title, kind }: Props) => { templateColumns={["1fr", "1fr", "repeat(2, 1fr)", "repeat(3, 1fr)"]} gap="1em" > - {kind === "video" && ( - <> + {kind === "video" && + data && + data.map((resource) => ( - - - - )} + ))} {kind === "documentation" && ( <> diff --git a/frontend/src/types/resource.d.ts b/frontend/src/types/resource.d.ts index e8b2772..3dae21e 100644 --- a/frontend/src/types/resource.d.ts +++ b/frontend/src/types/resource.d.ts @@ -3,6 +3,7 @@ import Parse from "parse/node"; export type IResource = | { + objectId: string; type: "video"; status: "not started" | "in progress" | "completed"; level: 1 | 2; @@ -15,6 +16,7 @@ export type IResource = course: Parse.Object; } | { + objectId: string; type: "website"; status: "not started" | "in progress" | "completed"; level: 1 | 2; From ce26e67cb318dad1c60965995b5cce023c7d514f Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 09:51:07 -0700 Subject: [PATCH 052/355] Adding fetching courses from database --- frontend/src/components/Hub/HubIndex.tsx | 42 +++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Hub/HubIndex.tsx b/frontend/src/components/Hub/HubIndex.tsx index 538875d..8651716 100644 --- a/frontend/src/components/Hub/HubIndex.tsx +++ b/frontend/src/components/Hub/HubIndex.tsx @@ -6,9 +6,37 @@ import { CircularProgressLabel, } from "@chakra-ui/react"; import React from "react"; +import { useQuery } from "react-query"; +import { useParams, useSearchParams } from "react-router-dom"; +import { getConfig, useSession } from "../../utils/auth"; import ResourceGroup from "./ResourceGroup"; +import axios from "axios"; +import { baseURL } from "../../utils/constants"; +import { IResource } from "../../types/resource"; const HubIndex = () => { + const { isFetching, user } = useSession(); + const [searchParams] = useSearchParams(); + + const { id } = useParams(); + const difficulty = searchParams.get("difficulty"); + + const { data } = useQuery( + `hub-${id}-${difficulty}`, + async () => { + if (!user) throw new Error("User is not defined"); + + const res = await axios.get( + `${baseURL}/resources/byCourse/${id}/${difficulty}`, + getConfig(user.sessionToken) + ); + return res.data; + }, + { + enabled: !isFetching && !!id && !!difficulty, + } + ); + return ( <> @@ -16,16 +44,22 @@ const HubIndex = () => { React - Beginner + {difficulty === "1" ? "Beginner" : "Advanced"} 30% - - - + {data && ( + <> + + + )} ); From b60de40a621191dda1aa559ffaa5a0fab5170b34 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 09:51:18 -0700 Subject: [PATCH 053/355] adding difficulty propr --- frontend/src/components/Difficulty/DifficultyCard.tsx | 5 ++++- frontend/src/components/Difficulty/DifficultyIndex.tsx | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Difficulty/DifficultyCard.tsx b/frontend/src/components/Difficulty/DifficultyCard.tsx index 61d89eb..597b76e 100644 --- a/frontend/src/components/Difficulty/DifficultyCard.tsx +++ b/frontend/src/components/Difficulty/DifficultyCard.tsx @@ -10,6 +10,7 @@ import { Text, } from "@chakra-ui/react"; import React from "react"; +import { createSearchParams } from "react-router-dom"; import { HashLink } from "react-router-hash-link"; import useThemeColor from "../../hooks/useThemeColor"; import { scrollWithOffset } from "../../utils/scrollWithOffset"; @@ -21,6 +22,7 @@ interface Props { courseId: string; started?: boolean; phrase: string; + difficulty: 1 | 2; } const DifficultyCard = ({ @@ -30,6 +32,7 @@ const DifficultyCard = ({ src, started = false, phrase, + difficulty, }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); @@ -73,7 +76,7 @@ const DifficultyCard = ({ scrollWithOffset(el)} > diff --git a/frontend/src/components/Difficulty/DifficultyIndex.tsx b/frontend/src/components/Difficulty/DifficultyIndex.tsx index d363306..bb9b6e5 100644 --- a/frontend/src/components/Difficulty/DifficultyIndex.tsx +++ b/frontend/src/components/Difficulty/DifficultyIndex.tsx @@ -23,6 +23,7 @@ const DifficultyIndex = () => { phrase="You can do it!" courseId={id || ""} started + difficulty={1} /> { src="https://images.unsplash.com/photo-1569748130764-3fed0c102c59?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80" courseId={id || ""} phrase="Let's go" + difficulty={2} /> From 6229ab45da8124fdf3ed3a4e77c95fb2eda3d3d1 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 09:51:33 -0700 Subject: [PATCH 054/355] Adding function to get resources from course and difficulty --- backend/src/resources/resources.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backend/src/resources/resources.ts b/backend/src/resources/resources.ts index 69600b4..a61956f 100644 --- a/backend/src/resources/resources.ts +++ b/backend/src/resources/resources.ts @@ -25,3 +25,22 @@ export const getResourcesFromCourse = async (courseId: string) => { const resources = await query.findAll(); return resources; }; + +export const getResourcesFromCourseAndDifficulty = async ( + courseId: string, + level: number +) => { + const Resource = Parse.Object.extend("Resource"); + const Course = Parse.Object.extend("Course"); + + const query = new Parse.Query(Resource); + + const course = new Course(); + course.id = courseId; + + query.equalTo("course", course); + query.equalTo("level", level); + + const resources = await query.findAll(); + return resources; +}; From be9fc621a9a03c26449c8b95964bf0991b505e6b Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 09:51:46 -0700 Subject: [PATCH 055/355] adding route to get resources from courses and difficulty --- backend/src/routes/resources.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/resources.ts b/backend/src/routes/resources.ts index 9e0cdc5..bc61704 100644 --- a/backend/src/routes/resources.ts +++ b/backend/src/routes/resources.ts @@ -1,16 +1,25 @@ import express from "express"; import { getAuthUser } from "../middleware/getAuthUser"; -import { getResourcesFromCourse } from "../resources/resources"; +import { + getResourcesFromCourse, + getResourcesFromCourseAndDifficulty, +} from "../resources/resources"; import { RequestWUser } from "../types/user"; const resources = express.Router(); resources.use(getAuthUser); -resources.get("/byCourse/:courseId", async (req: RequestWUser, res, next) => { - const { courseId } = req.params; - const courses = await getResourcesFromCourse(courseId); - res.send(courses); -}); +resources.get( + "/byCourse/:courseId/:level", + async (req: RequestWUser, res, next) => { + const { courseId, level } = req.params; + const courses = await getResourcesFromCourseAndDifficulty( + courseId, + parseInt(level) + ); + res.send(courses); + } +); export default resources; From 7317ee0bee98632569c5e35aa65a131621215e17 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 10:00:22 -0700 Subject: [PATCH 056/355] Adding loading card in dashboard --- .../components/Dashboard/DashboardIndex.tsx | 23 ++++++++++++++++--- .../src/components/Loading/LoadingCard.tsx | 13 +++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/Loading/LoadingCard.tsx diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index 22687c6..8b89395 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -1,4 +1,13 @@ -import { Button, Container, Flex, Grid, Heading } from "@chakra-ui/react"; +import { + Button, + Container, + Flex, + Grid, + Heading, + HStack, + Skeleton, + Stack, +} from "@chakra-ui/react"; import React from "react"; import { useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; @@ -7,6 +16,7 @@ import CourseCard from "./CourseCard"; import axios from "axios"; import { baseURL } from "../../utils/constants"; import { ICourse } from "../../types/course"; +import LoadingCard from "../Loading/LoadingCard"; const DashboardIndex = () => { const navigate = useNavigate(); @@ -43,7 +53,13 @@ const DashboardIndex = () => { gap="1em" mt={10} > - {courses && + {!courses ? ( + <> + + + + + ) : ( courses.map((course) => ( { intermediateProgress={40} advancedProgress={10} /> - ))} + )) + )} diff --git a/frontend/src/components/Loading/LoadingCard.tsx b/frontend/src/components/Loading/LoadingCard.tsx new file mode 100644 index 0000000..12c486d --- /dev/null +++ b/frontend/src/components/Loading/LoadingCard.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { Skeleton, Stack } from "@chakra-ui/react"; + +const LoadingCard = () => { + return ( + + + + + ); +}; + +export default LoadingCard; From 9bbb8e62b7c0eb0a0bc8c49995d6b5f7a53e9ada Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 10:14:29 -0700 Subject: [PATCH 057/355] Adding loading card to hub --- frontend/src/components/Hub/HubIndex.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Hub/HubIndex.tsx b/frontend/src/components/Hub/HubIndex.tsx index 8651716..714b782 100644 --- a/frontend/src/components/Hub/HubIndex.tsx +++ b/frontend/src/components/Hub/HubIndex.tsx @@ -4,6 +4,7 @@ import { Badge, CircularProgress, CircularProgressLabel, + HStack, } from "@chakra-ui/react"; import React from "react"; import { useQuery } from "react-query"; @@ -13,6 +14,7 @@ import ResourceGroup from "./ResourceGroup"; import axios from "axios"; import { baseURL } from "../../utils/constants"; import { IResource } from "../../types/resource"; +import LoadingCard from "../Loading/LoadingCard"; const HubIndex = () => { const { isFetching, user } = useSession(); @@ -20,6 +22,7 @@ const HubIndex = () => { const { id } = useParams(); const difficulty = searchParams.get("difficulty"); + const title = searchParams.get("title"); const { data } = useQuery( `hub-${id}-${difficulty}`, @@ -42,7 +45,7 @@ const HubIndex = () => { - React + {title} {difficulty === "1" ? "Beginner" : "Advanced"} @@ -51,7 +54,13 @@ const HubIndex = () => { - {data && ( + {!data ? ( + <> + + + + + ) : ( <> Date: Fri, 15 Jul 2022 10:14:36 -0700 Subject: [PATCH 058/355] adding course title to hub --- frontend/src/components/Difficulty/DifficultyCard.tsx | 4 +++- frontend/src/components/Difficulty/DifficultyIndex.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Difficulty/DifficultyCard.tsx b/frontend/src/components/Difficulty/DifficultyCard.tsx index 597b76e..1b38a8e 100644 --- a/frontend/src/components/Difficulty/DifficultyCard.tsx +++ b/frontend/src/components/Difficulty/DifficultyCard.tsx @@ -22,6 +22,7 @@ interface Props { courseId: string; started?: boolean; phrase: string; + courseTitle: string; difficulty: 1 | 2; } @@ -32,6 +33,7 @@ const DifficultyCard = ({ src, started = false, phrase, + courseTitle, difficulty, }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); @@ -76,7 +78,7 @@ const DifficultyCard = ({ scrollWithOffset(el)} > diff --git a/frontend/src/components/Difficulty/DifficultyIndex.tsx b/frontend/src/components/Difficulty/DifficultyIndex.tsx index bb9b6e5..ee0d0a0 100644 --- a/frontend/src/components/Difficulty/DifficultyIndex.tsx +++ b/frontend/src/components/Difficulty/DifficultyIndex.tsx @@ -1,8 +1,6 @@ import { Box, Heading } from "@chakra-ui/react"; import React from "react"; -import { useQuery } from "react-query"; import { useParams, useSearchParams } from "react-router-dom"; -import { ICourse } from "../../types/course"; import DifficultyCard from "./DifficultyCard"; const DifficultyIndex = () => { @@ -22,6 +20,7 @@ const DifficultyIndex = () => { src="https://images.unsplash.com/photo-1633356122102-3fe601e05bd2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80" phrase="You can do it!" courseId={id || ""} + courseTitle={name || ""} started difficulty={1} /> @@ -30,6 +29,7 @@ const DifficultyIndex = () => { progress={0} src="https://images.unsplash.com/photo-1569748130764-3fed0c102c59?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80" courseId={id || ""} + courseTitle={name || ""} phrase="Let's go" difficulty={2} /> From 4c4b26cf610ffbbbd598e1d2278cb85bd32d970b Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 11:09:15 -0700 Subject: [PATCH 059/355] adding types for unsplash image links --- frontend/src/types/course.d.ts | 5 ++++- frontend/src/types/image.d.ts | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 frontend/src/types/image.d.ts diff --git a/frontend/src/types/course.d.ts b/frontend/src/types/course.d.ts index 0053ba9..2116266 100644 --- a/frontend/src/types/course.d.ts +++ b/frontend/src/types/course.d.ts @@ -1,6 +1,9 @@ import { DbObject } from "./global"; +import { IUnsplashLinks } from "./image"; +import { IUser } from "./user"; export interface ICourse extends DbObject { name: string; - resources: any[]; + user: IUser; + images: IUnsplashLinks[]; } diff --git a/frontend/src/types/image.d.ts b/frontend/src/types/image.d.ts new file mode 100644 index 0000000..019b247 --- /dev/null +++ b/frontend/src/types/image.d.ts @@ -0,0 +1,7 @@ +export interface IUnsplashLinks { + raw: string; + full: string; + regular: string; + small: string; + thumb: string; +} From 88e145c1149f9a63a148398b4fa6c3bc35f706dc Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 11:09:25 -0700 Subject: [PATCH 060/355] installing axios in backend --- backend/package.json | 3 ++- backend/pnpm-lock.yaml | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 8085e1a..7f01bef 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,8 +13,9 @@ "author": "", "license": "ISC", "dependencies": { - "date-fns": "^2.28.0", + "axios": "^0.27.2", "cors": "^2.8.5", + "date-fns": "^2.28.0", "dotenv": "^16.0.1", "express": "^4.18.1", "googleapis": "^105.0.0", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 692d006..313c34d 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -9,6 +9,7 @@ specifiers: '@types/node': ^18.0.0 '@types/nodemailer': ^6.4.4 '@types/parse': ^2.18.16 + axios: ^0.27.2 babel-cli: ^6.26.0 cors: ^2.8.5 date-fns: ^2.28.0 @@ -24,6 +25,7 @@ specifiers: typescript: ^4.7.4 dependencies: + axios: 0.27.2 cors: 2.8.5 date-fns: 2.28.0 dotenv: 16.0.1 @@ -1897,6 +1899,10 @@ packages: dev: true optional: true + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + /atob/2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} @@ -1904,6 +1910,15 @@ packages: dev: true optional: true + /axios/0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.1 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: false + /babel-cli/6.26.0: resolution: {integrity: sha512-wau+BDtQfuSBGQ9PzzFL3REvR9Sxnd4LKwtcHAiPjhugA7K/80vpHXafj+O5bAqJOuSefjOx5ZJnNSR2J1Qw6Q==} hasBin: true @@ -2536,6 +2551,13 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + /commander/2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true @@ -2750,6 +2772,11 @@ packages: dev: true optional: true + /delayed-stream/1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + /depd/2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -3108,6 +3135,16 @@ packages: path-exists: 4.0.0 dev: true + /follow-redirects/1.15.1: + resolution: {integrity: sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-in/1.0.2: resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} engines: {node: '>=0.10.0'} @@ -3122,6 +3159,15 @@ packages: dev: true optional: true + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /forwarded/0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} From c989b56289d71f4289a40acfff13cd15247e7c32 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 11:35:11 -0700 Subject: [PATCH 061/355] adding calculate course completition function --- frontend/src/utils/courseCompletition.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 frontend/src/utils/courseCompletition.ts diff --git a/frontend/src/utils/courseCompletition.ts b/frontend/src/utils/courseCompletition.ts new file mode 100644 index 0000000..f42721c --- /dev/null +++ b/frontend/src/utils/courseCompletition.ts @@ -0,0 +1,9 @@ +import { IResource } from "../types/resource"; + +export const calculateCourseCompletition = (resources: IResource[]) => { + let totalCompleted = 0; + for (const resource of resources) { + if (resource.status === "completed") totalCompleted++; + } + return totalCompleted / resources.length; +}; From b1bdc136a16a0ce18b0c9f73ca8a9a1d55a9d03a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 11:37:25 -0700 Subject: [PATCH 062/355] adding image fetching in layout component --- .../src/components/Layouts/CourseLayout.tsx | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Layouts/CourseLayout.tsx b/frontend/src/components/Layouts/CourseLayout.tsx index 3225ad3..4c399d6 100644 --- a/frontend/src/components/Layouts/CourseLayout.tsx +++ b/frontend/src/components/Layouts/CourseLayout.tsx @@ -1,12 +1,41 @@ -import { Container, Heading } from "@chakra-ui/react"; +import { Container, Heading, Skeleton } from "@chakra-ui/react"; import React from "react"; -import { Outlet } from "react-router-dom"; +import { Outlet, useParams } from "react-router-dom"; +import { getConfig, useSession } from "../../utils/auth"; import Banner from "../Hub/Banner"; +import { useQuery } from "react-query"; +import { baseURL } from "../../utils/constants"; +import axios from "axios"; +import { ICourse } from "../../types/course"; const CourseLayout = () => { + const { id } = useParams(); + const { user, isFetching } = useSession(); + + const { data, isLoading } = useQuery( + `${id}`, + async () => { + if (!user) throw new Error("User is not defiend"); + + const res = await axios.get( + `${baseURL}/course/${id}`, + getConfig(user.sessionToken) + ); + return res.data; + }, + { enabled: !!user && !!id } + ); + return ( <> - + {isLoading ? ( + + ) : ( + + )} From 52393b3b87d1adf76405de2d09e3071b0712054a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 11:37:44 -0700 Subject: [PATCH 063/355] adding get course by user and id function and route --- backend/src/course/course.ts | 21 ++++++++++++++++++++- backend/src/routes/course.ts | 11 ++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index dd7ad42..9e23cd6 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -4,6 +4,7 @@ import Parse from "parse/node"; import { IResource } from "../types/resource"; import { IWeightedYoutubeVideo } from "../types/youtube"; import { createResource } from "../resources/resources"; +import { getImagesByQuery } from "../unsplash/unsplash"; const VIDEOS_PER_QUERY = 100; @@ -17,13 +18,31 @@ export const getUserCourses = async (user: Parse.Object) => { return courses; }; +export const getCourseByUserAndId = async ( + user: Parse.Object, + courseId: string +) => { + const Course = Parse.Object.extend("Course"); + const query = new Parse.Query(Course); + + query.equalTo("user", user); + query.equalTo("objectId", courseId); -export const createCourse = ( + const course = await query.find(); + + return course[0]; +}; + +export const createCourse = async ( name: string, user: Parse.Object ) => { const Course: Parse.Object = new Parse.Object("Course"); + + const images = await getImagesByQuery(name); + Course.set("name", name); + Course.set("images", images); Course.set("user", user); return Course; }; diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 22f9d94..1e5bf3a 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -2,6 +2,7 @@ import express from "express"; import { createCourse, generateResources, + getCourseByUserAndId, getUserCourses, saveResources, } from "../course/course"; @@ -23,7 +24,7 @@ course.post("/new", async (req: RequestWUser, res, next) => { return; } - const course = createCourse(name, user); + const course = await createCourse(name, user); const { beginner, advanced } = await generateResources(name); try { @@ -46,4 +47,12 @@ course.get("/me", async (req: RequestWUser, res, next) => { res.send(courses); }); +course.get("/:courseId", async (req: RequestWUser, res, next) => { + const { user } = req; + const { courseId } = req.params; + + const course = await getCourseByUserAndId(user, courseId); + res.send(course); +}); + export default course; From 2dd99e9fdfdef0ca3cc0326b20ca1f98ab11e226 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 11:38:02 -0700 Subject: [PATCH 064/355] adding get images by query function --- backend/src/unsplash/unsplash.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 backend/src/unsplash/unsplash.ts diff --git a/backend/src/unsplash/unsplash.ts b/backend/src/unsplash/unsplash.ts new file mode 100644 index 0000000..54953d1 --- /dev/null +++ b/backend/src/unsplash/unsplash.ts @@ -0,0 +1,23 @@ +import axios from "axios"; +import dotenv from "dotenv"; +import { IUnsplashLinks } from "../types/image"; + +dotenv.config(); + +const unsplashUrl = "https://api.unsplash.com"; + +export const getImagesByQuery = async (query: string) => { + try { + const images = await axios.get( + `${unsplashUrl}/search/photos?page=1&query=${encodeURIComponent( + query + )}&client_id=${process.env.UNSPLASH_ACCESS_KEY}` + ); + + return images.data.results.map((image) => { + return { ...image.urls }; + }) as Array; + } catch (err) { + console.log(err); + } +}; From 2a337051db143a8e3512becc3ac618b7e0bd470a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 11:42:22 -0700 Subject: [PATCH 065/355] adding course fetching in --- .../components/Difficulty/DifficultyIndex.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Difficulty/DifficultyIndex.tsx b/frontend/src/components/Difficulty/DifficultyIndex.tsx index ee0d0a0..6c498b9 100644 --- a/frontend/src/components/Difficulty/DifficultyIndex.tsx +++ b/frontend/src/components/Difficulty/DifficultyIndex.tsx @@ -1,13 +1,34 @@ import { Box, Heading } from "@chakra-ui/react"; import React from "react"; import { useParams, useSearchParams } from "react-router-dom"; +import { ICourse } from "../../types/course"; +import { useSession, getConfig } from "../../utils/auth"; +import { baseURL } from "../../utils/constants"; import DifficultyCard from "./DifficultyCard"; +import { useQuery } from "react-query"; +import axios from "axios"; const DifficultyIndex = () => { const [searchParams] = useSearchParams(); const { id } = useParams(); const name = searchParams.get("name"); + const { user, isFetching } = useSession(); + + const { data, isLoading } = useQuery( + `${id}`, + async () => { + if (!user) throw new Error("User is not defiend"); + + const res = await axios.get( + `${baseURL}/course/${id}`, + getConfig(user.sessionToken) + ); + return res.data; + }, + { enabled: !!user && !!id } + ); + return ( <> @@ -17,7 +38,7 @@ const DifficultyIndex = () => { { Date: Fri, 15 Jul 2022 13:55:44 -0700 Subject: [PATCH 066/355] changing route params so the layout is consistent --- frontend/src/App.tsx | 6 +++--- frontend/src/components/Dashboard/CourseCard.tsx | 4 ++-- frontend/src/components/Difficulty/DifficultyCard.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c95dbcb..81130f2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,15 +32,15 @@ function App() { } /> } > - } /> - } /> + } /> + } /> diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index 3a9620b..af785c2 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -54,7 +54,7 @@ const CourseCard = ({ /> } > - + diff --git a/frontend/src/components/Difficulty/DifficultyCard.tsx b/frontend/src/components/Difficulty/DifficultyCard.tsx index 1b38a8e..03a147f 100644 --- a/frontend/src/components/Difficulty/DifficultyCard.tsx +++ b/frontend/src/components/Difficulty/DifficultyCard.tsx @@ -78,7 +78,7 @@ const DifficultyCard = ({ scrollWithOffset(el)} > From e17fc6ae9c97c90d61dce6a34538347c1ee3ae4c Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 13:56:09 -0700 Subject: [PATCH 067/355] Making the image redirect to the actual link --- frontend/src/components/Hub/VideoCard.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Hub/VideoCard.tsx b/frontend/src/components/Hub/VideoCard.tsx index 9a14c5e..f87d430 100644 --- a/frontend/src/components/Hub/VideoCard.tsx +++ b/frontend/src/components/Hub/VideoCard.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Badge, Box, Icon, Image, Text } from "@chakra-ui/react"; import { BiVideo } from "react-icons/bi"; import useThemeColor from "../../hooks/useThemeColor"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; type ResourceStatus = "completed" | "in progress" | "not started"; interface Props { @@ -31,11 +31,12 @@ const VideoCard = ({ src, title, href, status }: Props) => { borderColor={borderColor} borderWidth={1} borderRadius={4} - cursor="pointer" transition={"all 0.3s"} - onClick={() => navigate(href)} > - + + + + Date: Fri, 15 Jul 2022 13:56:56 -0700 Subject: [PATCH 068/355] Displaying images fetch from backend --- backend/src/types/image.d.ts | 7 +++++++ frontend/src/components/Dashboard/DashboardIndex.tsx | 2 +- frontend/src/components/Hub/Banner.tsx | 5 +++-- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 backend/src/types/image.d.ts diff --git a/backend/src/types/image.d.ts b/backend/src/types/image.d.ts new file mode 100644 index 0000000..019b247 --- /dev/null +++ b/backend/src/types/image.d.ts @@ -0,0 +1,7 @@ +export interface IUnsplashLinks { + raw: string; + full: string; + regular: string; + small: string; + thumb: string; +} diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index 8b89395..35dd8c6 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -65,7 +65,7 @@ const DashboardIndex = () => { key={course.objectId} link={course.objectId} title={course.name} - src="https://assets-global.website-files.com/61a0a53beeb118af7ddb4c55/61c0ba0267c18ebf1fd19b2f_maxresdefault-1-1-1024x576.jpeg" + src={course.images[2].regular} beginnerProgress={100} intermediateProgress={40} advancedProgress={10} diff --git a/frontend/src/components/Hub/Banner.tsx b/frontend/src/components/Hub/Banner.tsx index 7b19c9e..bbab24a 100644 --- a/frontend/src/components/Hub/Banner.tsx +++ b/frontend/src/components/Hub/Banner.tsx @@ -3,8 +3,9 @@ import React from "react"; interface Props { src: string; + fallback?: string; } -const Banner = ({ src }: Props) => { +const Banner = ({ src, fallback = "" }: Props) => { const linearGradientColor = useColorModeValue( "linear-gradient(to bottom, rgba(255,0,0,0), rgba(255,255,255,1))", "linear-gradient(to bottom, rgba(255,0,0,0), rgba(26,32,44,1))" @@ -12,7 +13,7 @@ const Banner = ({ src }: Props) => { return ( <> - + {/* Makes the gradient effect */} Date: Fri, 15 Jul 2022 13:58:30 -0700 Subject: [PATCH 069/355] Showing course completition --- frontend/src/components/Hub/HubIndex.tsx | 21 ++++++++++++++++--- frontend/src/components/Hub/ResourceGroup.tsx | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Hub/HubIndex.tsx b/frontend/src/components/Hub/HubIndex.tsx index 714b782..879688e 100644 --- a/frontend/src/components/Hub/HubIndex.tsx +++ b/frontend/src/components/Hub/HubIndex.tsx @@ -6,7 +6,7 @@ import { CircularProgressLabel, HStack, } from "@chakra-ui/react"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { useQuery } from "react-query"; import { useParams, useSearchParams } from "react-router-dom"; import { getConfig, useSession } from "../../utils/auth"; @@ -15,6 +15,7 @@ import axios from "axios"; import { baseURL } from "../../utils/constants"; import { IResource } from "../../types/resource"; import LoadingCard from "../Loading/LoadingCard"; +import { calculateCourseCompletition } from "../../utils/courseCompletition"; const HubIndex = () => { const { isFetching, user } = useSession(); @@ -40,6 +41,14 @@ const HubIndex = () => { } ); + const [completition, setCompletition] = useState(0); + + useEffect(() => { + if (!data) return; + const courseCompletition = calculateCourseCompletition(data); + setCompletition(courseCompletition); + }, [data]); + return ( <> @@ -49,8 +58,14 @@ const HubIndex = () => { {difficulty === "1" ? "Beginner" : "Advanced"} - - 30% + + + {completition.toFixed(0)}% + diff --git a/frontend/src/components/Hub/ResourceGroup.tsx b/frontend/src/components/Hub/ResourceGroup.tsx index 2174b35..fc8088f 100644 --- a/frontend/src/components/Hub/ResourceGroup.tsx +++ b/frontend/src/components/Hub/ResourceGroup.tsx @@ -28,7 +28,7 @@ const ResourceGroup = ({ title, kind, data }: Props) => { key={resource.objectId} title={resource.title} src={resource.type === "video" ? resource.thumbnail : ""} - status="completed" + status={resource.status} href={resource.url} /> ))} From ace4330ccd47996fd14f181e4d6bc6e74be8d382 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 14:05:41 -0700 Subject: [PATCH 070/355] Making images taking a fixed about of space --- frontend/src/components/Dashboard/CourseCard.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index af785c2..fd8ad38 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -54,7 +54,15 @@ const CourseCard = ({ /> } > - + + + Date: Fri, 15 Jul 2022 14:05:47 -0700 Subject: [PATCH 071/355] Adding margin to the bottom ofg the page --- frontend/src/components/Difficulty/DifficultyIndex.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Difficulty/DifficultyIndex.tsx b/frontend/src/components/Difficulty/DifficultyIndex.tsx index 6c498b9..2cf5d0c 100644 --- a/frontend/src/components/Difficulty/DifficultyIndex.tsx +++ b/frontend/src/components/Difficulty/DifficultyIndex.tsx @@ -34,7 +34,7 @@ const DifficultyIndex = () => { {name} - + Date: Fri, 15 Jul 2022 14:12:35 -0700 Subject: [PATCH 072/355] getting the highest quality thumbnail when generating course --- backend/src/course/course.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index 9e23cd6..1918c76 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -61,7 +61,7 @@ export const saveResources = async ( title: resource.snippet.title, description: resource.snippet.description, url: `https://youtube.com/video/${resource.id}`, - thumbnail: resource.snippet.thumbnails.default.url, + thumbnail: resource.snippet.thumbnails.high.url, channel: resource.snippet.channelTitle, feedback: 0, course, From 6dc521da33bcbef22999786be4346510ba0840f9 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 14:14:30 -0700 Subject: [PATCH 073/355] Making the dashboard navbar link actually work --- frontend/src/components/Navbar.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index dd7827e..4d09398 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -10,18 +10,23 @@ import { import { MoonIcon, SunIcon } from "@chakra-ui/icons"; import useThemeColor from "../hooks/useThemeColor"; import { deleteCachedUser, useSession } from "../utils/auth"; +import { useNavigate } from "react-router-dom"; const Navbar = () => { const { toggleColorMode, colorMode } = useColorMode(); const colorToggleIcon = colorMode === "dark" ? : ; + const { backgroundColor, borderColor } = useThemeColor(); const { user } = useSession(); + const navigate = useNavigate(); + const handleLogout = () => { deleteCachedUser(); window.location.reload(); }; + return ( { Learning U - - - + From 04c06ab3d0adb2579a9ed05c681cc0122da08f69 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 14:15:11 -0700 Subject: [PATCH 074/355] Removing intermediate from course popover --- frontend/src/components/Popover/Popover.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frontend/src/components/Popover/Popover.tsx b/frontend/src/components/Popover/Popover.tsx index c56f864..0334ec5 100644 --- a/frontend/src/components/Popover/Popover.tsx +++ b/frontend/src/components/Popover/Popover.tsx @@ -43,14 +43,6 @@ const Popover = ({ Beginner - - Intermediate - - Advanced From af3601a4ec67833fc00b64dba0e38c8dc829d6e1 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 14:22:00 -0700 Subject: [PATCH 075/355] Making thumbnail style look better --- frontend/src/components/Hub/VideoCard.tsx | 12 +++++++++--- frontend/src/components/NewCourse/NewCourseIndex.tsx | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Hub/VideoCard.tsx b/frontend/src/components/Hub/VideoCard.tsx index f87d430..e917f31 100644 --- a/frontend/src/components/Hub/VideoCard.tsx +++ b/frontend/src/components/Hub/VideoCard.tsx @@ -21,7 +21,6 @@ const getBadgeColor = (status: ResourceStatus) => { const VideoCard = ({ src, title, href, status }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); - const navigate = useNavigate(); const badgeColor = getBadgeColor(status); @@ -34,9 +33,16 @@ const VideoCard = ({ src, title, href, status }: Props) => { transition={"all 0.3s"} > - + + + - { description: "Your custom course was created successfully!", status: "success", }); - navigate(`/course/difficulty/${course.objectId}`); + navigate(`/courses/${course.objectId}/difficulty?name=${course.name}`); }, onError: (error: AxiosError) => { toast({ From 07972051b16839481a2603307055ab54a047f460 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 14:49:34 -0700 Subject: [PATCH 076/355] Adding tooltip to video component to show full video title --- frontend/src/components/Hub/VideoCard.tsx | 36 ++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/Hub/VideoCard.tsx b/frontend/src/components/Hub/VideoCard.tsx index e917f31..81e0837 100644 --- a/frontend/src/components/Hub/VideoCard.tsx +++ b/frontend/src/components/Hub/VideoCard.tsx @@ -3,6 +3,7 @@ import { Badge, Box, Icon, Image, Text } from "@chakra-ui/react"; import { BiVideo } from "react-icons/bi"; import useThemeColor from "../../hooks/useThemeColor"; import { Link, useNavigate } from "react-router-dom"; +import Tooltip from "../Popover/Tooltip"; type ResourceStatus = "completed" | "in progress" | "not started"; interface Props { @@ -33,15 +34,30 @@ const VideoCard = ({ src, title, href, status }: Props) => { transition={"all 0.3s"} > - - - + + {title} + + } + > + + + + { > - {title} + {title} {status} From 10aaf43e3046da8ad9fe094b4a17a780a0e869f4 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 15:03:20 -0700 Subject: [PATCH 077/355] adding resource status type --- backend/src/types/resource.d.ts | 2 ++ frontend/src/types/resource.d.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/backend/src/types/resource.d.ts b/backend/src/types/resource.d.ts index 7e9cf16..c8ae795 100644 --- a/backend/src/types/resource.d.ts +++ b/backend/src/types/resource.d.ts @@ -1,6 +1,8 @@ import { ICourse } from "./course"; import Parse from "parse/node"; +export type IResourceStatus = "not started" | "in progress" | "completed"; + export type IResource = | { type: "video"; diff --git a/frontend/src/types/resource.d.ts b/frontend/src/types/resource.d.ts index 3dae21e..bb764ae 100644 --- a/frontend/src/types/resource.d.ts +++ b/frontend/src/types/resource.d.ts @@ -1,6 +1,8 @@ import { ICourse } from "./course"; import Parse from "parse/node"; +export type IResourceStatus = "not started" | "in progress" | "completed"; + export type IResource = | { objectId: string; From 7e2653b5dd264e50c6fded8be8a84d7c94a0f477 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 15:09:43 -0700 Subject: [PATCH 078/355] adding update stauts route and function --- backend/src/resources/resources.ts | 13 ++++++++++++- backend/src/routes/resources.ts | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/backend/src/resources/resources.ts b/backend/src/resources/resources.ts index a61956f..0c4d783 100644 --- a/backend/src/resources/resources.ts +++ b/backend/src/resources/resources.ts @@ -1,4 +1,4 @@ -import { IResource } from "../types/resource"; +import { IResource, IResourceStatus } from "../types/resource"; import Parse from "parse/node"; export const createResource = (resource: IResource) => { @@ -44,3 +44,14 @@ export const getResourcesFromCourseAndDifficulty = async ( const resources = await query.findAll(); return resources; }; + +export const updateResourceStatus = async ( + resourceId: string, + status: IResourceStatus +) => { + const Resource = new Parse.Object("Resource"); + Resource.set("objectId", resourceId); + Resource.set("status", status); + + return Resource; +}; diff --git a/backend/src/routes/resources.ts b/backend/src/routes/resources.ts index bc61704..8f48de8 100644 --- a/backend/src/routes/resources.ts +++ b/backend/src/routes/resources.ts @@ -1,10 +1,13 @@ import express from "express"; +import { runInNewContext } from "vm"; import { getAuthUser } from "../middleware/getAuthUser"; import { getResourcesFromCourse, getResourcesFromCourseAndDifficulty, + updateResourceStatus, } from "../resources/resources"; import { RequestWUser } from "../types/user"; +import { BadRequestError } from "../utils/errors"; const resources = express.Router(); @@ -22,4 +25,22 @@ resources.get( } ); +resources.put( + "/updateStatus/:resourceId", + async (req: RequestWUser, res, next) => { + const { resourceId } = req.params; + const { status } = req.body; + + if (!status) { + next(new BadRequestError("Status is missing")); + return; + } + + const resource = await updateResourceStatus(resourceId, status); + const result = await resource.save(); + + res.send(result); + } +); + export default resources; From d3ae3302babdc8eaaa1210fde1837b9d0c139ac1 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 15:26:49 -0700 Subject: [PATCH 079/355] adding update course mutation --- frontend/src/components/Hub/VideoCard.tsx | 50 +++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Hub/VideoCard.tsx b/frontend/src/components/Hub/VideoCard.tsx index 81e0837..f081565 100644 --- a/frontend/src/components/Hub/VideoCard.tsx +++ b/frontend/src/components/Hub/VideoCard.tsx @@ -2,11 +2,17 @@ import React from "react"; import { Badge, Box, Icon, Image, Text } from "@chakra-ui/react"; import { BiVideo } from "react-icons/bi"; import useThemeColor from "../../hooks/useThemeColor"; -import { Link, useNavigate } from "react-router-dom"; import Tooltip from "../Popover/Tooltip"; +import { useMutation, useQueryClient } from "react-query"; +import axios from "axios"; +import { baseURL } from "../../utils/constants"; +import { IResourceStatus } from "../../types/resource"; +import { useParams, useSearchParams } from "react-router-dom"; +import { getConfig, useSession } from "../../utils/auth"; type ResourceStatus = "completed" | "in progress" | "not started"; interface Props { + objectId: string; src: string; title: string; href: string; @@ -20,10 +26,43 @@ const getBadgeColor = (status: ResourceStatus) => { return "gray"; }; -const VideoCard = ({ src, title, href, status }: Props) => { +const VideoCard = ({ src, title, href, status, objectId }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); const badgeColor = getBadgeColor(status); + const queryClient = useQueryClient(); + + const [searchParams] = useSearchParams(); + const { id } = useParams(); + const difficulty = searchParams.get("difficulty"); + + const { user } = useSession(); + + const updateVideoStatus = useMutation( + async (status: IResourceStatus) => { + if (!user) throw new Error("No user"); + const res = await axios.put( + `${baseURL}/resources/updateStatus/${objectId}`, + { status }, + getConfig(user?.sessionToken) + ); + return res.data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries(`hub-${id}-${difficulty}`); + }, + onError: () => { + alert("error"); + }, + } + ); + + const handleUpdateVideoStatus = () => { + if (status === "not started") { + updateVideoStatus.mutate("in progress"); + } + }; return ( { borderRadius={4} transition={"all 0.3s"} > - + handleUpdateVideoStatus()} + > Date: Fri, 15 Jul 2022 15:27:02 -0700 Subject: [PATCH 080/355] passing objectId as a prop to video card --- frontend/src/components/Hub/HubIndex.tsx | 2 +- frontend/src/components/Hub/ResourceGroup.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Hub/HubIndex.tsx b/frontend/src/components/Hub/HubIndex.tsx index 879688e..d7174ed 100644 --- a/frontend/src/components/Hub/HubIndex.tsx +++ b/frontend/src/components/Hub/HubIndex.tsx @@ -19,8 +19,8 @@ import { calculateCourseCompletition } from "../../utils/courseCompletition"; const HubIndex = () => { const { isFetching, user } = useSession(); - const [searchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); const { id } = useParams(); const difficulty = searchParams.get("difficulty"); const title = searchParams.get("title"); diff --git a/frontend/src/components/Hub/ResourceGroup.tsx b/frontend/src/components/Hub/ResourceGroup.tsx index fc8088f..1db8f40 100644 --- a/frontend/src/components/Hub/ResourceGroup.tsx +++ b/frontend/src/components/Hub/ResourceGroup.tsx @@ -25,6 +25,7 @@ const ResourceGroup = ({ title, kind, data }: Props) => { data && data.map((resource) => ( Date: Fri, 15 Jul 2022 15:30:11 -0700 Subject: [PATCH 081/355] changing courseCompletition formula so it takes inProgress into account --- frontend/src/utils/courseCompletition.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/utils/courseCompletition.ts b/frontend/src/utils/courseCompletition.ts index f42721c..626afd0 100644 --- a/frontend/src/utils/courseCompletition.ts +++ b/frontend/src/utils/courseCompletition.ts @@ -2,8 +2,12 @@ import { IResource } from "../types/resource"; export const calculateCourseCompletition = (resources: IResource[]) => { let totalCompleted = 0; + let totalInProgress = 0; + for (const resource of resources) { if (resource.status === "completed") totalCompleted++; + if (resource.status === "in progress") totalInProgress++; } - return totalCompleted / resources.length; + + return ((totalCompleted + 0.5 * totalInProgress) / resources.length) * 100; }; From 4dab2d070c569e5d3b25805319aade61c61a61cc Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 15:49:54 -0700 Subject: [PATCH 082/355] Adding space prop to tooltip component --- frontend/src/components/Popover/Tooltip.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Popover/Tooltip.tsx b/frontend/src/components/Popover/Tooltip.tsx index 6d81e94..3ccc666 100644 --- a/frontend/src/components/Popover/Tooltip.tsx +++ b/frontend/src/components/Popover/Tooltip.tsx @@ -5,18 +5,19 @@ import PopoverPortal from "./PopoverPortal"; interface Props { children: React.ReactElement; render: React.ReactElement; + space?: number; } -const getPoint = (element: HTMLElement, ref: HTMLElement) => { +const getPoint = (element: HTMLElement, ref: HTMLElement, space = 10) => { const pt = { x: 0, y: 0 }; const rectangle = element.getBoundingClientRect(); const refRectangle = ref.getBoundingClientRect(); pt.x = rectangle.left + (rectangle.width - refRectangle.width) / 2; - pt.y = rectangle.top - refRectangle.height + 10; + pt.y = rectangle.top - refRectangle.height + space; return pt; }; -const Tooltip = ({ children, render }: Props) => { +const Tooltip = ({ children, render, space = 10 }: Props) => { const [show, setShow] = useState(false); const posRef = useRef({ x: 0, y: 0 }); @@ -25,7 +26,7 @@ const Tooltip = ({ children, render }: Props) => { const handleMouseOver = (e: React.MouseEvent) => { setShow(true); if (!tooltipRef.current) return; - posRef.current = getPoint(e.currentTarget, tooltipRef.current); + posRef.current = getPoint(e.currentTarget, tooltipRef.current, space); }; const handleMouseOut = () => { From 63077726b479f85a86e9b37206c5d216dcd6fea3 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 15:50:12 -0700 Subject: [PATCH 083/355] Adding complete course interaction --- frontend/src/components/Hub/VideoCard.tsx | 34 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Hub/VideoCard.tsx b/frontend/src/components/Hub/VideoCard.tsx index f081565..4e05d7d 100644 --- a/frontend/src/components/Hub/VideoCard.tsx +++ b/frontend/src/components/Hub/VideoCard.tsx @@ -58,10 +58,8 @@ const VideoCard = ({ src, title, href, status, objectId }: Props) => { } ); - const handleUpdateVideoStatus = () => { - if (status === "not started") { - updateVideoStatus.mutate("in progress"); - } + const handleUpdateVideoStatus = (status: IResourceStatus) => { + updateVideoStatus.mutate(status); }; return ( @@ -76,7 +74,9 @@ const VideoCard = ({ src, title, href, status, objectId }: Props) => { href={href} target="_blank" rel="noreferrer" - onClick={() => handleUpdateVideoStatus()} + onClick={() => + status === "not started" && handleUpdateVideoStatus("in progress") + } > { {title} - {status} + + Mark as completed? + + } + > + handleUpdateVideoStatus("completed")} + > + {status} + + ); From cf1fb678b21bfc92e16b2ee7a68ff872e89070bb Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 15:59:24 -0700 Subject: [PATCH 084/355] Stopping showing tooltip if the course is already completed --- frontend/src/components/Hub/VideoCard.tsx | 28 ++++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Hub/VideoCard.tsx b/frontend/src/components/Hub/VideoCard.tsx index 4e05d7d..e516953 100644 --- a/frontend/src/components/Hub/VideoCard.tsx +++ b/frontend/src/components/Hub/VideoCard.tsx @@ -117,22 +117,28 @@ const VideoCard = ({ src, title, href, status, objectId }: Props) => { - Mark as completed? - + status !== "completed" ? ( + + Mark as completed? + + ) : ( + <> + ) } > handleUpdateVideoStatus("completed")} + onClick={() => + status !== "completed" && handleUpdateVideoStatus("completed") + } > {status} From bbc41d665de950bef7b441276e7cacd0d4bee472 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 15:59:33 -0700 Subject: [PATCH 085/355] Adding loading spinner --- frontend/src/components/Hub/HubIndex.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Hub/HubIndex.tsx b/frontend/src/components/Hub/HubIndex.tsx index d7174ed..eb48155 100644 --- a/frontend/src/components/Hub/HubIndex.tsx +++ b/frontend/src/components/Hub/HubIndex.tsx @@ -5,6 +5,7 @@ import { CircularProgress, CircularProgressLabel, HStack, + Spinner, } from "@chakra-ui/react"; import React, { useEffect, useState } from "react"; import { useQuery } from "react-query"; @@ -25,7 +26,11 @@ const HubIndex = () => { const difficulty = searchParams.get("difficulty"); const title = searchParams.get("title"); - const { data } = useQuery( + const { + data, + isFetching: isQueryFetching, + isLoading, + } = useQuery( `hub-${id}-${difficulty}`, async () => { if (!user) throw new Error("User is not defined"); @@ -54,7 +59,10 @@ const HubIndex = () => { - {title} + <> + {title} + {(isLoading || isQueryFetching) && } + {difficulty === "1" ? "Beginner" : "Advanced"} From 99a6e834d6fcb5d3850109981c96bfb1c89ae4e9 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 15 Jul 2022 16:02:16 -0700 Subject: [PATCH 086/355] fixing type import --- frontend/src/components/NewCourse/NewCourseIndex.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/NewCourse/NewCourseIndex.tsx b/frontend/src/components/NewCourse/NewCourseIndex.tsx index c50d40f..55f0db5 100644 --- a/frontend/src/components/NewCourse/NewCourseIndex.tsx +++ b/frontend/src/components/NewCourse/NewCourseIndex.tsx @@ -19,7 +19,7 @@ import axios, { AxiosError } from "axios"; import { baseURL } from "../../utils/constants"; import { ErrorType } from "../../types/requests"; import { useNavigate } from "react-router-dom"; -import { ICourse } from "../../../../types/course"; +import { ICourse } from "../../types/course"; import { getConfig, useSession } from "../../utils/auth"; interface LearnForm { From 3a308f9d7b2dd63d376e2ee7b3647ec585d40b94 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 09:52:12 -0700 Subject: [PATCH 087/355] adding getProgressByCourse endpoint --- backend/src/course/course.ts | 47 +++++++++++++++++++ backend/src/routes/course.ts | 11 +++++ .../components/Difficulty/DifficultyIndex.tsx | 15 ++++++ 3 files changed, 73 insertions(+) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index 1918c76..b540869 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -18,6 +18,7 @@ export const getUserCourses = async (user: Parse.Object) => { return courses; }; + export const getCourseByUserAndId = async ( user: Parse.Object, courseId: string @@ -33,6 +34,38 @@ export const getCourseByUserAndId = async ( return course[0]; }; +export const getProgressByCourse = async ( + user: Parse.Object, + courseId: string +) => { + const Resource = Parse.Object.extend("Resource"); + const Course = Parse.Object.extend("Course"); + + const query = new Parse.Query(Resource); + + const course = new Course(); + course.id = courseId; + + query.equalTo("user", user); + query.equalTo("course", course); + + const resources = await query.findAll(); + + // filter by level + const beginnerLevelResources = resources.filter( + (resource: any) => resource.level === 1 + ); + const advancedLevelResources = resources.filter( + (resource: any) => resource.level === 2 + ); + + console.log(beginnerLevelResources); + return { + 1: calculateCourseCompletition(beginnerLevelResources), + 2: calculateCourseCompletition(advancedLevelResources), + }; +}; + export const createCourse = async ( name: string, user: Parse.Object @@ -105,3 +138,17 @@ const getRankedVideos = async (query: string) => { return sortedRankedVideos; }; + +const calculateCourseCompletition = ( + resources: Parse.Object[] +) => { + let totalCompleted = 0; + let totalInProgress = 0; + + for (const resource of resources) { + if ((resource as any).status === "completed") totalCompleted++; + if ((resource as any).status === "in progress") totalInProgress++; + } + + return ((totalCompleted + 0.5 * totalInProgress) / resources.length) * 100; +}; diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 1e5bf3a..90eb17a 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -1,8 +1,11 @@ import express from "express"; +import { readdirSync } from "fs"; +import { reseller_v1 } from "googleapis"; import { createCourse, generateResources, getCourseByUserAndId, + getProgressByCourse, getUserCourses, saveResources, } from "../course/course"; @@ -55,4 +58,12 @@ course.get("/:courseId", async (req: RequestWUser, res, next) => { res.send(course); }); +course.get("/progress/:courseId", async (req: RequestWUser, res, next) => { + const { user } = req; + const { courseId } = req.params; + + const progress = await getProgressByCourse(user, courseId); + res.send(progress); +}); + export default course; diff --git a/frontend/src/components/Difficulty/DifficultyIndex.tsx b/frontend/src/components/Difficulty/DifficultyIndex.tsx index 2cf5d0c..916460d 100644 --- a/frontend/src/components/Difficulty/DifficultyIndex.tsx +++ b/frontend/src/components/Difficulty/DifficultyIndex.tsx @@ -29,12 +29,27 @@ const DifficultyIndex = () => { { enabled: !!user && !!id } ); + const { data: progress } = useQuery( + `progress-${id}`, + async () => { + if (!user) throw new Error("User is not defined"); + + const res = await axios.get<{ 1: number; 2: number }>( + `${baseURL}/course/progress/${id}`, + getConfig(user.sessionToken) + ); + return res.data; + }, + { enabled: !!user } + ); + return ( <> {name} +
{JSON.stringify(progress)}
Date: Mon, 18 Jul 2022 10:48:04 -0700 Subject: [PATCH 088/355] Added function to get actual data from parse object --- backend/src/course/course.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index b540869..ba615d8 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -53,13 +53,12 @@ export const getProgressByCourse = async ( // filter by level const beginnerLevelResources = resources.filter( - (resource: any) => resource.level === 1 + (resource) => resource.get("level") === 1 ); const advancedLevelResources = resources.filter( - (resource: any) => resource.level === 2 + (resource) => resource.get("level") === 2 ); - console.log(beginnerLevelResources); return { 1: calculateCourseCompletition(beginnerLevelResources), 2: calculateCourseCompletition(advancedLevelResources), @@ -146,8 +145,8 @@ const calculateCourseCompletition = ( let totalInProgress = 0; for (const resource of resources) { - if ((resource as any).status === "completed") totalCompleted++; - if ((resource as any).status === "in progress") totalInProgress++; + if (resource.get("status") === "completed") totalCompleted++; + if (resource.get("status") === "in progress") totalInProgress++; } return ((totalCompleted + 0.5 * totalInProgress) / resources.length) * 100; From 72e9bc4d1ee1b8c877eda1cd59aa9ffe99792ef3 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 10:54:51 -0700 Subject: [PATCH 089/355] Adding progress tracking inside difficulty page --- .../src/components/Difficulty/DifficultyCard.tsx | 14 +++++++++++--- .../src/components/Difficulty/DifficultyIndex.tsx | 5 ++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Difficulty/DifficultyCard.tsx b/frontend/src/components/Difficulty/DifficultyCard.tsx index 03a147f..a76525e 100644 --- a/frontend/src/components/Difficulty/DifficultyCard.tsx +++ b/frontend/src/components/Difficulty/DifficultyCard.tsx @@ -17,7 +17,7 @@ import { scrollWithOffset } from "../../utils/scrollWithOffset"; interface Props { title: string; - progress: number; + progress: number | undefined; src: string; courseId: string; started?: boolean; @@ -92,8 +92,16 @@ const DifficultyCard = ({
- - {progress}% + + {progress !== undefined && ( + + {progress.toFixed()}% + + )} diff --git a/frontend/src/components/Difficulty/DifficultyIndex.tsx b/frontend/src/components/Difficulty/DifficultyIndex.tsx index 916460d..820de39 100644 --- a/frontend/src/components/Difficulty/DifficultyIndex.tsx +++ b/frontend/src/components/Difficulty/DifficultyIndex.tsx @@ -49,10 +49,9 @@ const DifficultyIndex = () => { {name} -
{JSON.stringify(progress)}
{ /> Date: Mon, 18 Jul 2022 10:59:03 -0700 Subject: [PATCH 090/355] Passing ID as prop to dashboard tooltip popover --- frontend/src/components/Dashboard/CourseCard.tsx | 3 +-- frontend/src/components/Dashboard/DashboardIndex.tsx | 1 - frontend/src/components/Popover/Popover.tsx | 5 +++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index fd8ad38..7ee5e9c 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -31,7 +31,6 @@ const CourseCard = ({ link, src, beginnerProgress, - intermediateProgress, advancedProgress, }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); @@ -47,9 +46,9 @@ const CourseCard = ({ } diff --git a/frontend/src/components/Dashboard/DashboardIndex.tsx b/frontend/src/components/Dashboard/DashboardIndex.tsx index 35dd8c6..c215f4c 100644 --- a/frontend/src/components/Dashboard/DashboardIndex.tsx +++ b/frontend/src/components/Dashboard/DashboardIndex.tsx @@ -67,7 +67,6 @@ const DashboardIndex = () => { title={course.name} src={course.images[2].regular} beginnerProgress={100} - intermediateProgress={40} advancedProgress={10} /> )) diff --git a/frontend/src/components/Popover/Popover.tsx b/frontend/src/components/Popover/Popover.tsx index 0334ec5..5e74f8f 100644 --- a/frontend/src/components/Popover/Popover.tsx +++ b/frontend/src/components/Popover/Popover.tsx @@ -13,17 +13,18 @@ import useThemeColor from "../../hooks/useThemeColor"; interface Props { courseName: string; beginnerProgress: number; - intermediateProgress: number; advancedProgress: number; + id: string; } const Popover = ({ courseName, beginnerProgress, - intermediateProgress, advancedProgress, + id, }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); + return ( Date: Mon, 18 Jul 2022 11:04:01 -0700 Subject: [PATCH 091/355] Fetching course progress and showing it inside popover --- .../src/components/Dashboard/CourseCard.tsx | 22 +--------- .../components/Dashboard/DashboardIndex.tsx | 2 - frontend/src/components/Popover/Popover.tsx | 43 ++++++++++++++----- 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index 7ee5e9c..a85f801 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -21,18 +21,9 @@ interface Props { link: string; title: string; src: string; - beginnerProgress: number; - intermediateProgress: number; - advancedProgress: number; } -const CourseCard = ({ - title, - link, - src, - beginnerProgress, - advancedProgress, -}: Props) => { +const CourseCard = ({ title, link, src }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); return ( - - } - > + }> { link={course.objectId} title={course.name} src={course.images[2].regular} - beginnerProgress={100} - advancedProgress={10} /> )) )} diff --git a/frontend/src/components/Popover/Popover.tsx b/frontend/src/components/Popover/Popover.tsx index 5e74f8f..d5aa235 100644 --- a/frontend/src/components/Popover/Popover.tsx +++ b/frontend/src/components/Popover/Popover.tsx @@ -7,24 +7,37 @@ import { Flex, Heading, } from "@chakra-ui/react"; +import axios from "axios"; import React from "react"; import useThemeColor from "../../hooks/useThemeColor"; +import { getConfig, useSession } from "../../utils/auth"; +import { baseURL } from "../../utils/constants"; +import { useQuery } from "react-query"; interface Props { courseName: string; - beginnerProgress: number; - advancedProgress: number; id: string; } -const Popover = ({ - courseName, - beginnerProgress, - advancedProgress, - id, -}: Props) => { +const Popover = ({ courseName, id }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); + const { user } = useSession(); + + const { data: progress } = useQuery( + `progress-${id}`, + async () => { + if (!user) throw new Error("User is not defined"); + + const res = await axios.get<{ 1: number; 2: number }>( + `${baseURL}/course/progress/${id}`, + getConfig(user.sessionToken) + ); + return res.data; + }, + { enabled: !!user } + ); + return ( Beginner - + Advanced - + ); From ca0da67161bcad8c9a7ae101fcc32683b6bf8001 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 11:08:02 -0700 Subject: [PATCH 092/355] fixed a bug where advanced progress was the same as beginner progress --- frontend/src/components/Popover/Popover.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Popover/Popover.tsx b/frontend/src/components/Popover/Popover.tsx index d5aa235..b14977e 100644 --- a/frontend/src/components/Popover/Popover.tsx +++ b/frontend/src/components/Popover/Popover.tsx @@ -24,7 +24,7 @@ const Popover = ({ courseName, id }: Props) => { const { user } = useSession(); - const { data: progress } = useQuery( + const { data: progress, isFetching } = useQuery( `progress-${id}`, async () => { if (!user) throw new Error("User is not defined"); @@ -56,7 +56,7 @@ const Popover = ({ courseName, id }: Props) => { Beginner { Advanced From 0100bf648d4af3bd3b114e9c2a19b3bbf155e7f5 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 11:11:33 -0700 Subject: [PATCH 093/355] Adding feed route and dummy component --- frontend/src/App.tsx | 11 +++++++++++ frontend/src/components/Feed/FeedIndex.tsx | 7 +++++++ 2 files changed, 18 insertions(+) create mode 100644 frontend/src/components/Feed/FeedIndex.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 81130f2..91ee333 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import LoginIndex from "./components/Registration/LoginIndex"; import RegisterIndex from "./components/Registration/RegisterIndex"; import RequireAuth from "./components/Registration/RequireAuth"; import NewCourseIndex from "./components/NewCourse/NewCourseIndex"; +import FeedIndex from "./components/Feed/FeedIndex"; function App() { return ( @@ -42,6 +43,16 @@ function App() { } /> } /> + + + + } + > + } /> + ); diff --git a/frontend/src/components/Feed/FeedIndex.tsx b/frontend/src/components/Feed/FeedIndex.tsx new file mode 100644 index 0000000..f03b02a --- /dev/null +++ b/frontend/src/components/Feed/FeedIndex.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const FeedIndex = () => { + return
FeedIndex
; +}; + +export default FeedIndex; From 13f512bcc81a7447cc1144e8b813dbc88b68df8b Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 11:18:01 -0700 Subject: [PATCH 094/355] adding post route --- backend/app.ts | 2 ++ backend/src/routes/post.ts | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 backend/src/routes/post.ts diff --git a/backend/app.ts b/backend/app.ts index dac2070..22b92c2 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -8,6 +8,7 @@ import debug from "./src/routes/debug"; import cors from "cors"; import course from "./src/routes/course"; import resources from "./src/routes/resources"; +import post from "./src/routes/post"; dotenv.config(); @@ -23,6 +24,7 @@ app.use("/auth", auth); app.use("/debug", debug); app.use("/course", course); app.use("/resources", resources); +app.use("/post", post); app.get("/", async (req, res) => { const testObject = new Parse.Object("test"); diff --git a/backend/src/routes/post.ts b/backend/src/routes/post.ts new file mode 100644 index 0000000..c2f2022 --- /dev/null +++ b/backend/src/routes/post.ts @@ -0,0 +1,5 @@ +import express from "express"; + +const post = express.Router(); + +export default post; From 8302bb2f7e89a305be1c5e1c2bdfc677ef6ab6eb Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 11:23:23 -0700 Subject: [PATCH 095/355] adding createPost function --- backend/src/post/post.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 backend/src/post/post.ts diff --git a/backend/src/post/post.ts b/backend/src/post/post.ts new file mode 100644 index 0000000..9b4dac7 --- /dev/null +++ b/backend/src/post/post.ts @@ -0,0 +1,18 @@ +import Parse from "parse/node"; + +export const createPost = ( + content: string, + userId: string, + courseId?: string | undefined +) => { + const Post = new Parse.Object("Post"); + + Post.set("content", content); + Post.set("user", userId); + + if (courseId) { + Post.set("course", courseId); + } + + return Post; +}; From 4d805e89fdd06e3d65477e0db6ca895400c26791 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 11:27:36 -0700 Subject: [PATCH 096/355] changed userId to ParseUser --- backend/src/post/post.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/post/post.ts b/backend/src/post/post.ts index 9b4dac7..e18fb6b 100644 --- a/backend/src/post/post.ts +++ b/backend/src/post/post.ts @@ -2,13 +2,13 @@ import Parse from "parse/node"; export const createPost = ( content: string, - userId: string, + user: Parse.User, courseId?: string | undefined ) => { const Post = new Parse.Object("Post"); Post.set("content", content); - Post.set("user", userId); + Post.set("user", user); if (courseId) { Post.set("course", courseId); From ba3c2f914d9cad82bef710d962a4f315923f2e00 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 11:27:42 -0700 Subject: [PATCH 097/355] Adding create post route --- backend/src/routes/post.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/backend/src/routes/post.ts b/backend/src/routes/post.ts index c2f2022..ef9d881 100644 --- a/backend/src/routes/post.ts +++ b/backend/src/routes/post.ts @@ -1,5 +1,30 @@ import express from "express"; +import { getAuthUser } from "../middleware/getAuthUser"; +import { createPost } from "../post/post"; +import { RequestWUser } from "../types/user"; +import { BadRequestError } from "../utils/errors"; const post = express.Router(); +post.use(getAuthUser); + +post.post("/", async (req: RequestWUser, res, next) => { + const { user } = req; + const { content, courseId } = req.body; + + if (!content) { + next(new BadRequestError("Missing parameters")); + return; + } + + try { + const post = createPost(content, user, courseId); + const result = await post.save(); + res.status(201).send(result); + } catch (err) { + next(new BadRequestError(err)); + return; + } +}); + export default post; From bc73f998db3002fdced5b284b6ff8b344a876e2f Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 11:38:37 -0700 Subject: [PATCH 098/355] creating getPostsByUser function --- backend/src/post/post.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/src/post/post.ts b/backend/src/post/post.ts index e18fb6b..d9538ba 100644 --- a/backend/src/post/post.ts +++ b/backend/src/post/post.ts @@ -16,3 +16,17 @@ export const createPost = ( return Post; }; + +export const getPostsByUser = async (userId: string) => { + const Post = Parse.Object.extend("Post"); + const User = Parse.Object.extend("User"); + + const query = new Parse.Query(Post); + + const user = new User(); + user.id = userId; + + query.equalTo("user", user); + + return await query.findAll(); +}; From fd421e81de97d8ef106be21d94b8ec4d3185721c Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 11:39:08 -0700 Subject: [PATCH 099/355] creating get posts created by me --- backend/src/routes/post.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/src/routes/post.ts b/backend/src/routes/post.ts index ef9d881..317ac8e 100644 --- a/backend/src/routes/post.ts +++ b/backend/src/routes/post.ts @@ -1,6 +1,6 @@ import express from "express"; import { getAuthUser } from "../middleware/getAuthUser"; -import { createPost } from "../post/post"; +import { createPost, getPostsByUser } from "../post/post"; import { RequestWUser } from "../types/user"; import { BadRequestError } from "../utils/errors"; @@ -27,4 +27,11 @@ post.post("/", async (req: RequestWUser, res, next) => { } }); +post.get("/me", async (req: RequestWUser, res, next) => { + const { user } = req; + + const posts = await getPostsByUser(user.id); + res.send(posts); +}); + export default post; From 7925b6d440e3ccf9131dbb9586bf5d1200353971 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 13:35:27 -0700 Subject: [PATCH 100/355] Adding chakra ui modal component --- frontend/src/components/Share/Share.tsx | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 frontend/src/components/Share/Share.tsx diff --git a/frontend/src/components/Share/Share.tsx b/frontend/src/components/Share/Share.tsx new file mode 100644 index 0000000..a1364f3 --- /dev/null +++ b/frontend/src/components/Share/Share.tsx @@ -0,0 +1,29 @@ +import { + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, +} from "@chakra-ui/react"; +import React from "react"; + +interface Props { + isOpen: boolean; + onClose: () => void; +} + +const Share = ({ isOpen, onClose }: Props) => { + return ( + + + + Share this course to your friends! + + hola + + + ); +}; + +export default Share; From 7f8aff97bf8f14ea8ceb54d92f55b3e7eade8167 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 13:46:11 -0700 Subject: [PATCH 101/355] Adding clipboard hook to the modal --- frontend/src/components/Share/Share.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Share/Share.tsx b/frontend/src/components/Share/Share.tsx index a1364f3..4c35375 100644 --- a/frontend/src/components/Share/Share.tsx +++ b/frontend/src/components/Share/Share.tsx @@ -1,12 +1,16 @@ import { + Button, + Flex, + Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, + useClipboard, } from "@chakra-ui/react"; -import React from "react"; +import React, { useState } from "react"; interface Props { isOpen: boolean; @@ -14,13 +18,22 @@ interface Props { } const Share = ({ isOpen, onClose }: Props) => { + const [value, setValue] = useState("CODE"); + + const { onCopy, hasCopied } = useClipboard(value); + return ( Share this course to your friends! - hola + + + + + + ); From 2f8ff38a632fc996b327ebe560f9b6b0b1815302 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 13:46:19 -0700 Subject: [PATCH 102/355] Adding modal inside course card --- frontend/src/components/Dashboard/CourseCard.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index a85f801..19f9338 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -10,12 +10,14 @@ import { MenuButton, MenuItem, MenuList, + useDisclosure, } from "@chakra-ui/react"; import React from "react"; import { createSearchParams, Link } from "react-router-dom"; import useThemeColor from "../../hooks/useThemeColor"; import Popover from "../Popover/Popover"; import Tooltip from "../Popover/Tooltip"; +import Share from "../Share/Share"; interface Props { link: string; @@ -25,6 +27,9 @@ interface Props { const CourseCard = ({ title, link, src }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); + + const { isOpen, onOpen, onClose } = useDisclosure(); + return ( { icon={} > - Share course + onOpen()}>Share course Delete course +
From 17fdc89598aab2379101824800c305dd1bd47d5b Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 14:16:09 -0700 Subject: [PATCH 103/355] Adding helper text and code propr --- frontend/src/components/Dashboard/CourseCard.tsx | 2 +- frontend/src/components/Share/Share.tsx | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index 19f9338..f85481c 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -79,7 +79,7 @@ const CourseCard = ({ title, link, src }: Props) => { onOpen()}>Share course Delete course - +
diff --git a/frontend/src/components/Share/Share.tsx b/frontend/src/components/Share/Share.tsx index 4c35375..bdc9b67 100644 --- a/frontend/src/components/Share/Share.tsx +++ b/frontend/src/components/Share/Share.tsx @@ -8,6 +8,7 @@ import { ModalContent, ModalHeader, ModalOverlay, + Text, useClipboard, } from "@chakra-ui/react"; import React, { useState } from "react"; @@ -15,12 +16,11 @@ import React, { useState } from "react"; interface Props { isOpen: boolean; onClose: () => void; + code: string; } -const Share = ({ isOpen, onClose }: Props) => { - const [value, setValue] = useState("CODE"); - - const { onCopy, hasCopied } = useClipboard(value); +const Share = ({ isOpen, onClose, code }: Props) => { + const { onCopy, hasCopied } = useClipboard(code); return ( @@ -28,11 +28,15 @@ const Share = ({ isOpen, onClose }: Props) => { Share this course to your friends! - + - + + + Share this code with your friends, and tell them to paste it when + creating a new course! + From 9bd46b1c4211bf8398c3ef1d02511865a09888fa Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 14:28:38 -0700 Subject: [PATCH 104/355] Adding form to input course code --- .../src/components/NewCourse/CourseCode.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 frontend/src/components/NewCourse/CourseCode.tsx diff --git a/frontend/src/components/NewCourse/CourseCode.tsx b/frontend/src/components/NewCourse/CourseCode.tsx new file mode 100644 index 0000000..9ff4fbc --- /dev/null +++ b/frontend/src/components/NewCourse/CourseCode.tsx @@ -0,0 +1,44 @@ +import { + FormControl, + FormLabel, + Input, + FormHelperText, + FormErrorMessage, + Button, + Box, +} from "@chakra-ui/react"; +import { Field, Form, Formik } from "formik"; +import React from "react"; + +interface CodeForm { + code: string; +} +const CourseCode = () => { + const handleOnSubmit = (values: CodeForm) => { + console.log(values); + }; + + return ( + + + {({ touched, errors }) => ( + + + Enter the course code below + + + It should be something like Prdp7PRvLa + + {errors.code} + + + + )} + + + ); +}; + +export default CourseCode; From e849f6de768adbfa25f4ffed742e1cc0ce8de0ea Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 14:28:51 -0700 Subject: [PATCH 105/355] Adding state to conditional show course input --- .../components/NewCourse/NewCourseIndex.tsx | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/NewCourse/NewCourseIndex.tsx b/frontend/src/components/NewCourse/NewCourseIndex.tsx index 55f0db5..a473b57 100644 --- a/frontend/src/components/NewCourse/NewCourseIndex.tsx +++ b/frontend/src/components/NewCourse/NewCourseIndex.tsx @@ -1,4 +1,5 @@ import { + Box, Button, Container, Flex, @@ -11,7 +12,7 @@ import { useToast, } from "@chakra-ui/react"; import { Field, Form, Formik } from "formik"; -import React from "react"; +import React, { useState } from "react"; import Banner from "../Hub/Banner"; import * as Yup from "yup"; import { useMutation } from "react-query"; @@ -21,6 +22,7 @@ import { ErrorType } from "../../types/requests"; import { useNavigate } from "react-router-dom"; import { ICourse } from "../../types/course"; import { getConfig, useSession } from "../../utils/auth"; +import CourseCode from "./CourseCode"; interface LearnForm { name: string; @@ -35,6 +37,8 @@ const NewCourseIndex = () => { const navigate = useNavigate(); const { user } = useSession(); + const [openCode, setOpenCode] = useState(false); + const handleSubmit = (values: LearnForm) => { createCourse.mutate(values.name); }; @@ -101,18 +105,29 @@ const NewCourseIndex = () => { {errors.name} - + + + + )} + {openCode && } ); From e4e3a7e7f2f96d776aabe4a21c2fb2ee54f34f9f Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 14:47:40 -0700 Subject: [PATCH 106/355] adding parse object to json util function --- backend/src/utils/parseToJason.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 backend/src/utils/parseToJason.ts diff --git a/backend/src/utils/parseToJason.ts b/backend/src/utils/parseToJason.ts new file mode 100644 index 0000000..f21d600 --- /dev/null +++ b/backend/src/utils/parseToJason.ts @@ -0,0 +1,9 @@ +export const parseObjectToJson = (object: Parse.Object) => { + return object.toJSON(); +}; + +export const parseObjectsToJson = ( + objects: Parse.Object[] +) => { + return objects.map((object) => object.toJSON()); +}; From 40a2940285f1c94583159ad179eedfb7989ef6c4 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 14:59:35 -0700 Subject: [PATCH 107/355] Adding function to clone a complete code based on its courseId --- backend/src/course/clone.ts | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 backend/src/course/clone.ts diff --git a/backend/src/course/clone.ts b/backend/src/course/clone.ts new file mode 100644 index 0000000..e5e470f --- /dev/null +++ b/backend/src/course/clone.ts @@ -0,0 +1,39 @@ +import Parse from "parse/node"; +import { createResource, getResourcesFromCourse } from "../resources/resources"; +import { parseObjectsToJson } from "../utils/parseToJason"; +import { createCourse } from "./course"; +/* +To clone a course: + 1. Create a new course (destination course) + 2. Fetch all resources from the course that wants to be cloned + 3. Create new resources with content from the other one + 4. Make sure to reference the destination course with the resources +*/ + +export const cloneCourse = async ( + courseName: string, + courseId: string, + user: Parse.User +) => { + const course = await createCourse(courseName, user); + const resources = parseObjectsToJson(await getResourcesFromCourse(courseId)); + + await course.save(); + + for (const resource of resources) { + const newResource = createResource({ + type: resource.type, + status: "not started", + url: resource.url, + level: resource.level, + title: resource.title, + description: resource.description, + thumbnail: resource.thumbnail, + feedback: 0, + course, + user, + }); + + await newResource.save(); + } +}; From b4d82787adfb4e71bfdbdc6c8caae0b13f140d88 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 15:41:47 -0700 Subject: [PATCH 108/355] making the function returning the course --- backend/src/course/clone.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/course/clone.ts b/backend/src/course/clone.ts index e5e470f..126786b 100644 --- a/backend/src/course/clone.ts +++ b/backend/src/course/clone.ts @@ -36,4 +36,6 @@ export const cloneCourse = async ( await newResource.save(); } + + return course; }; From bf929d3dc9e2ae0216a24abeda1e264b2ceb68ae Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 18 Jul 2022 15:43:09 -0700 Subject: [PATCH 109/355] adding clone course route --- backend/src/routes/course.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 90eb17a..40a9798 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -1,6 +1,5 @@ import express from "express"; -import { readdirSync } from "fs"; -import { reseller_v1 } from "googleapis"; +import { cloneCourse } from "../course/clone"; import { createCourse, generateResources, @@ -10,7 +9,6 @@ import { saveResources, } from "../course/course"; import { getAuthUser } from "../middleware/getAuthUser"; -import { ICourse } from "../types/course"; import { RequestWUser } from "../types/user"; import { BadRequestError } from "../utils/errors"; @@ -66,4 +64,17 @@ course.get("/progress/:courseId", async (req: RequestWUser, res, next) => { res.send(progress); }); +course.post("/clone/:courseId", async (req: RequestWUser, res, next) => { + const { user } = req; + const { courseId } = req.params; + const { name } = req.body; + + if (!name) { + next(new BadRequestError("Missing params")); + return; + } + + const course = await cloneCourse(name, courseId, user); + res.send(course); +}); export default course; From 5a89fd390a7efbbc55f6360174afe80ee2d2565b Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 09:55:42 -0700 Subject: [PATCH 110/355] Adding name form input inside Course Code Form --- .../src/components/NewCourse/CourseCode.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/NewCourse/CourseCode.tsx b/frontend/src/components/NewCourse/CourseCode.tsx index 9ff4fbc..b426673 100644 --- a/frontend/src/components/NewCourse/CourseCode.tsx +++ b/frontend/src/components/NewCourse/CourseCode.tsx @@ -7,20 +7,24 @@ import { Button, Box, } from "@chakra-ui/react"; +import axios from "axios"; import { Field, Form, Formik } from "formik"; import React from "react"; +import { useMutation } from "react-query"; +import { ICourse } from "../../types/course"; +import { getConfig, useSession } from "../../utils/auth"; +import { baseURL } from "../../utils/constants"; interface CodeForm { code: string; + name: string; } const CourseCode = () => { - const handleOnSubmit = (values: CodeForm) => { - console.log(values); - }; + const handleOnSubmit = (values: CodeForm) => {}; return ( - + {({ touched, errors }) => (
@@ -31,7 +35,12 @@ const CourseCode = () => { {errors.code} -
From 32d415e8eb19a4d0de8592a5108557f2df364f53 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 09:56:14 -0700 Subject: [PATCH 111/355] Adding clone course mutation that lets the user clone a course in the frontend inside code form --- .../src/components/NewCourse/CourseCode.tsx | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/NewCourse/CourseCode.tsx b/frontend/src/components/NewCourse/CourseCode.tsx index b426673..0a6cc71 100644 --- a/frontend/src/components/NewCourse/CourseCode.tsx +++ b/frontend/src/components/NewCourse/CourseCode.tsx @@ -20,7 +20,33 @@ interface CodeForm { name: string; } const CourseCode = () => { - const handleOnSubmit = (values: CodeForm) => {}; + const { user } = useSession(); + + const handleOnSubmit = (values: CodeForm) => { + cloneCourse.mutate(values); + }; + + const cloneCourse = useMutation( + async ({ name, code }: CodeForm) => { + if (!user) return; + const res = await axios.post( + `${baseURL}/course/clone/${code}`, + { + name, + }, + getConfig(user?.sessionToken) + ); + return res.data; + }, + { + onSuccess: () => { + console.log("yay"); + }, + onError: () => { + console.log("error"); + }, + } + ); return ( @@ -40,7 +66,13 @@ const CourseCode = () => { {errors.name} - From 0e5a8e24dfbb8adc27e29bca47080e394039272a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 10:00:30 -0700 Subject: [PATCH 112/355] Adding toast for when the user succeds/fails to clone a course inside the code form --- .../src/components/NewCourse/CourseCode.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/NewCourse/CourseCode.tsx b/frontend/src/components/NewCourse/CourseCode.tsx index 0a6cc71..cd9df91 100644 --- a/frontend/src/components/NewCourse/CourseCode.tsx +++ b/frontend/src/components/NewCourse/CourseCode.tsx @@ -6,12 +6,14 @@ import { FormErrorMessage, Button, Box, + useToast, } from "@chakra-ui/react"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import { Field, Form, Formik } from "formik"; import React from "react"; import { useMutation } from "react-query"; import { ICourse } from "../../types/course"; +import { ErrorType } from "../../types/requests"; import { getConfig, useSession } from "../../utils/auth"; import { baseURL } from "../../utils/constants"; @@ -21,6 +23,7 @@ interface CodeForm { } const CourseCode = () => { const { user } = useSession(); + const toast = useToast(); const handleOnSubmit = (values: CodeForm) => { cloneCourse.mutate(values); @@ -40,10 +43,21 @@ const CourseCode = () => { }, { onSuccess: () => { - console.log("yay"); + toast({ + status: "success", + title: "Course cloned!", + description: "The course has successfully been cloned", + isClosable: true, + }); }, - onError: () => { - console.log("error"); + onError: (error: AxiosError) => { + toast({ + title: "An error ocurred", + description: error.response?.data.message, + status: "error", + duration: 5000, + isClosable: true, + }); }, } ); From e8b6c71f977a0486a031c14bfe3b01361ac0c14c Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 10:03:46 -0700 Subject: [PATCH 113/355] Redirecting the user when the course gets cloned successfully in code form --- frontend/src/components/NewCourse/CourseCode.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/NewCourse/CourseCode.tsx b/frontend/src/components/NewCourse/CourseCode.tsx index cd9df91..537afe9 100644 --- a/frontend/src/components/NewCourse/CourseCode.tsx +++ b/frontend/src/components/NewCourse/CourseCode.tsx @@ -12,6 +12,7 @@ import axios, { AxiosError } from "axios"; import { Field, Form, Formik } from "formik"; import React from "react"; import { useMutation } from "react-query"; +import { useNavigate } from "react-router-dom"; import { ICourse } from "../../types/course"; import { ErrorType } from "../../types/requests"; import { getConfig, useSession } from "../../utils/auth"; @@ -24,6 +25,7 @@ interface CodeForm { const CourseCode = () => { const { user } = useSession(); const toast = useToast(); + const navigate = useNavigate(); const handleOnSubmit = (values: CodeForm) => { cloneCourse.mutate(values); @@ -31,7 +33,10 @@ const CourseCode = () => { const cloneCourse = useMutation( async ({ name, code }: CodeForm) => { - if (!user) return; + if (!user) { + throw new Error("User not defined"); + return; + } const res = await axios.post( `${baseURL}/course/clone/${code}`, { @@ -42,13 +47,14 @@ const CourseCode = () => { return res.data; }, { - onSuccess: () => { + onSuccess: (course) => { toast({ status: "success", title: "Course cloned!", description: "The course has successfully been cloned", isClosable: true, }); + navigate(`/courses/${course?.objectId}/difficulty`); }, onError: (error: AxiosError) => { toast({ From 84e29bf372c6f988cc56f6452f43c3350653b544 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 10:09:21 -0700 Subject: [PATCH 114/355] Adding frontend form validation with Yup inside code form --- frontend/src/components/NewCourse/CourseCode.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/NewCourse/CourseCode.tsx b/frontend/src/components/NewCourse/CourseCode.tsx index 537afe9..104b748 100644 --- a/frontend/src/components/NewCourse/CourseCode.tsx +++ b/frontend/src/components/NewCourse/CourseCode.tsx @@ -17,11 +17,18 @@ import { ICourse } from "../../types/course"; import { ErrorType } from "../../types/requests"; import { getConfig, useSession } from "../../utils/auth"; import { baseURL } from "../../utils/constants"; +import * as Yup from "yup"; interface CodeForm { code: string; name: string; } + +const schema = Yup.object({ + code: Yup.string().required("Course code is required"), + name: Yup.string().required("Course name is required"), +}); + const CourseCode = () => { const { user } = useSession(); const toast = useToast(); @@ -70,7 +77,11 @@ const CourseCode = () => { return ( - + {({ touched, errors }) => (
From c29e2e21da564a9d8423f7447457e2a3d628b8ef Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 10:22:44 -0700 Subject: [PATCH 115/355] Adding error when course doesn't exist when cloning a course in code form --- backend/src/course/clone.ts | 4 ++++ backend/src/routes/course.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/course/clone.ts b/backend/src/course/clone.ts index 126786b..44c6247 100644 --- a/backend/src/course/clone.ts +++ b/backend/src/course/clone.ts @@ -18,6 +18,10 @@ export const cloneCourse = async ( const course = await createCourse(courseName, user); const resources = parseObjectsToJson(await getResourcesFromCourse(courseId)); + if (!resources || resources.length === 0) { + throw new Error("Course doesn't exist"); + } + await course.save(); for (const resource of resources) { diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 40a9798..2c1f343 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -74,7 +74,11 @@ course.post("/clone/:courseId", async (req: RequestWUser, res, next) => { return; } - const course = await cloneCourse(name, courseId, user); - res.send(course); + try { + const course = await cloneCourse(name, courseId, user); + res.send(course); + } catch (err) { + next(new BadRequestError(err.message)); + } }); export default course; From 149ddd16c341f32cbc7170a115867a724cc4fdb2 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 10:39:17 -0700 Subject: [PATCH 116/355] Adding basic modal to display when the user wants to delete a course --- .../components/DeleteCourse/DeleteCourse.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 frontend/src/components/DeleteCourse/DeleteCourse.tsx diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx new file mode 100644 index 0000000..bc84a6f --- /dev/null +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -0,0 +1,29 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, +} from "@chakra-ui/react"; +import React from "react"; + +interface Props { + isOpen: boolean; + onClose: () => void; +} + +const DeleteCourse = ({ isOpen, onClose }: Props) => { + return ( + + + + Share this course to your friends! + + + + + ); +}; + +export default DeleteCourse; From 98e2af64d3525ddb816fc6dd0e7aa7b20cabc356 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 10:40:00 -0700 Subject: [PATCH 117/355] adding logic to open and close the modal to delete a course --- frontend/src/components/Dashboard/CourseCard.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index f85481c..5ec2a40 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -15,6 +15,7 @@ import { import React from "react"; import { createSearchParams, Link } from "react-router-dom"; import useThemeColor from "../../hooks/useThemeColor"; +import DeleteCourse from "../DeleteCourse/DeleteCourse"; import Popover from "../Popover/Popover"; import Tooltip from "../Popover/Tooltip"; import Share from "../Share/Share"; @@ -29,6 +30,11 @@ const CourseCard = ({ title, link, src }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); const { isOpen, onOpen, onClose } = useDisclosure(); + const { + isOpen: isOpenDeleteCourse, + onOpen: onOpenDeleteCourse, + onClose: onCloseDeleteCourse, + } = useDisclosure(); return ( { > onOpen()}>Share course - Delete course + onOpenDeleteCourse()}> + Delete course + + From d02730dc2016f47346755d40d07f0e8155f2f4e9 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 13:43:48 -0700 Subject: [PATCH 118/355] Adding a card that to display in the delete course modal when user wants to delete a course --- .../DeleteCourse/VideoCardToDelete.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 frontend/src/components/DeleteCourse/VideoCardToDelete.tsx diff --git a/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx b/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx new file mode 100644 index 0000000..5ad0ed2 --- /dev/null +++ b/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx @@ -0,0 +1,40 @@ +import { Box, Text, Icon, Image } from "@chakra-ui/react"; +import React from "react"; +import { BiVideo } from "react-icons/bi"; +import useThemeColor from "../../hooks/useThemeColor"; + +interface Props { + src: string; + title: string; +} +const VideoCardToDelete = ({ src, title }: Props) => { + const { backgroundColor, borderColor } = useThemeColor(); + + return ( + + + + + + + + {title} + + + + ); +}; + +export default VideoCardToDelete; From cd1a22f942ca93bc01f54b1e29ac767afb5acc75 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 13:44:02 -0700 Subject: [PATCH 119/355] Adding grid to display multiple video cards when user want's to delete a course --- .../src/components/DeleteCourse/DeleteCourse.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index bc84a6f..b092c9b 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -5,8 +5,12 @@ import { ModalHeader, ModalCloseButton, ModalBody, + Text, + Grid, } from "@chakra-ui/react"; import React from "react"; +import VideoCard from "../Hub/VideoCard"; +import VideoCardToDelete from "./VideoCardToDelete"; interface Props { isOpen: boolean; @@ -15,12 +19,17 @@ interface Props { const DeleteCourse = ({ isOpen, onClose }: Props) => { return ( - + - Share this course to your friends! + Are you sure you want to delete this course? - + + Select which tutorials you didn{"'"}t like? + + + + ); From fd735da5fa9eab5ae404e4bb299f82e468142eba Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 13:46:23 -0700 Subject: [PATCH 120/355] Typo --- frontend/src/components/DeleteCourse/DeleteCourse.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index b092c9b..db7bb6b 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -25,9 +25,10 @@ const DeleteCourse = ({ isOpen, onClose }: Props) => { Are you sure you want to delete this course? - Select which tutorials you didn{"'"}t like? + Select which tutorials you didn{"'"}t like + From 755c8c5a40ae518a4e610af967571d84fdd4d5aa Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 14:09:38 -0700 Subject: [PATCH 121/355] adding onClick and objectId props so an user can select which videos it didn't like when deleting a course --- .../DeleteCourse/VideoCardToDelete.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx b/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx index 5ad0ed2..2894b9b 100644 --- a/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx +++ b/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx @@ -6,17 +6,27 @@ import useThemeColor from "../../hooks/useThemeColor"; interface Props { src: string; title: string; + active: boolean; + objectId: string; + onClick: (objectId: string) => void; } -const VideoCardToDelete = ({ src, title }: Props) => { +const VideoCardToDelete = ({ + src, + title, + objectId, + onClick, + active, +}: Props) => { const { backgroundColor, borderColor } = useThemeColor(); return ( onClick(objectId)} > From d8ffe30bf5073e9fe3875156d00555d02d07d5cf Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 14:12:25 -0700 Subject: [PATCH 122/355] adding backend route to fetch the resources of a course given the id --- backend/src/routes/resources.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/routes/resources.ts b/backend/src/routes/resources.ts index 8f48de8..e6b6d05 100644 --- a/backend/src/routes/resources.ts +++ b/backend/src/routes/resources.ts @@ -25,6 +25,12 @@ resources.get( } ); +resources.get("/byCourse/:courseId", async (req: RequestWUser, res, next) => { + const { courseId } = req.params; + const resources = await getResourcesFromCourse(courseId); + res.send(resources); +}); + resources.put( "/updateStatus/:resourceId", async (req: RequestWUser, res, next) => { From d001d64fc159c317973b3c60539cc10226464ae2 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 14:14:32 -0700 Subject: [PATCH 123/355] Adding useQuery hook to fetch from backend the resources from an specific course that wants to be deleted --- .../components/DeleteCourse/DeleteCourse.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index db7bb6b..9bd6669 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -8,16 +8,40 @@ import { Text, Grid, } from "@chakra-ui/react"; +import axios from "axios"; import React from "react"; +import { IResource } from "../../types/resource"; +import { getConfig, useSession } from "../../utils/auth"; +import { baseURL } from "../../utils/constants"; import VideoCard from "../Hub/VideoCard"; import VideoCardToDelete from "./VideoCardToDelete"; +import { useQuery } from "react-query"; interface Props { isOpen: boolean; onClose: () => void; + courseId: string; } -const DeleteCourse = ({ isOpen, onClose }: Props) => { +const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { + const { user, isFetching } = useSession(); + + const { data, isFetching: isQueryFetching } = useQuery( + `hub-${courseId}`, + async () => { + if (!user) throw new Error("User is not defined"); + + const res = await axios.get( + `${baseURL}/resources/byCourse/${courseId}`, + getConfig(user.sessionToken) + ); + return res.data; + }, + { + enabled: !isFetching && !!courseId, + } + ); + return ( From 991e9c563a4cfade73a6224c6574c918de640c74 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 14:16:59 -0700 Subject: [PATCH 124/355] Rendering multiple video cards based on what we received from the backend --- .../src/components/DeleteCourse/DeleteCourse.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index 9bd6669..8c1b540 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -13,7 +13,6 @@ import React from "react"; import { IResource } from "../../types/resource"; import { getConfig, useSession } from "../../utils/auth"; import { baseURL } from "../../utils/constants"; -import VideoCard from "../Hub/VideoCard"; import VideoCardToDelete from "./VideoCardToDelete"; import { useQuery } from "react-query"; @@ -26,7 +25,7 @@ interface Props { const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { const { user, isFetching } = useSession(); - const { data, isFetching: isQueryFetching } = useQuery( + const { data: resources, isFetching: isQueryFetching } = useQuery( `hub-${courseId}`, async () => { if (!user) throw new Error("User is not defined"); @@ -51,8 +50,15 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { Select which tutorials you didn{"'"}t like - - + {resources && + resources.map((resource) => ( + + ))} From cc123a796827fc8f00bdbb7f4eeb3e7f7eb0072f Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 14:23:44 -0700 Subject: [PATCH 125/355] Adding toggling feature where the user can select which videos he didn't like when deleting a course --- .../components/DeleteCourse/DeleteCourse.tsx | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index 8c1b540..87d18ea 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -9,7 +9,7 @@ import { Grid, } from "@chakra-ui/react"; import axios from "axios"; -import React from "react"; +import React, { useState } from "react"; import { IResource } from "../../types/resource"; import { getConfig, useSession } from "../../utils/auth"; import { baseURL } from "../../utils/constants"; @@ -25,6 +25,8 @@ interface Props { const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { const { user, isFetching } = useSession(); + const [selectedResources, setSelectedResources] = useState([]); + const { data: resources, isFetching: isQueryFetching } = useQuery( `hub-${courseId}`, async () => { @@ -37,10 +39,26 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { return res.data; }, { - enabled: !isFetching && !!courseId, + enabled: isOpen && !isFetching && !!courseId, } ); + // Future Note: It's better if we use something like set() or hashmap. Better complexity. + const handleToggleSelectedResource = (id: string) => { + const alreadySelected = selectedResources.find( + (selectedId) => selectedId === id + ); + + if (alreadySelected) { + setSelectedResources( + selectedResources.filter((selectedId) => selectedId !== id) + ); + return; + } + + setSelectedResources([...selectedResources, id]); + }; + return ( @@ -57,6 +75,12 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { title={resource.title} objectId={resource.objectId} src={resource.type === "video" ? resource.thumbnail : ""} + onClick={(objectId) => handleToggleSelectedResource(objectId)} + active={ + !!selectedResources.find( + (selectedId) => selectedId === resource.objectId + ) + } /> ))} From 4c41372bc914e0be03438cfb8ca3d3f50a9332d4 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 14:27:08 -0700 Subject: [PATCH 126/355] Adding courseId prop to deleteCourse modal so it can fetch resources from backend --- frontend/src/components/Dashboard/CourseCard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/Dashboard/CourseCard.tsx b/frontend/src/components/Dashboard/CourseCard.tsx index 5ec2a40..bf2087c 100644 --- a/frontend/src/components/Dashboard/CourseCard.tsx +++ b/frontend/src/components/Dashboard/CourseCard.tsx @@ -91,6 +91,7 @@ const CourseCard = ({ title, link, src }: Props) => { From 38443ed3083eb9a6e4b199d8f6a78a5c9920d14a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 14:31:36 -0700 Subject: [PATCH 127/355] Sorting videos by difficulty when the user is which selecting which one it didn't like when deleting a course --- frontend/src/components/DeleteCourse/DeleteCourse.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index 87d18ea..795d6f8 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -36,7 +36,8 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { `${baseURL}/resources/byCourse/${courseId}`, getConfig(user.sessionToken) ); - return res.data; + + return res.data.sort((a, b) => a.level - b.level); }, { enabled: isOpen && !isFetching && !!courseId, @@ -67,7 +68,7 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { Select which tutorials you didn{"'"}t like - + {resources && resources.map((resource) => ( Date: Tue, 19 Jul 2022 14:32:04 -0700 Subject: [PATCH 128/355] Making the video card image smaller so it doesn't have black bars when displaying on a grid --- frontend/src/components/DeleteCourse/VideoCardToDelete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx b/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx index 2894b9b..f57c9bd 100644 --- a/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx +++ b/frontend/src/components/DeleteCourse/VideoCardToDelete.tsx @@ -28,7 +28,7 @@ const VideoCardToDelete = ({ transition={"all 0.3s"} onClick={() => onClick(objectId)} > - + Date: Tue, 19 Jul 2022 15:05:14 -0700 Subject: [PATCH 129/355] Adding resources selected text and delete course button --- frontend/src/components/DeleteCourse/DeleteCourse.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index 795d6f8..da235ff 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -7,6 +7,8 @@ import { ModalBody, Text, Grid, + Button, + ModalFooter, } from "@chakra-ui/react"; import axios from "axios"; import React, { useState } from "react"; @@ -85,7 +87,16 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { /> ))} + + {selectedResources.length} resources selected + + + + + ); From 9742d170aa15064bdc7cf04029763b97d4788675 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 18:02:02 -0700 Subject: [PATCH 130/355] Adding missing imports that were lost when merging into main --- frontend/src/components/NewCourse/NewCourseIndex.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/components/NewCourse/NewCourseIndex.tsx b/frontend/src/components/NewCourse/NewCourseIndex.tsx index 42b18c6..47c7f0a 100644 --- a/frontend/src/components/NewCourse/NewCourseIndex.tsx +++ b/frontend/src/components/NewCourse/NewCourseIndex.tsx @@ -20,6 +20,9 @@ import { ErrorType } from "../../types/requests"; import { ICourse } from "../../types/course"; import { getConfig, useSession } from "../../utils/auth"; import CourseCode from "./CourseCode"; +import { useMutation } from "react-query"; +import { useNavigate } from "react-router-dom"; +import { baseURL } from "../../utils/constants"; interface LearnForm { name: string; From c5ea1fdcd3e5dbe9d7991820f6a15ef593a3d5bd Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Tue, 19 Jul 2022 18:09:27 -0700 Subject: [PATCH 131/355] Fixing build errors to make the app deployable --- frontend/src/components/DeleteCourse/DeleteCourse.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index db7bb6b..899c838 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -9,8 +9,6 @@ import { Grid, } from "@chakra-ui/react"; import React from "react"; -import VideoCard from "../Hub/VideoCard"; -import VideoCardToDelete from "./VideoCardToDelete"; interface Props { isOpen: boolean; @@ -26,10 +24,7 @@ const DeleteCourse = ({ isOpen, onClose }: Props) => { Select which tutorials you didn{"'"}t like - - - - + From a9b8764b31e3a093c47096b1d04857b11cace5f0 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 20 Jul 2022 09:48:25 -0700 Subject: [PATCH 132/355] adding a function to delete a course from the database given its objectId --- backend/src/course/course.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index ba615d8..7e5edc3 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -79,6 +79,13 @@ export const createCourse = async ( return Course; }; +export const deleteCourse = async (courseId: string) => { + const Course = new Parse.Object("Course"); + Course.set("objectId", courseId); + + return await Course.destroy(); +}; + export const saveResources = async ( resources: IWeightedYoutubeVideo[], course: Parse.Object, From 42a308be2dc95a96c9b941942df43a25ea8e6bd2 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 20 Jul 2022 09:48:39 -0700 Subject: [PATCH 133/355] adding a backend route that interacts with deleteCourse function so a user can delete a course --- backend/src/routes/course.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index 2c1f343..ecdc994 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -2,6 +2,7 @@ import express from "express"; import { cloneCourse } from "../course/clone"; import { createCourse, + deleteCourse, generateResources, getCourseByUserAndId, getProgressByCourse, @@ -81,4 +82,14 @@ course.post("/clone/:courseId", async (req: RequestWUser, res, next) => { next(new BadRequestError(err.message)); } }); + +course.delete("/:courseId", async (req: RequestWUser, res, next) => { + // I want to both delete the course and take feedback into consideration + + const { courseId } = req.params; + const deletedCourse = await deleteCourse(courseId); + + res.send(deletedCourse); +}); + export default course; From ee28c21ddfecb3ea513bd7b7bc3a6469f76af66c Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 20 Jul 2022 10:21:38 -0700 Subject: [PATCH 134/355] Adding loading state using skeleton and spinners inside delete course modal --- .../components/DeleteCourse/DeleteCourse.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index da235ff..437e5c0 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -9,6 +9,9 @@ import { Grid, Button, ModalFooter, + Spinner, + HStack, + Skeleton, } from "@chakra-ui/react"; import axios from "axios"; import React, { useState } from "react"; @@ -29,7 +32,11 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { const [selectedResources, setSelectedResources] = useState([]); - const { data: resources, isFetching: isQueryFetching } = useQuery( + const { + data: resources, + isFetching: isQueryFetching, + isLoading, + } = useQuery( `hub-${courseId}`, async () => { if (!user) throw new Error("User is not defined"); @@ -66,12 +73,24 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { - Are you sure you want to delete this course? + + + Are you sure you want to delete this course? + {isQueryFetching && } + + Select which tutorials you didn{"'"}t like - {resources && + {isLoading ? ( + <> + + + + + ) : ( + resources && resources.map((resource) => ( { ) } /> - ))} + )) + )} {selectedResources.length} resources selected From ee726c02c8245d010943d093f9af288af19d6b31 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 20 Jul 2022 10:42:01 -0700 Subject: [PATCH 135/355] adding mutation that handles the connection between frontend and backend to delete a course given its id inside the delete course moda --- .../components/DeleteCourse/DeleteCourse.tsx | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index 437e5c0..2f2b2e7 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -12,14 +12,16 @@ import { Spinner, HStack, Skeleton, + useToast, } from "@chakra-ui/react"; -import axios from "axios"; +import axios, { AxiosError } from "axios"; import React, { useState } from "react"; import { IResource } from "../../types/resource"; import { getConfig, useSession } from "../../utils/auth"; import { baseURL } from "../../utils/constants"; import VideoCardToDelete from "./VideoCardToDelete"; -import { useQuery } from "react-query"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { ErrorType } from "../../types/requests"; interface Props { isOpen: boolean; @@ -29,6 +31,8 @@ interface Props { const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { const { user, isFetching } = useSession(); + const queryClient = useQueryClient(); + const toast = useToast(); const [selectedResources, setSelectedResources] = useState([]); @@ -53,6 +57,43 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { } ); + const deleteCourse = useMutation( + async (courseId: string) => { + if (!user) { + throw new Error("User is not defined"); + } + const res = await axios.delete( + `${baseURL}/course/${courseId}`, + getConfig(user.sessionToken) + ); + return res.data; + }, + { + onSuccess: () => { + queryClient.invalidateQueries("courses"); + toast({ + status: "success", + title: "Course deleted!", + description: "We appreciate your feedback!", + isClosable: true, + }); + }, + onError: (error: AxiosError) => { + toast({ + status: "error", + title: "Error deleting course", + description: error.response?.data.message + isClosable: true, + }); + }, + } + ); + + const handleDeleteCourse = () => { + deleteCourse.mutate(courseId); + onClose(); + }; + // Future Note: It's better if we use something like set() or hashmap. Better complexity. const handleToggleSelectedResource = (id: string) => { const alreadySelected = selectedResources.find( @@ -115,7 +156,9 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { - + From 4e68d4577eade78e32e8580957a1c7181b1e3d61 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 20 Jul 2022 10:43:36 -0700 Subject: [PATCH 136/355] Typo --- frontend/src/components/DeleteCourse/DeleteCourse.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index 2f2b2e7..1de29f7 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -82,7 +82,7 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { toast({ status: "error", title: "Error deleting course", - description: error.response?.data.message + description: error.response?.data.message, isClosable: true, }); }, From 7ad5cca0608ec49b4e4c00af9d56228f3a6e888d Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 20 Jul 2022 11:27:47 -0700 Subject: [PATCH 137/355] Saving videoId when creating a resource, to be used inside internal ranking --- backend/src/course/clone.ts | 1 + backend/src/course/course.ts | 1 + backend/src/types/resource.d.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/backend/src/course/clone.ts b/backend/src/course/clone.ts index 44c6247..9d606f9 100644 --- a/backend/src/course/clone.ts +++ b/backend/src/course/clone.ts @@ -28,6 +28,7 @@ export const cloneCourse = async ( const newResource = createResource({ type: resource.type, status: "not started", + videoId: resource.id, url: resource.url, level: resource.level, title: resource.title, diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index 7e5edc3..c547cea 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -96,6 +96,7 @@ export const saveResources = async ( const video = createResource({ type: "video", level, + videoId: resource.id, status: "not started", title: resource.snippet.title, description: resource.snippet.description, diff --git a/backend/src/types/resource.d.ts b/backend/src/types/resource.d.ts index c8ae795..789cf40 100644 --- a/backend/src/types/resource.d.ts +++ b/backend/src/types/resource.d.ts @@ -16,6 +16,7 @@ export type IResource = channel: string; course: Parse.Object; user: Parse.User; + videoId: string; } | { type: "website"; From 3311ded13896ca2785c274b94c8f9b676882dda6 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Wed, 20 Jul 2022 15:19:56 -0700 Subject: [PATCH 138/355] Adding helper functions to update a feedback object, and if it doesn't exist, it creates one. --- backend/src/feedback/feedback.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 backend/src/feedback/feedback.ts diff --git a/backend/src/feedback/feedback.ts b/backend/src/feedback/feedback.ts new file mode 100644 index 0000000..4d13393 --- /dev/null +++ b/backend/src/feedback/feedback.ts @@ -0,0 +1,30 @@ +import Parse from "parse/node"; + +export const createFeedback = (videoId: string, feedback: number) => { + const Feedback = new Parse.Object("Feedback"); + Feedback.set("videoId", videoId); + Feedback.set("feedback", feedback); + return Feedback; +}; + +export const updateFeedback = async ( + videoId: string, + amountToIncrement: number +) => { + const feedback = await getFeedbackByVideoId(videoId); + + if (!feedback || feedback.length === 0) { + return createFeedback(videoId, amountToIncrement); + } + + return feedback[0].increment("feedback", amountToIncrement); +}; + +export const getFeedbackByVideoId = async (videoId: string) => { + const Feedback = Parse.Object.extend("Feedback"); + const query = new Parse.Query(Feedback); + + query.equalTo("videoId", videoId); + const feedback = await query.find(); + return feedback; +}; From b3e913f08cb2e99ee0446f7a5817fa2bf749f2ba Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 21 Jul 2022 10:25:50 -0700 Subject: [PATCH 139/355] Fixing a bug where feedback was not saving in the database --- backend/src/feedback/feedback.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/feedback/feedback.ts b/backend/src/feedback/feedback.ts index 4d13393..442b738 100644 --- a/backend/src/feedback/feedback.ts +++ b/backend/src/feedback/feedback.ts @@ -14,10 +14,12 @@ export const updateFeedback = async ( const feedback = await getFeedbackByVideoId(videoId); if (!feedback || feedback.length === 0) { - return createFeedback(videoId, amountToIncrement); + const newFeedbackObject = createFeedback(videoId, amountToIncrement); + return await newFeedbackObject.save(); } - return feedback[0].increment("feedback", amountToIncrement); + feedback[0].increment("feedback", amountToIncrement); + return await feedback[0].save(); }; export const getFeedbackByVideoId = async (videoId: string) => { From ff9f14a43d04d4da532b321e4928cbd87ed61e1f Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 21 Jul 2022 10:26:46 -0700 Subject: [PATCH 140/355] adding a functions that adds positive feedback to all resources that were shared when a user clones a course --- backend/src/course/clone.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/src/course/clone.ts b/backend/src/course/clone.ts index 9d606f9..f4e4d6d 100644 --- a/backend/src/course/clone.ts +++ b/backend/src/course/clone.ts @@ -1,4 +1,5 @@ import Parse from "parse/node"; +import { updateFeedback } from "../feedback/feedback"; import { createResource, getResourcesFromCourse } from "../resources/resources"; import { parseObjectsToJson } from "../utils/parseToJason"; import { createCourse } from "./course"; @@ -10,6 +11,8 @@ To clone a course: 4. Make sure to reference the destination course with the resources */ +export const SCORE_PER_SHARE = 1; + export const cloneCourse = async ( courseName: string, courseId: string, @@ -28,7 +31,7 @@ export const cloneCourse = async ( const newResource = createResource({ type: resource.type, status: "not started", - videoId: resource.id, + videoId: resource.videoId, url: resource.url, level: resource.level, title: resource.title, @@ -42,5 +45,15 @@ export const cloneCourse = async ( await newResource.save(); } + await updateFeedbackForEveryResource(resources); return course; }; + +const updateFeedbackForEveryResource = async ( + resources: (Parse.Object.ToJSON & + Parse.JSONBaseAttributes)[] +) => { + for (const resource of resources) { + await updateFeedback(resource.videoId, SCORE_PER_SHARE); + } +}; From e19e4d60f783d68ebe97ae73590399cbfc662b8c Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 21 Jul 2022 10:42:52 -0700 Subject: [PATCH 141/355] giving positive feedback to a resource when a user marks it as completed --- backend/src/resources/resources.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/src/resources/resources.ts b/backend/src/resources/resources.ts index 0c4d783..6fbd331 100644 --- a/backend/src/resources/resources.ts +++ b/backend/src/resources/resources.ts @@ -1,6 +1,8 @@ import { IResource, IResourceStatus } from "../types/resource"; import Parse from "parse/node"; +import { updateFeedback } from "../feedback/feedback"; +const SCORE_PER_COMPLETED = 0.1; export const createResource = (resource: IResource) => { const Resource: Parse.Object = new Parse.Object("Resource"); @@ -26,6 +28,13 @@ export const getResourcesFromCourse = async (courseId: string) => { return resources; }; +export const findResourceById = async (id: string) => { + const Resource = Parse.Object.extend("Resource"); + const query = new Parse.Query(Resource); + query.equalTo("objectId", id); + return await query.find(); +}; + export const getResourcesFromCourseAndDifficulty = async ( courseId: string, level: number @@ -53,5 +62,10 @@ export const updateResourceStatus = async ( Resource.set("objectId", resourceId); Resource.set("status", status); + if (status === "completed") { + const resource = await findResourceById(resourceId); + updateFeedback(resource[0].get("videoId"), SCORE_PER_COMPLETED); + } + return Resource; }; From 809ff7b2a838586922a891cac59691b4546c8afc Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 21 Jul 2022 15:41:18 -0700 Subject: [PATCH 142/355] adding a function that gives negative feedback to a list of videoIds --- backend/src/course/course.ts | 16 +++++++++++++++- backend/src/routes/course.ts | 10 ++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index c547cea..fcdfa59 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -5,8 +5,10 @@ import { IResource } from "../types/resource"; import { IWeightedYoutubeVideo } from "../types/youtube"; import { createResource } from "../resources/resources"; import { getImagesByQuery } from "../unsplash/unsplash"; +import { updateFeedback } from "../feedback/feedback"; const VIDEOS_PER_QUERY = 100; +const SCORE_PER_DISLIKED_VIDEO = -2; export const getUserCourses = async (user: Parse.Object) => { const Course = Parse.Object.extend("Course"); @@ -79,13 +81,25 @@ export const createCourse = async ( return Course; }; -export const deleteCourse = async (courseId: string) => { +export const deleteCourse = async ( + courseId: string, + dislikedVideos: string[] +) => { const Course = new Parse.Object("Course"); Course.set("objectId", courseId); + giveNegativeFeedbackToDislikedVideos(dislikedVideos); return await Course.destroy(); }; +export const giveNegativeFeedbackToDislikedVideos = async ( + dislikedVideos: string[] +) => { + for (const dislikedVideo of dislikedVideos) { + await updateFeedback(dislikedVideo, SCORE_PER_DISLIKED_VIDEO); + } +}; + export const saveResources = async ( resources: IWeightedYoutubeVideo[], course: Parse.Object, diff --git a/backend/src/routes/course.ts b/backend/src/routes/course.ts index ecdc994..69261cc 100644 --- a/backend/src/routes/course.ts +++ b/backend/src/routes/course.ts @@ -85,9 +85,15 @@ course.post("/clone/:courseId", async (req: RequestWUser, res, next) => { course.delete("/:courseId", async (req: RequestWUser, res, next) => { // I want to both delete the course and take feedback into consideration - const { courseId } = req.params; - const deletedCourse = await deleteCourse(courseId); + const { dislikedVideos } = req.body; + + if (dislikedVideos === undefined) { + next(new BadRequestError("Missing parameters")); + return; + } + + const deletedCourse = await deleteCourse(courseId, dislikedVideos); res.send(deletedCourse); }); From c0ba300b62aa9ca277b18e70ac73b9255f96aaa6 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Thu, 21 Jul 2022 15:53:40 -0700 Subject: [PATCH 143/355] passing disliked videos parameters to the backend using axios --- .../components/DeleteCourse/DeleteCourse.tsx | 24 ++++++++++++------- frontend/src/types/resource.d.ts | 2 ++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/DeleteCourse/DeleteCourse.tsx b/frontend/src/components/DeleteCourse/DeleteCourse.tsx index 1de29f7..d09566a 100644 --- a/frontend/src/components/DeleteCourse/DeleteCourse.tsx +++ b/frontend/src/components/DeleteCourse/DeleteCourse.tsx @@ -58,14 +58,22 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { ); const deleteCourse = useMutation( - async (courseId: string) => { + async ({ + courseId, + dislikedVideos, + }: { + courseId: string; + dislikedVideos: string[]; + }) => { if (!user) { throw new Error("User is not defined"); } - const res = await axios.delete( - `${baseURL}/course/${courseId}`, - getConfig(user.sessionToken) - ); + const res = await axios.delete(`${baseURL}/course/${courseId}`, { + ...getConfig(user.sessionToken), + data: { + dislikedVideos: dislikedVideos, + }, + }); return res.data; }, { @@ -90,7 +98,7 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { ); const handleDeleteCourse = () => { - deleteCourse.mutate(courseId); + deleteCourse.mutate({ courseId, dislikedVideos: selectedResources }); onClose(); }; @@ -136,12 +144,12 @@ const DeleteCourse = ({ isOpen, onClose, courseId }: Props) => { handleToggleSelectedResource(objectId)} active={ !!selectedResources.find( - (selectedId) => selectedId === resource.objectId + (selectedId) => selectedId === resource.videoId ) } /> diff --git a/frontend/src/types/resource.d.ts b/frontend/src/types/resource.d.ts index bb764ae..aba770f 100644 --- a/frontend/src/types/resource.d.ts +++ b/frontend/src/types/resource.d.ts @@ -16,6 +16,7 @@ export type IResource = thumbnail: string; channel: string; course: Parse.Object; + videoId: string; } | { objectId: string; @@ -26,4 +27,5 @@ export type IResource = title: string; url: string; course: Parse.Object; + videoId: string; }; From b5c2d8db9b5a5ca74c4a11e5478b98115cc0d3b8 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 11:10:13 -0700 Subject: [PATCH 144/355] Adding types to handle internal ranking interfaces when getting interal ranking given a video query --- backend/src/types/youtube.d.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/types/youtube.d.ts b/backend/src/types/youtube.d.ts index 0febe51..94a3928 100644 --- a/backend/src/types/youtube.d.ts +++ b/backend/src/types/youtube.d.ts @@ -19,3 +19,15 @@ export interface IWeightedYoutubeVideo extends INormalizedYoutubeVideo { weighted_score: IExternalRankingScore; final_score: number; } + +export interface IRawInternalYoutubeVideo extends youtube_v3.Schema$Video { + raw_internal_score: number; +} + +export interface INormalizedInternalYoutubeVideo + extends IRawInternalYoutubeVideo { + internal_score: number; +} + +export type IFinalRankingYoutubeVideo = INormalizedInternalYoutubeVideo & + IWeightedYoutubeVideo; From d3fd2f110efefe3dd52d4ff0db308e56d369df7f Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 11:11:08 -0700 Subject: [PATCH 145/355] getting internal feedback from database and assign it to videos, and then normalize the results so it can be used for ranking --- backend/src/feedback/feedback.ts | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/backend/src/feedback/feedback.ts b/backend/src/feedback/feedback.ts index 442b738..fd0bd9f 100644 --- a/backend/src/feedback/feedback.ts +++ b/backend/src/feedback/feedback.ts @@ -1,4 +1,10 @@ +import { youtube_v3 } from "googleapis"; import Parse from "parse/node"; +import { + INormalizedInternalYoutubeVideo, + IRawInternalYoutubeVideo, +} from "../types/youtube"; +import { normalize } from "../utils/math"; export const createFeedback = (videoId: string, feedback: number) => { const Feedback = new Parse.Object("Feedback"); @@ -30,3 +36,66 @@ export const getFeedbackByVideoId = async (videoId: string) => { const feedback = await query.find(); return feedback; }; +export const getFeedback = async () => { + const Feedback = Parse.Object.extend("Feedback"); + const query = new Parse.Query(Feedback); + + const feedback = await query.find(); + return feedback; +}; + +export const assignFeedbackScoreToFetchedVideos = ( + videos: youtube_v3.Schema$Video[], + feedback: Parse.Object[] +) => { + const rawFeedbackVideos = assignRawFeedbackScoreToFetchedVideos( + videos, + feedback + ); + const { minScore, maxScore } = getMaxMinFeedbackScore(rawFeedbackVideos); + return normalizeFeedbackFromFetchedVideos( + rawFeedbackVideos, + minScore, + maxScore + ); +}; + +export const assignRawFeedbackScoreToFetchedVideos = ( + videos: youtube_v3.Schema$Video[], + feedback: Parse.Object[] +): IRawInternalYoutubeVideo[] => { + return videos.map((video) => { + const videoInFeedback = feedback.find((f) => f.get("videoId") === video.id); + + if (!videoInFeedback) return { ...video, raw_internal_score: 0 }; + return { ...video, raw_internal_score: videoInFeedback.get("feedback") }; + }); +}; + +export const getMaxMinFeedbackScore = (videos: IRawInternalYoutubeVideo[]) => { + let maxFeedbackScore = videos[0].raw_internal_score; + let minFeedbackScore = videos[0].raw_internal_score; + + for (const video of videos) { + maxFeedbackScore = Math.max(video.raw_internal_score, maxFeedbackScore); + minFeedbackScore = Math.min(video.raw_internal_score, minFeedbackScore); + } + return { minScore: minFeedbackScore, maxScore: maxFeedbackScore }; +}; + +export const normalizeFeedbackFromFetchedVideos = ( + videos: IRawInternalYoutubeVideo[], + minInternalFeedbackScore: number, + maxInternalFeedbackScore: number +): INormalizedInternalYoutubeVideo[] => { + return videos.map((video) => { + return { + ...video, + internal_score: normalize( + video.raw_internal_score, + minInternalFeedbackScore, + maxInternalFeedbackScore + ), + }; + }); +}; From 14aff03b0ec3e4b8e9f61f28bc3c6d001e984fd7 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 11:11:48 -0700 Subject: [PATCH 146/355] using internal ranking functions and merging them together with external ranking to create a final ranking score --- backend/src/rating/ranking.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/backend/src/rating/ranking.ts b/backend/src/rating/ranking.ts index 0e5d319..5333449 100644 --- a/backend/src/rating/ranking.ts +++ b/backend/src/rating/ranking.ts @@ -3,10 +3,16 @@ import { ExpressError } from "../utils/errors"; import { parseISO, differenceInCalendarDays } from "date-fns"; import { normalize } from "../utils/math"; import { + IFinalRankingYoutubeVideo, + INormalizedInternalYoutubeVideo, INormalizedYoutubeVideo, IRawYoutubeVideo, IWeightedYoutubeVideo, } from "../types/youtube"; +import { + assignFeedbackScoreToFetchedVideos, + getFeedback, +} from "../feedback/feedback"; interface IMaxScores { dateXViews: number; @@ -34,6 +40,32 @@ export const WEIGHTS = { weight6: 0, }; +export const getFinalRanking = async (videos: youtube_v3.Schema$Video[]) => { + const externalRankedVideos = getExternalRanking(videos); + const internalRankedVideos = await getInternalRanking(videos); + return mergeInternalExternalRanking( + externalRankedVideos, + internalRankedVideos + ); +}; + +export const getInternalRanking = async (videos: youtube_v3.Schema$Video[]) => { + const feedback = await getFeedback(); + return assignFeedbackScoreToFetchedVideos(videos, feedback); +}; + +export const mergeInternalExternalRanking = ( + externalRankedVideos: IWeightedYoutubeVideo[], + internalRankedVideos: INormalizedInternalYoutubeVideo[] +): IFinalRankingYoutubeVideo[] => { + if (externalRankedVideos.length !== internalRankedVideos.length) { + throw new Error("External and internal ranking doesn't match"); + } + return externalRankedVideos.map((externalVideo, idx) => { + return { ...externalVideo, ...internalRankedVideos[idx] }; + }); +}; + export const getExternalRanking = (videos: youtube_v3.Schema$Video[]) => { const rawExternalScoreVideos = getRawExternalRanking(videos); const maxScores = getMaxScores(rawExternalScoreVideos); From 05a0751cdc4ce568e4ff036d91b405de3c55fbe6 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 11:12:30 -0700 Subject: [PATCH 147/355] changin getExternalRanking function when creating a course to getFinalRanking that combines both external and internal ranking --- backend/src/course/course.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index fcdfa59..681373e 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -1,4 +1,8 @@ -import { getExternalRanking } from "../rating/ranking"; +import { + getExternalRanking, + getFinalRanking, + getInternalRanking, +} from "../rating/ranking"; import { getVideoDetailByIds, getVideosByQuery } from "../rating/youtube"; import Parse from "parse/node"; import { IResource } from "../types/resource"; @@ -152,7 +156,8 @@ const getRankedVideos = async (query: string) => { const videosDetailed = await getVideoDetailByIds(ids, VIDEOS_PER_QUERY); - const rankedVideos = getExternalRanking(videosDetailed.data.items); + const rankedVideos = await getFinalRanking(videosDetailed.data.items); + const sortedRankedVideos = rankedVideos.sort( (a, b) => b.final_score - a.final_score ); From 3d57de6d5b1f24172ca78d2a94e0d156b9bbc88d Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 11:40:08 -0700 Subject: [PATCH 148/355] changing the parameter from final_score to external_final_score to be more accurate --- backend/src/types/youtube.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/types/youtube.d.ts b/backend/src/types/youtube.d.ts index 94a3928..1b81e30 100644 --- a/backend/src/types/youtube.d.ts +++ b/backend/src/types/youtube.d.ts @@ -17,7 +17,7 @@ export interface INormalizedYoutubeVideo extends IRawYoutubeVideo { export interface IWeightedYoutubeVideo extends INormalizedYoutubeVideo { weighted_score: IExternalRankingScore; - final_score: number; + final_external_score: number; } export interface IRawInternalYoutubeVideo extends youtube_v3.Schema$Video { @@ -30,4 +30,4 @@ export interface INormalizedInternalYoutubeVideo } export type IFinalRankingYoutubeVideo = INormalizedInternalYoutubeVideo & - IWeightedYoutubeVideo; + IWeightedYoutubeVideo & { final_score: number }; From 82f1bab16fabd39557be91a887b9d60bec8d0246 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 11:41:00 -0700 Subject: [PATCH 149/355] applying weight to external and internal ranking so it matches the formula proposed early in the design doc --- backend/src/rating/ranking.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/src/rating/ranking.ts b/backend/src/rating/ranking.ts index 5333449..d5f0b3d 100644 --- a/backend/src/rating/ranking.ts +++ b/backend/src/rating/ranking.ts @@ -62,7 +62,10 @@ export const mergeInternalExternalRanking = ( throw new Error("External and internal ranking doesn't match"); } return externalRankedVideos.map((externalVideo, idx) => { - return { ...externalVideo, ...internalRankedVideos[idx] }; + const final_score = + 0.4 * externalVideo.final_external_score + + 0.6 * internalRankedVideos[idx].internal_score; + return { ...externalVideo, ...internalRankedVideos[idx], final_score }; }); }; @@ -142,7 +145,7 @@ export const getWeightedExternalRanking = ( useOfChapters: video.normalized_score.useOfChapters * WEIGHTS.weight5, }; - const final_score = + const final_external_score = 0.5 * (weighted_score.date + weighted_score.dateXLikes) + 0.3 * weighted_score.dateXViews + 0.2 * weighted_score.useOfChapters; @@ -150,7 +153,7 @@ export const getWeightedExternalRanking = ( return { ...video, weighted_score, - final_score, + final_external_score, }; }); }; From e49b7b54dffb94d309176a4af77087972689368e Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 11:41:26 -0700 Subject: [PATCH 150/355] multiplying final internal score by 100 so it matches the data of the external score --- backend/src/feedback/feedback.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/feedback/feedback.ts b/backend/src/feedback/feedback.ts index fd0bd9f..5a53866 100644 --- a/backend/src/feedback/feedback.ts +++ b/backend/src/feedback/feedback.ts @@ -91,11 +91,12 @@ export const normalizeFeedbackFromFetchedVideos = ( return videos.map((video) => { return { ...video, - internal_score: normalize( - video.raw_internal_score, - minInternalFeedbackScore, - maxInternalFeedbackScore - ), + internal_score: + normalize( + video.raw_internal_score, + minInternalFeedbackScore, + maxInternalFeedbackScore + ) * 100, }; }); }; From 5318b4ed92dcbec0009436a5bef46c213b16d24e Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 11:48:20 -0700 Subject: [PATCH 151/355] adding debug routes for manual testing purposes --- backend/src/routes/debug.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/debug.ts b/backend/src/routes/debug.ts index 8b30fcc..884a567 100644 --- a/backend/src/routes/debug.ts +++ b/backend/src/routes/debug.ts @@ -1,6 +1,10 @@ import express, { Request } from "express"; -import { youtube_v3 } from "googleapis"; -import { getExternalRanking } from "../rating/ranking"; +import { run_v1, youtube_v3 } from "googleapis"; +import { + getExternalRanking, + getFinalRanking, + getInternalRanking, +} from "../rating/ranking"; import { getVideoDataFromJson, getVideoDetailByIds, @@ -31,6 +35,26 @@ debug.post("/", async (req, res, next) => { res.send(videosDetailed.data.items); }); +debug.get("/internal", async (req, res, next) => { + const videos = getVideoDataFromJson() as youtube_v3.Schema$Video[]; + const rankedVideos = await getFinalRanking(videos); + res.send( + rankedVideos.map((video) => { + return { + title: video.snippet.title, + description: video.snippet.description, + raw_score: video.raw_score, + normalized_score: video.normalized_score, + weighted_score: video.weighted_score, + final_external_score: video.final_external_score, + raw_internal_score: video.raw_internal_score, + final_internal_score: video.internal_score, + final_score: video.final_score, + }; + }) + ); +}); + debug.get("/json", (req, res, next) => { const videos = getVideoDataFromJson() as youtube_v3.Schema$Video[]; const rankedVideos = getExternalRanking(videos); @@ -42,7 +66,7 @@ debug.get("/json", (req, res, next) => { raw_score: video.raw_score, normalized_score: video.normalized_score, weighted_score: video.weighted_score, - final_score: video.final_score, + final_score: video.final_external_score, }; }) ); From 2072f4e0cb17da3c4877d0a024df1d98a18fcbab Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 14:05:14 -0700 Subject: [PATCH 152/355] adding algorithm to remove duplicates if they are found when fetching beginner and advanced videos --- backend/src/course/course.ts | 44 ++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/backend/src/course/course.ts b/backend/src/course/course.ts index 681373e..9a9ca0e 100644 --- a/backend/src/course/course.ts +++ b/backend/src/course/course.ts @@ -6,7 +6,10 @@ import { import { getVideoDetailByIds, getVideosByQuery } from "../rating/youtube"; import Parse from "parse/node"; import { IResource } from "../types/resource"; -import { IWeightedYoutubeVideo } from "../types/youtube"; +import { + IFinalRankingYoutubeVideo, + IWeightedYoutubeVideo, +} from "../types/youtube"; import { createResource } from "../resources/resources"; import { getImagesByQuery } from "../unsplash/unsplash"; import { updateFeedback } from "../feedback/feedback"; @@ -131,17 +134,44 @@ export const saveResources = async ( }; export const generateResources = async (name: string) => { - const rankedBeginner = await getTop3ByDifficulty(name, "beginner"); - const rankedAdvanced = await getTop3ByDifficulty(name, "advanced"); + return await getBeginnerAndAdvancedCourses(name); +}; +const getBeginnerAndAdvancedCourses = async (query: string) => { + const beginnerVideos = await getRankedVideos(`beginner ${query} tutorial`); + const top3AdvancedVideos = getTop3( + await getRankedVideos(`advanced ${query} tutorial`) + ); + const top3BeginnerVideos = getTop3BeginnerVideosWithoutDuplicates( + beginnerVideos, + top3AdvancedVideos + ); return { - beginner: rankedBeginner, - advanced: rankedAdvanced, + beginner: top3BeginnerVideos, + advanced: top3AdvancedVideos, }; }; -const getTop3ByDifficulty = async (query: string, difficulty: string) => { - const rankedVideos = await getRankedVideos(`${difficulty} ${query} tutorial`); +const getTop3BeginnerVideosWithoutDuplicates = ( + beginnerVideos: IFinalRankingYoutubeVideo[], + top3AdvancedVideos: IFinalRankingYoutubeVideo[] +) => { + const selectedBeginnerVideos = new Set(); + + const top3BeginnersVideos: IFinalRankingYoutubeVideo[] = []; + + // We have to iterate over all videos. And then break as soon as we have 3 + for (const video of beginnerVideos) { + if (top3AdvancedVideos.find((v) => v.id === video.id)) { + continue; + } + top3BeginnersVideos.push(video); + if (top3BeginnersVideos.length === 3) break; + } + return top3BeginnersVideos; +}; + +const getTop3 = (rankedVideos: IFinalRankingYoutubeVideo[]) => { const splicedRankedVideos = rankedVideos.splice(0, 3); return splicedRankedVideos; }; From 1ca6f62e3089fa3ea8e43bfa91d7361ee09383d4 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 16:40:57 -0700 Subject: [PATCH 153/355] Adding feed button to the navbar --- frontend/src/components/Navbar.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 4d09398..96dcd22 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -49,6 +49,9 @@ const Navbar = () => { + From 045a8ad45c53e7a2586d3e6c9d18b18a997ab68a Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Fri, 22 Jul 2022 17:09:11 -0700 Subject: [PATCH 154/355] Adding feed card that shows a post inside the feed --- frontend/src/components/Feed/FeedCard.tsx | 53 +++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 frontend/src/components/Feed/FeedCard.tsx diff --git a/frontend/src/components/Feed/FeedCard.tsx b/frontend/src/components/Feed/FeedCard.tsx new file mode 100644 index 0000000..66b244e --- /dev/null +++ b/frontend/src/components/Feed/FeedCard.tsx @@ -0,0 +1,53 @@ +import { + Avatar, + Badge, + Box, + Button, + Flex, + Heading, + HStack, + Text, +} from "@chakra-ui/react"; +import React from "react"; +import useThemeColor from "../../hooks/useThemeColor"; + +const FeedCard = () => { + const { backgroundColor, borderColor } = useThemeColor(); + + return ( + + + + + + Mauricio + 24 hours ago + + + + Lorem ipsum dolor sit amet consectetur, adipisicing elit. Suscipit + officia eaque vero odio sit, cupiditate quo itaque maxime, quis, at + distinctio nihil consequatur quae. Aut recusandae debitis magnam + accusamus harum! + + + + + + Object Oriented Programming + Beginner + + Generated 24 hours ago + + + + + ); +}; + +export default FeedCard; From a6d6123916b79155a9a6e473e9ab4686cdbc5e9f Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 10:02:17 -0700 Subject: [PATCH 155/355] Adding props to FeedCard so we can have multiple feed content --- frontend/src/components/Feed/FeedCard.tsx | 35 +++++++++++++++-------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/Feed/FeedCard.tsx b/frontend/src/components/Feed/FeedCard.tsx index 66b244e..39fc3c2 100644 --- a/frontend/src/components/Feed/FeedCard.tsx +++ b/frontend/src/components/Feed/FeedCard.tsx @@ -11,7 +11,23 @@ import { import React from "react"; import useThemeColor from "../../hooks/useThemeColor"; -const FeedCard = () => { +interface Props { + username: string; + content: string; + courseName: string; + courseId: string; + courseDifficulty: number; + createdAt: string; +} + +const FeedCard = ({ + content, + courseName, + username, + createdAt, + courseDifficulty, + courseId, +}: Props) => { const { backgroundColor, borderColor } = useThemeColor(); return ( @@ -25,24 +41,19 @@ const FeedCard = () => { - Mauricio - 24 hours ago + {username} + {createdAt} - - Lorem ipsum dolor sit amet consectetur, adipisicing elit. Suscipit - officia eaque vero odio sit, cupiditate quo itaque maxime, quis, at - distinctio nihil consequatur quae. Aut recusandae debitis magnam - accusamus harum! - + {content} - Object Oriented Programming - Beginner + {courseName} + {courseDifficulty === 1 ? "Beginner" : "Advanced"} - Generated 24 hours ago + {createdAt} From dbd1941112468812349e58c7279a08f8229d4295 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 10:28:15 -0700 Subject: [PATCH 156/355] Adding types for post so it mathces the backend --- frontend/src/types/post.d.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 frontend/src/types/post.d.ts diff --git a/frontend/src/types/post.d.ts b/frontend/src/types/post.d.ts new file mode 100644 index 0000000..945c674 --- /dev/null +++ b/frontend/src/types/post.d.ts @@ -0,0 +1,7 @@ +import { DbObject } from "./global"; + +interface IPost extends DbObject { + content: string; + user: string; + course: id; +} From b81bb2fd91806759333cd032b51a4acf01ea660f Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 10:28:57 -0700 Subject: [PATCH 157/355] including nested pointers inside backend so it also fetches the username and course from a post --- backend/src/post/post.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/post/post.ts b/backend/src/post/post.ts index d9538ba..4760c96 100644 --- a/backend/src/post/post.ts +++ b/backend/src/post/post.ts @@ -27,6 +27,7 @@ export const getPostsByUser = async (userId: string) => { user.id = userId; query.equalTo("user", user); - + query.includeAll(); + query.select(["user.username", "createdAt", "content"]); return await query.findAll(); }; From 11ed56fd7942f7ed51b062baf5807b7467a71708 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 10:29:24 -0700 Subject: [PATCH 158/355] Displaying in the frontend post fetched from the backend --- frontend/src/components/Feed/FeedIndex.tsx | 36 +++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Feed/FeedIndex.tsx b/frontend/src/components/Feed/FeedIndex.tsx index f03b02a..20bd3ab 100644 --- a/frontend/src/components/Feed/FeedIndex.tsx +++ b/frontend/src/components/Feed/FeedIndex.tsx @@ -1,7 +1,41 @@ +import { Box, Container, VStack } from "@chakra-ui/react"; +import axios from "axios"; import React from "react"; +import { useQuery } from "react-query"; +import { IPost } from "../../types/post"; +import { getConfig, useSession } from "../../utils/auth"; +import { baseURL } from "../../utils/constants"; +import FeedCard from "./FeedCard"; const FeedIndex = () => { - return
FeedIndex
; + const { user } = useSession(); + + const { data: posts } = useQuery( + "posts", + async () => { + if (!user) { + throw new Error("User is not defined"); + } + + const res = await axios.get( + `${baseURL}/post/me`, + getConfig(user?.sessionToken) + ); + return res.data; + }, + { enabled: !!user } + ); + + return ( + + + {posts && + posts.map((post) => ( + + ))} + + + ); }; export default FeedIndex; From c4353b9c70e951fb8a9d45dd7dcc503d024e9c66 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 10:29:54 -0700 Subject: [PATCH 159/355] modifying post type so it contains relations --- frontend/src/types/post.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/types/post.d.ts b/frontend/src/types/post.d.ts index 945c674..bb4cd18 100644 --- a/frontend/src/types/post.d.ts +++ b/frontend/src/types/post.d.ts @@ -1,7 +1,9 @@ +import { ICourse } from "./course"; import { DbObject } from "./global"; +import { IUser } from "./user"; interface IPost extends DbObject { content: string; - user: string; - course: id; + user: IUser; + course?: ICourse; } From 54823ad86901b6f447cd84bc540b427b56a4af73 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 10:34:15 -0700 Subject: [PATCH 160/355] Conditional rendering course component based if the post contains a course or not --- frontend/src/components/Feed/FeedCard.tsx | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Feed/FeedCard.tsx b/frontend/src/components/Feed/FeedCard.tsx index 39fc3c2..81bece2 100644 --- a/frontend/src/components/Feed/FeedCard.tsx +++ b/frontend/src/components/Feed/FeedCard.tsx @@ -37,7 +37,7 @@ const FeedCard = ({ borderWidth={1} borderRadius={4} > - + @@ -47,16 +47,18 @@ const FeedCard = ({ {content} - - - - {courseName} - {courseDifficulty === 1 ? "Beginner" : "Advanced"} - - {createdAt} - - - + {courseId && ( + + + + {courseName} + {courseDifficulty === 1 ? "Beginner" : "Advanced"} + + {createdAt} + + + + )} ); }; From 6f0ea246d2fe54f2158734dea9170aed2fc9788d Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 10:53:02 -0700 Subject: [PATCH 161/355] installing date fns to handle javascript dates more easily --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 756c190..13e16bd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@emotion/react": "^11", "@emotion/styled": "^11", "axios": "^0.27.2", + "date-fns": "^2.29.1", "formik": "^2.2.9", "framer-motion": "^6", "react": "^18.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3ef488d..acb5ecc 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -16,6 +16,7 @@ specifiers: '@vitejs/plugin-react': ^1.3.0 axios: ^0.27.2 cypress: ^10.3.0 + date-fns: ^2.29.1 eslint: ^8.18.0 eslint-plugin-react: ^7.30.1 formik: ^2.2.9 @@ -39,6 +40,7 @@ dependencies: '@emotion/react': 11.9.3_4jaruczdv2uxjj3lb2xbkiuci4 '@emotion/styled': 11.9.3_toiz7tndcw4z2b7gxmmeo5fkcu axios: 0.27.2 + date-fns: 2.29.1 formik: 2.2.9_react@18.2.0 framer-motion: 6.3.16_biqbaboplfbrettd7655fr4n2y react: 18.2.0 @@ -2081,6 +2083,11 @@ packages: assert-plus: 1.0.0 dev: true + /date-fns/2.29.1: + resolution: {integrity: sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw==} + engines: {node: '>=0.11'} + dev: false + /dayjs/1.11.3: resolution: {integrity: sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==} dev: true From 6bf41de2181e7290e665d4446c27d031f33153e8 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 10:53:29 -0700 Subject: [PATCH 162/355] adding formatDistanceToNow that returns in text how old a post ist --- frontend/src/components/Feed/FeedCard.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Feed/FeedCard.tsx b/frontend/src/components/Feed/FeedCard.tsx index 81bece2..924884e 100644 --- a/frontend/src/components/Feed/FeedCard.tsx +++ b/frontend/src/components/Feed/FeedCard.tsx @@ -10,13 +10,14 @@ import { } from "@chakra-ui/react"; import React from "react"; import useThemeColor from "../../hooks/useThemeColor"; +import { formatDistanceToNow } from "date-fns"; interface Props { username: string; content: string; - courseName: string; - courseId: string; - courseDifficulty: number; + courseName?: string; + courseId?: string; + courseDifficulty?: number; createdAt: string; } @@ -36,13 +37,16 @@ const FeedCard = ({ borderColor={borderColor} borderWidth={1} borderRadius={4} + w="100%" > - + {username} - {createdAt} + + {formatDistanceToNow(new Date(createdAt))} ago + {content} From df9b9ab307f619664fc72bed12f9c0d2c288007b Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 11:26:46 -0700 Subject: [PATCH 163/355] changing updateResourceStatus function so it also creates a post when finishing a resource --- backend/src/resources/resources.ts | 16 +++++++++++++++- backend/src/routes/resources.ts | 16 ++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/backend/src/resources/resources.ts b/backend/src/resources/resources.ts index 6fbd331..ba247e1 100644 --- a/backend/src/resources/resources.ts +++ b/backend/src/resources/resources.ts @@ -1,6 +1,8 @@ import { IResource, IResourceStatus } from "../types/resource"; import Parse from "parse/node"; import { updateFeedback } from "../feedback/feedback"; +import { createPost } from "../post/post"; +import { getCourseByUserAndId } from "../course/course"; const SCORE_PER_COMPLETED = 0.1; export const createResource = (resource: IResource) => { @@ -55,8 +57,12 @@ export const getResourcesFromCourseAndDifficulty = async ( }; export const updateResourceStatus = async ( + resourceName: string, resourceId: string, - status: IResourceStatus + status: IResourceStatus, + courseName: string, + courseId: string, + user: Parse.User ) => { const Resource = new Parse.Object("Resource"); Resource.set("objectId", resourceId); @@ -65,6 +71,14 @@ export const updateResourceStatus = async ( if (status === "completed") { const resource = await findResourceById(resourceId); updateFeedback(resource[0].get("videoId"), SCORE_PER_COMPLETED); + + const post = createPost( + `Finished ${resourceName} from the course ${courseName}`, + user, + courseId + ); + + await post.save(); } return Resource; diff --git a/backend/src/routes/resources.ts b/backend/src/routes/resources.ts index e6b6d05..148a6ec 100644 --- a/backend/src/routes/resources.ts +++ b/backend/src/routes/resources.ts @@ -34,15 +34,23 @@ resources.get("/byCourse/:courseId", async (req: RequestWUser, res, next) => { resources.put( "/updateStatus/:resourceId", async (req: RequestWUser, res, next) => { + const { user } = req; const { resourceId } = req.params; - const { status } = req.body; + const { status, courseName, courseId, resourceName } = req.body; - if (!status) { - next(new BadRequestError("Status is missing")); + if (!status || !courseName || !courseId || !resourceName) { + next(new BadRequestError("Missing params")); return; } - const resource = await updateResourceStatus(resourceId, status); + const resource = await updateResourceStatus( + resourceName, + resourceId, + status, + courseName, + courseId, + user + ); const result = await resource.save(); res.send(result); From 652f92b4630e952ddaf46def5a95297262b85afd Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 11:27:19 -0700 Subject: [PATCH 164/355] prop drilling the appropiate params required to create a post when finishing a resource --- frontend/src/components/Hub/HubIndex.tsx | 2 ++ frontend/src/components/Hub/ResourceGroup.tsx | 6 +++++- frontend/src/components/Hub/VideoCard.tsx | 15 +++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Hub/HubIndex.tsx b/frontend/src/components/Hub/HubIndex.tsx index eb48155..e3bd792 100644 --- a/frontend/src/components/Hub/HubIndex.tsx +++ b/frontend/src/components/Hub/HubIndex.tsx @@ -89,6 +89,8 @@ const HubIndex = () => { title="🎥 Recommended Videos" kind="video" data={data} + courseId={id || ""} + courseName={title || ""} /> )} diff --git a/frontend/src/components/Hub/ResourceGroup.tsx b/frontend/src/components/Hub/ResourceGroup.tsx index 1db8f40..3613a96 100644 --- a/frontend/src/components/Hub/ResourceGroup.tsx +++ b/frontend/src/components/Hub/ResourceGroup.tsx @@ -8,9 +8,11 @@ interface Props { title: string; kind: "video" | "documentation" | "both"; data: IResource[]; + courseId: string; + courseName: string; } -const ResourceGroup = ({ title, kind, data }: Props) => { +const ResourceGroup = ({ title, kind, data, courseId, courseName }: Props) => { return ( @@ -31,6 +33,8 @@ const ResourceGroup = ({ title, kind, data }: Props) => { src={resource.type === "video" ? resource.thumbnail : ""} status={resource.status} href={resource.url} + courseId={courseId} + courseName={courseName} /> ))} {kind === "documentation" && ( diff --git a/frontend/src/components/Hub/VideoCard.tsx b/frontend/src/components/Hub/VideoCard.tsx index e516953..4d27ffc 100644 --- a/frontend/src/components/Hub/VideoCard.tsx +++ b/frontend/src/components/Hub/VideoCard.tsx @@ -9,6 +9,7 @@ import { baseURL } from "../../utils/constants"; import { IResourceStatus } from "../../types/resource"; import { useParams, useSearchParams } from "react-router-dom"; import { getConfig, useSession } from "../../utils/auth"; +import { StringSchema } from "yup"; type ResourceStatus = "completed" | "in progress" | "not started"; interface Props { @@ -17,6 +18,8 @@ interface Props { title: string; href: string; status: ResourceStatus; + courseId: string; + courseName: string; } const getBadgeColor = (status: ResourceStatus) => { @@ -26,7 +29,15 @@ const getBadgeColor = (status: ResourceStatus) => { return "gray"; }; -const VideoCard = ({ src, title, href, status, objectId }: Props) => { +const VideoCard = ({ + src, + title, + href, + status, + objectId, + courseId, + courseName, +}: Props) => { const { backgroundColor, borderColor } = useThemeColor(); const badgeColor = getBadgeColor(status); @@ -43,7 +54,7 @@ const VideoCard = ({ src, title, href, status, objectId }: Props) => { if (!user) throw new Error("No user"); const res = await axios.put( `${baseURL}/resources/updateStatus/${objectId}`, - { status }, + { status, resourceName: title, courseName, courseId }, getConfig(user?.sessionToken) ); return res.data; From b1139d098f6492889cd7105f57a94b90147dacdf Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 11:27:55 -0700 Subject: [PATCH 165/355] saving correctly the course relation inside the backend and removing includeAl --- backend/src/post/post.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/post/post.ts b/backend/src/post/post.ts index 4760c96..48f42ed 100644 --- a/backend/src/post/post.ts +++ b/backend/src/post/post.ts @@ -11,7 +11,9 @@ export const createPost = ( Post.set("user", user); if (courseId) { - Post.set("course", courseId); + const Course = new Parse.Object("Course"); + Course.set("objectId", courseId); + Post.set("course", Course); } return Post; @@ -28,6 +30,5 @@ export const getPostsByUser = async (userId: string) => { query.equalTo("user", user); query.includeAll(); - query.select(["user.username", "createdAt", "content"]); return await query.findAll(); }; From dca1319ee3681f5a24166b7c40e0cc172d5b65d7 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 11:28:17 -0700 Subject: [PATCH 166/355] adding params to FeedCard so it also shows the course when you can copy it --- frontend/src/components/Feed/FeedIndex.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Feed/FeedIndex.tsx b/frontend/src/components/Feed/FeedIndex.tsx index 20bd3ab..b7ebbaf 100644 --- a/frontend/src/components/Feed/FeedIndex.tsx +++ b/frontend/src/components/Feed/FeedIndex.tsx @@ -31,7 +31,14 @@ const FeedIndex = () => { {posts && posts.map((post) => ( - + ))} From bc2ff287d29c3375df544eec1a85ac4c73bcfe8d Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 12:57:01 -0700 Subject: [PATCH 167/355] Using the cloneCourse mutation again to clone a course inside the user feed --- frontend/src/components/Feed/FeedCard.tsx | 66 ++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Feed/FeedCard.tsx b/frontend/src/components/Feed/FeedCard.tsx index 924884e..53f3039 100644 --- a/frontend/src/components/Feed/FeedCard.tsx +++ b/frontend/src/components/Feed/FeedCard.tsx @@ -7,10 +7,18 @@ import { Heading, HStack, Text, + useToast, } from "@chakra-ui/react"; import React from "react"; import useThemeColor from "../../hooks/useThemeColor"; import { formatDistanceToNow } from "date-fns"; +import axios, { AxiosError } from "axios"; +import { useMutation } from "react-query"; +import { ICourse } from "../../types/course"; +import { ErrorType } from "../../types/requests"; +import { getConfig, useSession } from "../../utils/auth"; +import { baseURL } from "../../utils/constants"; +import { useNavigate } from "react-router-dom"; interface Props { username: string; @@ -21,6 +29,11 @@ interface Props { createdAt: string; } +interface CloneForm { + name: string; + code: string; +} + const FeedCard = ({ content, courseName, @@ -31,6 +44,52 @@ const FeedCard = ({ }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); + const toast = useToast(); + const navigate = useNavigate(); + const { user } = useSession(); + + const cloneCourse = useMutation( + async ({ name, code }: CloneForm) => { + if (!user) { + throw new Error("User not defined"); + return; + } + const res = await axios.post( + `${baseURL}/course/clone/${code}`, + { + name, + }, + getConfig(user?.sessionToken) + ); + return res.data; + }, + { + onSuccess: (course) => { + toast({ + status: "success", + title: "Course cloned!", + description: "The course has successfully been cloned", + isClosable: true, + }); + navigate(`/courses/${course?.objectId}/difficulty`); + }, + onError: (error: AxiosError) => { + toast({ + title: "An error ocurred", + description: error.response?.data.message, + status: "error", + duration: 5000, + isClosable: true, + }); + }, + } + ); + + const handleCloneCourse = () => { + if (!courseId || !courseName) return; + cloneCourse.mutate({ code: courseId, name: courseName }); + }; + return ( {createdAt} - + )} From 5d33e2155f62708c1aee59a9668074fd03248f49 Mon Sep 17 00:00:00 2001 From: Mauricio Munoz Date: Mon, 25 Jul 2022 13:35:39 -0700 Subject: [PATCH 168/355] Formatting courseCreatingAt to display when trying to clone a course from the feed --- frontend/src/components/Feed/FeedCard.tsx | 10 +++++++--- frontend/src/components/Feed/FeedIndex.tsx | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Feed/FeedCard.tsx b/frontend/src/components/Feed/FeedCard.tsx index 53f3039..40c2e67 100644 --- a/frontend/src/components/Feed/FeedCard.tsx +++ b/frontend/src/components/Feed/FeedCard.tsx @@ -27,6 +27,7 @@ interface Props { courseId?: string; courseDifficulty?: number; createdAt: string; + courseCreatedAt?: string; } interface CloneForm { @@ -39,8 +40,8 @@ const FeedCard = ({ courseName, username, createdAt, - courseDifficulty, courseId, + courseCreatedAt, }: Props) => { const { backgroundColor, borderColor } = useThemeColor(); @@ -115,9 +116,12 @@ const FeedCard = ({ {courseName} - {courseDifficulty === 1 ? "Beginner" : "Advanced"} - {createdAt} + {courseCreatedAt && ( + + Created {formatDistanceToNow(new Date(courseCreatedAt))} ago + + )}