CLASSROOMIO-T-21: Course landing page + editor

This commit is contained in:
Rotimi Best
2022-12-30 13:08:54 +01:00
parent 38eae9dafb
commit 963d899adf
80 changed files with 2543 additions and 653 deletions
+7 -1
View File
@@ -21,4 +21,10 @@ export const PUBLIC_ROUTES = [
ROUTE.RESET,
];
export const ROUTES_TO_HIDE_NAV = [...PUBLIC_ROUTES, ROUTE.ONBOARDING];
export const ROUTES_TO_HIDE_NAV = [
ROUTE.LOGIN,
ROUTE.SIGN_UP,
ROUTE.FORGOT,
ROUTE.RESET,
ROUTE.ONBOARDING,
];
+51
View File
@@ -0,0 +1,51 @@
export function formatYoutubeVideo(url: string, errors: { video: string }) {
console.log(`url`, url);
const prefix = 'https://www.youtube.com/embed';
// https://www.youtube.com/embed/qajK1J1neAM
if (url.includes('embed')) {
return url;
}
// https://youtu.be/qajK1J1neAM
if (url.includes('.be/')) {
const splittedUrlWithBe = url.split('.be/');
return `${prefix}/${splittedUrlWithBe[1]}`;
}
// https://www.youtube.com/watch?v=qajK1J1neAM
const splitedUrl = url.split('watch');
if (splitedUrl.length !== 2) {
errors.video = 'Not a valid youtube link';
return;
}
const query = new URLSearchParams(splitedUrl[1]);
return `${prefix}/${query.get('v')}`;
}
export function getEmbedId(url: string) {
// https://www.youtube.com/embed/qajK1J1neAM
if (url.includes('embed')) {
return url.split('embed/')[1];
}
// https://youtu.be/qajK1J1neAM
if (url.includes('.be/')) {
const splittedUrlWithBe = url.split('.be/');
return splittedUrlWithBe[1];
}
// https://www.youtube.com/watch?v=qajK1J1neAM
const splitedUrl = url.split('watch');
if (splitedUrl.length !== 2) {
return '';
}
const query = new URLSearchParams(splitedUrl[1]);
return query.get('v');
}
@@ -1,11 +1,13 @@
export function isUUID(uuid) {
export function isUUID(uuid: string) {
let s = '' + uuid;
s = s.match(
const m: RegExpMatchArray | null = s.match(
'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
);
if (s === null) {
if (m === null) {
return false;
}
return true;
}
View File
@@ -1,22 +1,24 @@
import { supabase } from '../../functions/supabase';
import { isUUID } from '../../functions/isUUID';
import { QUESTION_TYPE } from '../../../components/Question/constants';
import type {
Lesson,
Course,
Group,
Groupmember,
Exercise,
} from '../../../utils/types';
export async function fetchCourse(courseId, session) {
console.log(`courseId`, courseId);
let jsonUser;
export async function fetchCourse(
courseId?: Course['id'],
slug?: Course['slug']
) {
let match: { slug?: string; id?: string } = {};
if (!process.browser) {
// try {
// jsonUser = JSON.parse(session.user);
// } catch (error) {
// console.log('Token is expired probably cause user not found');
// return {
// data: [],
// cantFetch: true,
// };
// // return this.redirect(301, '/login');
// }
if (slug) {
match.slug = slug;
} else {
match.id = courseId;
}
const { data, error } = await supabase
@@ -33,14 +35,19 @@ export async function fetchCourse(courseId, session) {
profile(*)
)
),
slug,
cost,
currency,
metadata,
lessons:lesson(
id, title,public, lesson_at, call_url, is_complete, order, created_at,
note, video_url, slide_url, call_url, totalExercises:exercise(count),
profile:teacher_id(id, avatar_url, fullname)
),
attendance:group_attendance(*)
`
)
.match({ id: courseId })
.match(match)
.single();
console.log(`error`, error);
if (!data || error) {
@@ -56,7 +63,7 @@ export async function fetchCourse(courseId, session) {
};
}
export async function fetchGroup(groupId) {
export async function fetchGroup(groupId: Group['id']) {
const { data, error } = await supabase
.from('group')
.select(`*,members:groupmember(*,profile(*))`)
@@ -69,7 +76,10 @@ export async function fetchGroup(groupId) {
};
}
export async function setProfileIdOfGroupMember(email, profileId) {
export async function setProfileIdOfGroupMember(
email: string,
profileId: string
) {
const { data, error } = await supabase
.from('groupmember')
.update({
@@ -84,13 +94,41 @@ export async function setProfileIdOfGroupMember(email, profileId) {
};
}
export async function updateCourse(courseId, course) {
if (course.avatar) {
export async function uploadAvatar(courseId: string, avatar: string) {
const filename = `course/${courseId + Date.now()}.webp`;
let logo;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
if (data && data.Key) {
const { publicURL } = supabase.storage
.from('avatars')
.getPublicUrl(filename);
if (!publicURL) return;
logo = publicURL;
}
return logo;
}
export async function updateCourse(
courseId: Course['id'],
avatar: string | undefined,
course: Course
) {
if (avatar) {
const filename = `course/${courseId + Date.now()}.webp`;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, course.avatar, {
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
@@ -100,12 +138,12 @@ export async function updateCourse(courseId, course) {
.from('avatars')
.getPublicUrl(filename);
if (!publicURL) return;
course.logo = publicURL;
}
}
delete course.avatar;
await supabase
.from('course')
.update(course, { returning: 'minimal' })
@@ -114,28 +152,28 @@ export async function updateCourse(courseId, course) {
return course.logo;
}
export async function deleteCourse(courseId) {
export async function deleteCourse(courseId: Course['id']) {
return await supabase.from('course').delete().match({ id: courseId });
}
export function addGroupMember(member) {
export function addGroupMember(member: any) {
return supabase.from('groupmember').insert(member, {
returning: 'minimal',
});
}
export function updatedGroupMember(update, match) {
export function updatedGroupMember(update: any, match: any) {
return supabase
.from('groupmember')
.update(update, { returning: 'minimal' })
.match(match);
}
export function deleteGroupMember(groupMemberId) {
export function deleteGroupMember(groupMemberId: Groupmember['id']) {
return supabase.from('groupmember').delete().match({ id: groupMemberId });
}
export function fetchLesson(lessonId) {
export function fetchLesson(lessonId: Lesson['id']) {
return supabase
.from('lesson')
.select(
@@ -145,35 +183,39 @@ export function fetchLesson(lessonId) {
.single();
}
export function createLesson(lesson) {
export function createLesson(lesson: any) {
return supabase.from('lesson').insert(lesson);
}
export function updateLesson(lesson, lessonId) {
export function updateLesson(lesson: any, lessonId: Lesson['id']) {
return supabase
.from('lesson')
.update({ ...lesson, id: undefined }, { returning: 'minimal' })
.match({ id: lessonId });
}
export function deleteLesson(lessonId) {
export function deleteLesson(lessonId: Lesson['id']) {
// Need to implement soft delete
return supabase.from('lesson').delete().match({ id: lessonId });
}
export function createExercise(exercise) {
export function createExercise(exercise: any) {
return supabase.from('exercise').insert(exercise);
}
export function fetchExercisesByMarks(courseId) {
export function fetchExercisesByMarks(courseId: Course['id']) {
return supabase.rpc('get_exercises').eq('course_id', courseId);
}
function isNew(item) {
function isNew(item: any) {
return isNaN(item);
}
export async function upsertExercise(questionnaire, exerciseId) {
// TODO: Add questionnaire type
export async function upsertExercise(
questionnaire: any,
exerciseId: Exercise['id']
) {
const {
questions,
title,
@@ -263,10 +305,12 @@ export async function upsertExercise(questionnaire, exerciseId) {
// Delete cause this is not a field in the table
delete newQuestion.question_type_id;
// @ts-ignore
newQuestion.question_type = { id: question_type_id };
newQuestion.id = id;
newQuestion.name = name;
newQuestion.order = order;
// @ts-ignore
newQuestion.options = [];
// Don't map options for 'Paragraph' questions
@@ -297,9 +341,11 @@ export async function upsertExercise(questionnaire, exerciseId) {
if (is_dirty || isNew(option.id)) {
const { data } = await supabase.from('option').upsert(newOption);
if (Array.isArray(data)) {
// @ts-ignore
newQuestion.options.push(data[0]);
}
} else {
// @ts-ignore
newQuestion.options.push(newOption);
}
}
@@ -312,12 +358,16 @@ export async function upsertExercise(questionnaire, exerciseId) {
return updatedQuestions;
}
interface LooseObject {
[key: string]: any;
}
export async function submitExercise(
answers,
questions,
exerciseId,
courseId,
groupMemberId
answers: Array<string>,
questions: Array<{ name: string; id: string }>,
exerciseId: Exercise['id'],
courseId: Course['id'],
groupMemberId: Groupmember['id'] | undefined
) {
if (!groupMemberId) {
return;
@@ -326,7 +376,7 @@ export async function submitExercise(
const questionsByName = questions.reduce(
(acc, q) => ({ ...acc, [q.name]: q.id }),
{}
);
) as LooseObject;
const questionAnswers = [];
const { data: submission } = await supabase.from('submission').insert({
@@ -343,7 +393,7 @@ export async function submitExercise(
question_id: questionsByName[questionName],
open_answer: '',
answers: [],
submission_id: submission[0].id,
submission_id: Array.isArray(submission) ? submission[0].id : null,
};
if (typeof value === 'string') {
@@ -359,7 +409,10 @@ export async function submitExercise(
console.log(`res`, res);
}
export async function deleteExercise(questions, exerciseId) {
export async function deleteExercise(
questions: Array<{ id: string }>,
exerciseId: Exercise['id']
) {
for (const question of questions) {
const { id } = question;
+79 -68
View File
@@ -1,25 +1,30 @@
export * from './config';
export * from './dashboard';
export interface CurrentSessionType {
access_token: string;
expires_at: number;
expires_in: number;
refresh_token: string;
token_type: string;
}
export interface UserType {
openAuthModal: boolean;
fetchingUser: boolean | null;
isLoggedIn: boolean;
currentSession: CurrentSessionType | null;
expiresAt: number;
interface CourseMetadata {
requirements?: string;
description?: string;
goals?: string;
videoUrl?: '';
showDiscount: false;
discount: 0;
reward?: {
show: boolean;
description: string;
};
instructor?: {
name: string;
role: string;
coursesNo: number;
description: string;
imgUrl: string;
};
}
// Generated from https://supabase-schema.vercel.app/
export interface Organization {
id: string /* primary key */;
name: string;
name: any; // type unknown;
}
export interface Profile {
@@ -29,27 +34,30 @@ export interface Profile {
avatar_url?: string;
created_at?: string;
updated_at?: string;
email?: string;
email?: any; // type unknown;
can_add_course?: boolean;
role?: any; // type unknown;
goal?: any; // type unknown;
source?: any; // type unknown;
}
export interface Question_type {
id: number /* primary key */;
label: string;
label: any; // type unknown;
created_at?: string;
updated_at?: string;
typename?: string;
typename?: any; // type unknown;
}
export interface Submissionstatus {
id: number /* primary key */;
label: string;
label: any; // type unknown;
updated_at?: string;
}
export interface Group {
id: string /* primary key */;
name: string;
name: any; // type unknown;
description?: string;
created_at?: string;
updated_at?: string;
@@ -58,8 +66,8 @@ export interface Group {
}
export interface Role {
type: string;
description?: string;
type: any; // type unknown;
description?: any; // type unknown;
id: number /* primary key */;
updated_at?: string;
created_at?: string;
@@ -67,24 +75,27 @@ export interface Role {
export interface Waitinglist {
id: number /* primary key */;
email: string;
email: any; // type unknown;
created_at?: string;
}
export interface Course {
created_at?: string;
description: string;
group_id?: string /* foreign key to group.id */;
group?: Group;
title: any; // type unknown;
description: any; // type unknown;
overview?: any; // type unknown;
id: string /* primary key */;
is_template?: boolean;
lessons: Lesson[];
logo?: string;
organization_id?: string /* foreign key to organization.id */;
organization?: Organization;
overview?: string;
title: string;
created_at?: string;
updated_at?: string;
group_id?: string /* foreign key to group.id */;
is_template?: boolean;
organization_id?: string /* foreign key to organization.id */;
logo?: string;
slug?: any; // type unknown;
metadata?: CourseMetadata;
cost?: number;
currency?: string;
group?: Group;
organization?: Organization;
}
export interface Groupmember {
@@ -92,9 +103,9 @@ export interface Groupmember {
group_id: string /* foreign key to group.id */;
role_id: number /* foreign key to role.id */;
profile_id?: string /* foreign key to profile.id */;
email?: string;
email?: any; // type unknown;
created_at?: string;
assigned_student_id?: string;
assigned_student_id?: any; // type unknown;
group?: Group;
role?: Role;
profile?: Profile;
@@ -123,46 +134,47 @@ export interface Group_attendance {
}
export interface Lesson {
call_url?: string;
note?: any; // type unknown;
video_url?: any; // type unknown;
slide_url?: any; // type unknown;
course_id: string /* foreign key to course.id */;
created_at: string;
exercise?: Exercise[];
id: string /* primary key */;
is_complete?: boolean;
lesson_at?: string;
note?: string;
order: number;
profile?: Profile;
public?: boolean;
slide_url?: string;
teacher_id?: string /* foreign key to profile.id */;
title: string;
created_at?: string;
updated_at?: string;
video_url?: string;
title: any; // type unknown;
public?: boolean;
lesson_at?: string;
teacher_id?: string /* foreign key to profile.id */;
is_complete?: boolean;
call_url?: string;
order?: number;
course?: Course;
profile?: Profile;
}
export interface Exercise {
title: any; // type unknown;
description?: any; // type unknown;
lesson_id?: string /* foreign key to lesson.id */;
created_at?: string;
description?: string;
due_by?: string;
id: string /* primary key */;
lesson_id: string /* foreign key */;
questions?: Question[];
title: string;
updated_at?: string;
id: string /* primary key */;
due_by?: string;
lesson?: Lesson;
}
export interface Question {
created_at?: string;
exercise_id: string /* foreign key to exercise.id */;
id: number /* primary key */;
name?: string;
options: Option[];
order?: number;
points?: string;
question_type_id: number /* foreign key to question_type.id */;
title: string;
title: any; // type unknown;
created_at?: string;
updated_at?: string;
exercise_id: string /* foreign key to exercise.id */;
name?: string;
points?: any; // type unknown;
order?: number;
question_type?: Question_type;
exercise?: Exercise;
}
export interface Submission {
@@ -182,15 +194,14 @@ export interface Submission {
}
export interface Option {
created_at?: string;
id?: number /* primary key */;
id: number /* primary key */;
label: any; // type unknown;
is_correct: boolean;
is_dirty?: boolean;
label: string;
question_id: number /* foreign key to question.id */;
question?: Question;
updated_at?: string;
value?: string;
created_at?: string;
updated_at?: string;
question?: Question;
}
export interface Question_answer {