mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: implement course edition
This commit is contained in:
parent
8c860a9516
commit
b582eaacfc
3 changed files with 154 additions and 19 deletions
|
|
@ -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 />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue