From c39d9d53409b9ed711568d0d10dde4bf6c70d506 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 13 Dec 2023 15:56:12 +0100 Subject: [PATCH] feat: courses dashboard --- .../orgs/[orgslug]/dash/courses/client.tsx | 109 ++++++++++++ .../course/[courseuuid]/[subpage]/page.tsx | 47 ++++-- .../app/orgs/[orgslug]/dash/courses/page.tsx | 67 ++++++-- apps/web/app/orgs/[orgslug]/dash/layout.tsx | 2 +- .../CourseContext.tsx | 3 +- .../EditCourseGeneral/EditCourseGeneral.tsx | 156 ++++++++++++++++++ .../Buttons/NewActivityButton.tsx | 12 +- .../DraggableElements/ActivityElement.tsx | 62 +++++-- .../DraggableElements/ChapterElement.tsx | 35 +++- .../EditCourseStructure.tsx | 42 ++++- .../UI/BreadCrumbs.tsx | 2 +- .../UI/CourseOverviewTop.tsx | 2 +- .../UI/LeftMenu.tsx | 5 +- .../UI/SaveState.tsx | 18 +- .../Modals/Course/Create/CreateCourse.tsx | 4 +- .../Objects/Other/CourseThumbnail.tsx | 2 +- .../StyledElements/Tooltip/Tooltip.tsx | 3 +- apps/web/package.json | 3 + apps/web/services/courses/chapters.ts | 2 +- apps/web/services/courses/courses.ts | 2 +- apps/web/styles/globals.css | 23 +++ pnpm-lock.yaml | 77 +++++++++ 22 files changed, 611 insertions(+), 67 deletions(-) create mode 100644 apps/web/app/orgs/[orgslug]/dash/courses/client.tsx rename apps/web/components/{Dashboard => DashboardPages}/CourseContext.tsx (93%) create mode 100644 apps/web/components/DashboardPages/EditCourseGeneral/EditCourseGeneral.tsx rename apps/web/components/{Dashboard => DashboardPages}/EditCourseStructure/Buttons/NewActivityButton.tsx (89%) rename apps/web/components/{Dashboard => DashboardPages}/EditCourseStructure/DraggableElements/ActivityElement.tsx (54%) rename apps/web/components/{Dashboard => DashboardPages}/EditCourseStructure/DraggableElements/ChapterElement.tsx (69%) rename apps/web/components/{Dashboard => DashboardPages}/EditCourseStructure/EditCourseStructure.tsx (69%) rename apps/web/components/{Dashboard => DashboardPages}/UI/BreadCrumbs.tsx (94%) rename apps/web/components/{Dashboard => DashboardPages}/UI/CourseOverviewTop.tsx (94%) rename apps/web/components/{Dashboard => DashboardPages}/UI/LeftMenu.tsx (63%) rename apps/web/components/{Dashboard => DashboardPages}/UI/SaveState.tsx (83%) diff --git a/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx b/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx new file mode 100644 index 00000000..a4fb9ad3 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx @@ -0,0 +1,109 @@ +'use client'; +import BreadCrumbs from '@components/DashboardPages/UI/BreadCrumbs' +import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse'; +import CourseThumbnail from '@components/Objects/Other/CourseThumbnail'; +import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; +import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'; +import Modal from '@components/StyledElements/Modal/Modal'; +import Link from 'next/link' +import { useSearchParams } from 'next/navigation'; +import React from 'react' + +type CourseProps = { + orgslug: string; + courses: any; + org_id: string; +} + +function CoursesHome(params: CourseProps) { + const searchParams = useSearchParams(); + const isCreatingCourse = searchParams.get('new') ? true : false; + const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse); + const orgslug = params.orgslug; + const courses = params.courses; + + + async function closeNewCourseModal() { + setNewCourseModal(false); + } + + return ( +
+
+
+ + +
+
Courses
+ + } + dialogTitle="Create Course" + dialogDescription="Create a new course" + dialogTrigger={ + + } + /> + +
+
+
+
+ {courses.map((course: any) => ( +
+ +
+ ))} + {courses.length == 0 && +
+
+
+ + + + +
+
+

No courses yet

+

Create a course to add content

+
+ + } + dialogTitle="Create Course" + dialogDescription="Create a new course" + dialogTrigger={ + } + /> + +
+
+ } +
+
+ ) +} + +export default CoursesHome \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx index d3a811f3..65432f5a 100644 --- a/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx @@ -1,16 +1,20 @@ 'use client'; -import EditCourseStructure from '../../../../../../../../components/Dashboard/EditCourseStructure/EditCourseStructure' -import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' +import EditCourseStructure from '../../../../../../../../components/DashboardPages/EditCourseStructure/EditCourseStructure' +import BreadCrumbs from '@components/DashboardPages/UI/BreadCrumbs' import PageLoading from '@components/Objects/Loaders/PageLoading'; import ClientComponentSkeleton from '@components/Utils/ClientComp'; import { getAPIUrl, getUriWithOrg } from '@services/config/config'; import { swrFetcher } from '@services/utils/ts/requests'; import React, { createContext, use, useEffect, useState } from 'react' import useSWR from 'swr'; -import { CourseProvider, useCourse } from '../../../../../../../../components/Dashboard/CourseContext'; -import SaveState from '@components/Dashboard/UI/SaveState'; +import { CourseProvider, useCourse } from '../../../../../../../../components/DashboardPages/CourseContext'; +import SaveState from '@components/DashboardPages/UI/SaveState'; import Link from 'next/link'; -import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop'; +import { CourseOverviewTop } from '@components/DashboardPages/UI/CourseOverviewTop'; +import { CSSTransition } from 'react-transition-group'; +import { motion } from 'framer-motion'; +import EditCourseGeneral from '@components/DashboardPages/EditCourseGeneral/EditCourseGeneral'; +import { GalleryVertical, GalleryVerticalEnd, Info } from 'lucide-react'; export type CourseOverviewParams = { orgslug: string, @@ -20,7 +24,6 @@ export type CourseOverviewParams = { export const CourseStructureContext = createContext({}) as any; - function CourseOverviewPage({ params }: { params: CourseOverviewParams }) { function getEntireCourseUUID(courseuuid: string) { @@ -30,23 +33,41 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) { return (
+
-
General
+
+ +
+ +
General
+
+
- -
Structure
+ +
+
+ +
Content
+
+ +
- -
- {params.subpage == 'structure' ? : ''} - + + {params.subpage == 'content' ? : ''} + {params.subpage == 'general' ? : ''} +
) diff --git a/apps/web/app/orgs/[orgslug]/dash/courses/page.tsx b/apps/web/app/orgs/[orgslug]/dash/courses/page.tsx index 8f422f1e..adb477a6 100644 --- a/apps/web/app/orgs/[orgslug]/dash/courses/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/courses/page.tsx @@ -1,22 +1,57 @@ -'use client'; -import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' -import Link from 'next/link' +import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'; +import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'; +import { getOrganizationContextInfo } from '@services/organizations/orgs'; +import { Metadata } from 'next'; +import { cookies } from 'next/headers'; import React from 'react' +import CoursesHome from './client'; + +type MetadataProps = { + params: { orgslug: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}; + +export async function generateMetadata( + { params }: MetadataProps, +): Promise { + + // Get Org context information + const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); + + // SEO + return { + title: "Courses — " + org.name, + description: org.description, + keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`, + robots: { + index: true, + follow: true, + nocache: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + } + }, + openGraph: { + title: "Courses — " + org.name, + description: org.description, + type: 'website', + }, + }; +} + +async function CoursesPage(params: any) { + const orgslug = params.params.orgslug; + const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); + const cookieStore = cookies(); + const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) + const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null); + -function CoursesHome() { return ( -
-
-
- -
-
Courses
-
-
- -
-
+ ) } -export default CoursesHome \ No newline at end of file +export default CoursesPage \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/layout.tsx b/apps/web/app/orgs/[orgslug]/dash/layout.tsx index b651d773..b906c09c 100644 --- a/apps/web/app/orgs/[orgslug]/dash/layout.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/layout.tsx @@ -1,4 +1,4 @@ -import LeftMenu from '@components/Dashboard/UI/LeftMenu' +import LeftMenu from '@components/DashboardPages/UI/LeftMenu' import AuthProvider from '@components/Security/AuthProvider' import React from 'react' diff --git a/apps/web/components/Dashboard/CourseContext.tsx b/apps/web/components/DashboardPages/CourseContext.tsx similarity index 93% rename from apps/web/components/Dashboard/CourseContext.tsx rename to apps/web/components/DashboardPages/CourseContext.tsx index a37d2a5e..7489def7 100644 --- a/apps/web/components/Dashboard/CourseContext.tsx +++ b/apps/web/components/DashboardPages/CourseContext.tsx @@ -1,4 +1,5 @@ 'use client'; +import PageLoading from '@components/Objects/Loaders/PageLoading'; import { getAPIUrl } from '@services/config/config'; import { swrFetcher } from '@services/utils/ts/requests'; import React, { createContext, useContext, useEffect, useReducer } from 'react' @@ -26,7 +27,7 @@ export function CourseProvider({ children, courseuuid }: { children: React.React }, [courseStructureData]); - if (!courseStructureData) return
Loading...
+ if (!courseStructureData) return return ( diff --git a/apps/web/components/DashboardPages/EditCourseGeneral/EditCourseGeneral.tsx b/apps/web/components/DashboardPages/EditCourseGeneral/EditCourseGeneral.tsx new file mode 100644 index 00000000..7530353a --- /dev/null +++ b/apps/web/components/DashboardPages/EditCourseGeneral/EditCourseGeneral.tsx @@ -0,0 +1,156 @@ +import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form'; +import { useFormik } from 'formik'; +import { AlertTriangle } from 'lucide-react' +import * as Switch from '@radix-ui/react-switch'; +import * as Form from '@radix-ui/react-form'; +import React from 'react' +import { useCourse, useCourseDispatch } from '../CourseContext'; + + +type EditCourseStructureProps = { + orgslug: string, + course_uuid?: string, +} + +const validate = (values: any) => { + const errors: any = {}; + + if (!values.name) { + errors.name = 'Required'; + } + + if (values.name.length > 100) { + errors.name = 'Must be 100 characters or less'; + } + + + if (!values.description) { + errors.description = 'Required'; + + } + + if (values.description.length > 1000) { + errors.description = 'Must be 1000 characters or less'; + } + + + if (!values.learnings) { + errors.learnings = 'Required'; + } + + return errors; +}; + + +function EditCourseGeneral(props: EditCourseStructureProps) { + const [error, setError] = React.useState(''); + const course = useCourse() as any; + const dispatchCourse = useCourseDispatch() as any; + + const courseStructure = course.courseStructure; + const formik = useFormik({ + initialValues: { + name: String(courseStructure.name), + description: String(courseStructure.description), + about: String(courseStructure.about), + learnings: String(courseStructure.learnings), + tags: String(courseStructure.tags), + public: String(courseStructure.public), + }, + validate, + onSubmit: async values => { + + }, + enableReinitialize: true, + }); + + + React.useEffect(() => { + // This code will run whenever form values are updated + if (formik.values !== formik.initialValues) { + dispatchCourse({ type: 'setIsNotSaved' }); + const updatedCourse = { + ...courseStructure, + name: formik.values.name, + description: formik.values.description, + about: formik.values.about, + learnings: formik.values.learnings, + tags: formik.values.tags, + public: formik.values.public, + } + dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse }); + } + + }, [course, formik.values, formik.initialValues]); + + return ( +
+ + {course.courseStructure && ( +
+ {error && ( +
+ +
{error}
+
+ )} + + + + + + + + + + + +