feat: implement course edition

This commit is contained in:
swve 2023-08-21 18:35:48 +02:00
parent 8c860a9516
commit b582eaacfc
3 changed files with 154 additions and 19 deletions

View file

@ -2,7 +2,7 @@
import React, { FC, use, useEffect, useReducer } from 'react' import React, { FC, use, useEffect, useReducer } from 'react'
import { swrFetcher } from "@services/utils/ts/requests"; import { swrFetcher } from "@services/utils/ts/requests";
import { getAPIUrl, getUriWithOrg } from '@services/config/config'; import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import useSWR from 'swr'; import useSWR, { mutate } from 'swr';
import { getCourseThumbnailMediaDirectory } from '@services/media/media'; import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import Link from 'next/link'; import Link from 'next/link';
import CourseEdition from '../subpages/CourseEdition'; import CourseEdition from '../subpages/CourseEdition';
@ -10,14 +10,18 @@ import CourseContentEdition from '../subpages/CourseContentEdition';
import ErrorUI from '@components/StyledElements/Error/Error'; import ErrorUI from '@components/StyledElements/Error/Error';
import { updateChaptersMetadata } from '@services/courses/chapters'; import { updateChaptersMetadata } from '@services/courses/chapters';
import { Check, SaveAllIcon, Timer } from 'lucide-react'; import { Check, SaveAllIcon, Timer } from 'lucide-react';
import Loading from '../../loading';
import { updateCourse } from '@services/courses/courses';
function CourseEditClient({ courseid, subpage, params }: { courseid: string, subpage: string, params: any }) { function CourseEditClient({ courseid, subpage, params }: { courseid: string, subpage: string, params: any }) {
const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/meta/course_${courseid}`, swrFetcher); const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/meta/course_${courseid}`, swrFetcher);
const { data: course_meta, error: course_meta_error, isLoading: course_meta_isloading } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`, swrFetcher); const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseid}`, swrFetcher);
const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {}); const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {});
const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {});
const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true); const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true);
function courseChaptersReducer(state: any, action: any) { function courseChaptersReducer(state: any, action: any) {
switch (action.type) { switch (action.type) {
case 'updated_chapter': case 'updated_chapter':
@ -28,6 +32,16 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
} }
} }
function courseReducer(state: any, action: any) {
switch (action.type) {
case 'updated_course':
// action will contain the entire state, just update the entire state
return action.payload;
default:
throw new Error();
}
}
function savedContentReducer(state: any, action: any) { function savedContentReducer(state: any, action: any) {
switch (action.type) { switch (action.type) {
case 'saved_content': case 'saved_content':
@ -39,13 +53,16 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
} }
} }
function saveCourse() { async function saveCourse() {
if (subpage.toString() === 'content') { if (subpage.toString() === 'content') {
updateChaptersMetadata(courseid, courseChaptersMetadata) await updateChaptersMetadata(courseid, courseChaptersMetadata)
dispatchSavedContent({ type: 'saved_content' }) dispatchSavedContent({ type: 'saved_content' })
await mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`)
} }
else if (subpage.toString() === 'general') { else if (subpage.toString() === 'general') {
console.log('general') await updateCourse(courseid, courseState)
dispatchSavedContent({ type: 'saved_content' })
await mutate(`${getAPIUrl()}courses/course_${courseid}`)
} }
} }
@ -54,23 +71,27 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta }) dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta })
dispatchSavedContent({ type: 'saved_content' }) dispatchSavedContent({ type: 'saved_content' })
} }
}, [chapters_meta]) if (course) {
dispatchCourseMetadata({ type: 'updated_course', payload: course })
dispatchSavedContent({ type: 'saved_content' })
}
}, [chapters_meta, course])
return ( return (
<> <>
<div className='bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'> <div className='bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'> <div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
{course_meta_isloading && <div className='text-sm text-gray-500'>Loading...</div>} {course_isloading && <div className='text-sm text-gray-500'>Loading...</div>}
{course_meta && <> {course && <>
<div className='flex items-center'><div className='info flex space-x-5 items-center grow'> <div className='flex items-center'><div className='info flex space-x-5 items-center grow'>
<div className='flex'> <div className='flex'>
<Link href={getUriWithOrg(course_meta.course.orgslug, "") + `/course/${courseid}`}> <Link href={getUriWithOrg(course.orgslug, "") + `/course/${courseid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course_meta.course.org_id, course_meta.course.course_id, course_meta.course.thumbnail)}`} alt="" /> <img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.org_id, "course_" + courseid, course.thumbnail)}`} alt="" />
</Link> </Link>
</div> </div>
<div className="flex flex-col "> <div className="flex flex-col ">
<div className='text-sm text-gray-500'>Edit Course</div> <div className='text-sm text-gray-500'>Edit Course</div>
<div className='text-2xl font-bold first-letter:uppercase'>{course_meta.course.name}</div> <div className='text-2xl font-bold first-letter:uppercase'>{course.name}</div>
</div> </div>
</div> </div>
<div className='flex space-x-5 items-center'> <div className='flex space-x-5 items-center'>
@ -100,19 +121,22 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
</div> </div>
</div> </div>
</div> </div>
<CoursePageViewer dispatchSavedContent={dispatchSavedContent} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseid={courseid} orgslug={params.params.orgslug} /> <CoursePageViewer dispatchSavedContent={dispatchSavedContent} courseState={courseState} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseMetadata={dispatchCourseMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseid={courseid} orgslug={params.params.orgslug} />
</> </>
) )
} }
const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any }) => { const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchCourseMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any, courseState: any }) => {
if (subpage.toString() === 'general') { if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) {
return <CourseEdition /> return <CourseEdition data={courseState} dispatchCourseMetadata={dispatchCourseMetadata} dispatchSavedContent={dispatchSavedContent} />
} }
else if (subpage.toString() === 'content') { else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) {
return <CourseContentEdition data={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} courseid={courseid} orgslug={orgslug} /> return <CourseContentEdition data={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} courseid={courseid} orgslug={orgslug} />
} }
else if (subpage.toString() === 'content' || subpage.toString() === 'general') {
return <Loading />
}
else { else {
return <ErrorUI /> return <ErrorUI />
} }

View file

@ -1,9 +1,114 @@
import React from 'react' "use client";
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input, Textarea } from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form';
import { useFormik } from 'formik';
import { AlertTriangle } from "lucide-react";
import React from "react";
const validate = (values: any) => {
const errors: any = {};
if (!values.name) {
errors.name = 'Required';
}
if (values.name.length > 100) {
errors.name = 'Must be 80 characters or less';
}
if (!values.mini_description) {
errors.mini_description = 'Required';
}
if (values.mini_description.length > 200) {
errors.mini_description = 'Must be 200 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 CourseEdition(props: any) {
const [error, setError] = React.useState('');
const formik = useFormik({
initialValues: {
name: String(props.data.name),
mini_description: String(props.data.mini_description),
description: String(props.data.description),
learnings: String(props.data.learnings),
},
validate,
onSubmit: async values => {
},
});
React.useEffect(() => {
// This code will run whenever form values are updated
if (formik.values !== formik.initialValues) {
props.dispatchSavedContent({ type: 'unsaved_content' });
const updatedCourse = {
...props.data,
name: formik.values.name,
mini_description: formik.values.mini_description,
description: formik.values.description,
learnings: formik.values.learnings.split(", "),
};
props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse });
}
}, [formik.values, formik.initialValues]);
function CourseEdition() {
return ( return (
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'> <div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
Course Edition <div className="login-form">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label='Name' message={formik.errors.name} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control>
</FormField>
<FormField name="mini_description">
<FormLabelAndMessage label='Mini description' message={formik.errors.mini_description} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.mini_description} type="text" required />
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label='Description' message={formik.errors.description} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
</Form.Control>
</FormField>
<FormField name="learnings">
<FormLabelAndMessage label='Learnings (Separated by , )' message={formik.errors.learnings} />
<Form.Control asChild>
<Textarea placeholder='Science, Design, Architecture' style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
</Form.Control>
</FormField>
</FormLayout>
</div>
</div> </div>
) )
} }

View file

@ -24,6 +24,12 @@ export async function getCourseMetadataWithAuthHeader(course_id: any, next: any,
return res; return res;
} }
export async function updateCourse(course_id: any, data: any) {
const result: any = await fetch(`${getAPIUrl()}courses/course_${course_id}`, RequestBody("PUT", data, null));
const res = await errorHandling(result);
return res;
}
export async function getCourse(course_id: string, next: any) { export async function getCourse(course_id: string, next: any) {
const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("GET", null, next)); const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("GET", null, next));
const res = await errorHandling(result); const res = await errorHandling(result);