CLASSROOMIO-T-80: Replace sapper with sveltekit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
export const blockedSubdomain = [
|
||||
'app',
|
||||
'apps',
|
||||
'play',
|
||||
'support',
|
||||
'help',
|
||||
'teachers',
|
||||
'church',
|
||||
'tech',
|
||||
'build',
|
||||
'community',
|
||||
'conference',
|
||||
'campaign',
|
||||
'blog',
|
||||
'forum',
|
||||
'mobile',
|
||||
'dev',
|
||||
'stage',
|
||||
'prod',
|
||||
'group',
|
||||
'groups',
|
||||
'academy',
|
||||
'schools',
|
||||
'training',
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
export const SIGNUP_FIELDS = {
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
export const RESET_FIELDS = {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
export const LOGIN_FIELDS = {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
export const FORGOT_PASSWORD_FIELDS = {
|
||||
email: '',
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
edit: 'edit',
|
||||
view: 'view',
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
export const themeImages = {
|
||||
standard: {
|
||||
card: 'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-card.svg',
|
||||
editor:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-editor.svg',
|
||||
play: 'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-play.svg',
|
||||
},
|
||||
mainland: {
|
||||
card: 'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-card.svg',
|
||||
editor:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-editor.svg',
|
||||
play: 'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-play.svg',
|
||||
},
|
||||
};
|
||||
|
||||
export const defOption = {
|
||||
id: 1,
|
||||
label: '',
|
||||
options: [],
|
||||
};
|
||||
|
||||
export const optionImage = {
|
||||
circle:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/circle.svg',
|
||||
spade:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/spade.svg',
|
||||
square:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/square.svg',
|
||||
pentagon:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/pentagon.svg',
|
||||
hexagon:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/hexagon.svg',
|
||||
triangle:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/triangle.svg',
|
||||
};
|
||||
|
||||
export const allOptions = [
|
||||
{
|
||||
id: 'circle',
|
||||
label: '',
|
||||
isCorrect: false,
|
||||
},
|
||||
{
|
||||
id: 'triangle',
|
||||
label: '',
|
||||
isCorrect: false,
|
||||
},
|
||||
{
|
||||
id: 'spade',
|
||||
label: '',
|
||||
isCorrect: false,
|
||||
},
|
||||
{
|
||||
id: 'square',
|
||||
label: '',
|
||||
isCorrect: false,
|
||||
},
|
||||
{
|
||||
id: 'pentagon',
|
||||
label: '',
|
||||
isCorrect: false,
|
||||
},
|
||||
{
|
||||
id: 'hexagon',
|
||||
label: '',
|
||||
isCorrect: false,
|
||||
},
|
||||
];
|
||||
export const booleanOptions = [
|
||||
{
|
||||
id: 'triangle',
|
||||
label: 'True',
|
||||
image:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/triangle.svg',
|
||||
},
|
||||
{
|
||||
id: 'square',
|
||||
label: 'False',
|
||||
image:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/square.svg',
|
||||
},
|
||||
];
|
||||
export const allThemes = [
|
||||
{
|
||||
id: 'standard',
|
||||
label: 'Standard',
|
||||
},
|
||||
{
|
||||
id: 'mainland',
|
||||
label: 'Mainland Bridge',
|
||||
},
|
||||
];
|
||||
|
||||
export const defQuestion = {
|
||||
id: new Date().getTime(),
|
||||
title: '',
|
||||
type: 'multichoice',
|
||||
options: [],
|
||||
timelimit: '10s',
|
||||
};
|
||||
|
||||
export const STEPS = {
|
||||
CONNECT_TO_PLAY: 'CONNECT_TO_PLAY',
|
||||
WAIT_FOR_PLAYERS: 'WAIT_FOR_PLAYERS',
|
||||
SHOW_NEXT_QUESTION: 'SHOW_NEXT_QUESTION',
|
||||
SCOREBOARD: 'SCOREBOARD',
|
||||
PODIUM: 'PODIUM',
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export const ROLE = {
|
||||
ADMIN: 1,
|
||||
TUTOR: 2,
|
||||
STUDENT: 3,
|
||||
};
|
||||
|
||||
export const ROLE_LABEL = {
|
||||
[ROLE.ADMIN]: 'Admin',
|
||||
[ROLE.TUTOR]: 'Tutor',
|
||||
[ROLE.STUDENT]: 'Student',
|
||||
};
|
||||
|
||||
export const ROLES = [
|
||||
{
|
||||
label: 'All',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: ROLE_LABEL[ROLE.ADMIN],
|
||||
value: ROLE.ADMIN,
|
||||
},
|
||||
{
|
||||
label: ROLE_LABEL[ROLE.TUTOR],
|
||||
value: ROLE.TUTOR,
|
||||
},
|
||||
{
|
||||
label: ROLE_LABEL[ROLE.STUDENT],
|
||||
value: ROLE.STUDENT,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
export const ROUTE = {
|
||||
HOME: '/',
|
||||
DASHBOARD: '/dashboard',
|
||||
ONBOARDING: '/onboarding',
|
||||
COURSES: '/courses',
|
||||
COURSE: '/course',
|
||||
LOGIN: '/login',
|
||||
SIGN_UP: '/signup',
|
||||
PROFILE: '/profile',
|
||||
PEOPLE: '/people',
|
||||
DISCUSSIONS: '/discussions',
|
||||
ASK: '/ask',
|
||||
FORGOT: '/forgot',
|
||||
RESET: '/reset',
|
||||
};
|
||||
|
||||
export const PUBLIC_ROUTES = [
|
||||
`^${ROUTE.HOME}$`,
|
||||
ROUTE.LOGIN,
|
||||
ROUTE.SIGN_UP,
|
||||
ROUTE.FORGOT,
|
||||
ROUTE.RESET,
|
||||
`^${ROUTE.COURSE}/.*`,
|
||||
];
|
||||
|
||||
export const ROUTES_TO_HIDE_NAV = [
|
||||
ROUTE.LOGIN,
|
||||
ROUTE.SIGN_UP,
|
||||
ROUTE.FORGOT,
|
||||
ROUTE.RESET,
|
||||
ROUTE.ONBOARDING,
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
export async function handleAuthChange(event, session) {
|
||||
await fetch('/api/auth', {
|
||||
method: 'POST',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ event, session }),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export function toggleBodyByTheme(isDark) {
|
||||
if (!document) return;
|
||||
|
||||
if (isDark) {
|
||||
document.body.className = document.body.className.concat(' ', 'dark');
|
||||
} else {
|
||||
document.body.className = document.body.className.replace(' dark', '');
|
||||
}
|
||||
}
|
||||
|
||||
export function isCoursesPage(path) {
|
||||
return /courses\/[a-z 0-9 -]/.test(path);
|
||||
}
|
||||
|
||||
export function isCoursePage(path) {
|
||||
return /course\/[a-z 0-9 -]/.test(path);
|
||||
}
|
||||
|
||||
export function isOrgPage(path) {
|
||||
return /org\/[a-z 0-9 -]/.test(path);
|
||||
}
|
||||
|
||||
export function isQuizPage(path) {
|
||||
return /org\/[a-z 0-9 -]+\/quiz\/[a-z 0-9 -]/.test(path);
|
||||
}
|
||||
|
||||
export function isLMSPage(path) {
|
||||
return /lms[\/a-z 0-9 -]*/.test(path);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export default (date) => {
|
||||
const d = new Date(date);
|
||||
|
||||
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(d);
|
||||
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(d);
|
||||
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(d);
|
||||
|
||||
// return `${ye}-${mo}-${da} ${d.toLocaleTimeString('en-US')}`;
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'medium',
|
||||
}).format(d);
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.match(/[^\W\s\d]*/g, '')
|
||||
.join('')
|
||||
.toLowerCase()
|
||||
.concat(`-${new Date().getTime()}`);
|
||||
}
|
||||
|
||||
export default generateSlug;
|
||||
@@ -0,0 +1,7 @@
|
||||
export default (currency: string | undefined) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export default (created_at, due_date) => {
|
||||
return !!due_date
|
||||
? !!(new Date(created_at).getTime() <= new Date(due_date).getTime())
|
||||
: true;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export function isUUID(uuid: string) {
|
||||
let s = '' + uuid;
|
||||
|
||||
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 (m === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function genQuizPin() {
|
||||
var minm = 100000;
|
||||
var maxm = 999999;
|
||||
return Math.floor(Math.random() * (maxm - minm + 1)) + minm;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function removeDuplicate(arr) {
|
||||
let s = new Set(arr);
|
||||
let it = s.values();
|
||||
return Array.from(it);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import dayjs from 'dayjs';
|
||||
import isYesterday from 'dayjs/plugin/isYesterday';
|
||||
import isToday from 'dayjs/plugin/isToday';
|
||||
import isTomorrow from 'dayjs/plugin/isTomorrow';
|
||||
|
||||
import type {
|
||||
UserLessonDataType,
|
||||
LessonsByMonthIndexType,
|
||||
} from '$lib/utils/types';
|
||||
|
||||
dayjs.extend(isYesterday);
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isTomorrow);
|
||||
|
||||
export const formatUserUpcomingData = (
|
||||
data: UserLessonDataType[]
|
||||
): LessonsByMonthIndexType => {
|
||||
const result: LessonsByMonthIndexType = {};
|
||||
|
||||
for (const item of data) {
|
||||
const { lesson_at } = item;
|
||||
const lessonAtDate = new Date(lesson_at);
|
||||
const monthIndex = lessonAtDate.getMonth();
|
||||
const dateIndex = lessonAtDate.getDate();
|
||||
|
||||
if (result[monthIndex]) {
|
||||
const prevDateData = result[monthIndex][dateIndex];
|
||||
|
||||
if (Array.isArray(prevDateData)) {
|
||||
result[monthIndex] = {
|
||||
...result[monthIndex],
|
||||
[dateIndex]: [...prevDateData, item],
|
||||
};
|
||||
} else {
|
||||
result[monthIndex] = { ...result[monthIndex], [dateIndex]: [item] };
|
||||
}
|
||||
} else {
|
||||
result[monthIndex] = { [dateIndex]: [item] };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const formatDate = (_selectedDate: string | undefined): string => {
|
||||
if (!_selectedDate) return '';
|
||||
|
||||
const selectedDate = dayjs(_selectedDate);
|
||||
|
||||
if (selectedDate.isToday()) {
|
||||
return 'Today';
|
||||
} else if (selectedDate.isYesterday()) {
|
||||
return 'Yesterday';
|
||||
} else if (selectedDate.isTomorrow()) {
|
||||
return 'Tomorrow';
|
||||
}
|
||||
|
||||
return selectedDate.format('ddd, MMM D');
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { PUBLIC_ROUTES } from '$lib/utils/constants/routes';
|
||||
|
||||
export default (route) => {
|
||||
return PUBLIC_ROUTES.some((publicRoute) => {
|
||||
const regex = new RegExp(publicRoute, 'g');
|
||||
return regex.test(route);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
function showAppsSideBar(path) {
|
||||
return path.includes('/lessons/') && !path.includes('landingpage');
|
||||
}
|
||||
|
||||
export default showAppsSideBar;
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
PUBLIC_SUPABASE_ANON_KEY,
|
||||
PUBLIC_SUPABASE_URL,
|
||||
} from '$env/static/public';
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
import type { ConfigType } from '../types';
|
||||
|
||||
export let supabase: SupabaseClient;
|
||||
|
||||
export const getSupabase = (config?: ConfigType) => {
|
||||
if (supabase) return supabase;
|
||||
|
||||
config = {
|
||||
supabaseConfig: {
|
||||
bucketPath:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.in/storage/v1/object/sign/',
|
||||
url: PUBLIC_SUPABASE_URL || '',
|
||||
anonKey: PUBLIC_SUPABASE_ANON_KEY || '',
|
||||
},
|
||||
};
|
||||
|
||||
supabase = createClient(
|
||||
config.supabaseConfig.url,
|
||||
config.supabaseConfig.anonKey
|
||||
);
|
||||
|
||||
return supabase;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export function validateEmail(email) {
|
||||
const re =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import get from 'lodash/get';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import z from 'zod';
|
||||
import { validateEmail } from './validateEmail';
|
||||
|
||||
const createQuizValidationSchema = z.object({
|
||||
title: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
});
|
||||
|
||||
const askCommunityValidationSchema = z.object({
|
||||
title: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
body: z.string().min(20, {
|
||||
message: 'Must be 10 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
});
|
||||
const commentInCommunityValidationSchema = z.object({
|
||||
comment: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
});
|
||||
|
||||
const forgotValidationSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: 'Invalid email address',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
});
|
||||
|
||||
const authValidationSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: 'Invalid email address',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
password: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
});
|
||||
|
||||
const resetValidationSchema = z.object({
|
||||
password: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty',
|
||||
}),
|
||||
});
|
||||
|
||||
const onboardingValidationSchema = {
|
||||
stepOne: z.object({
|
||||
fullname: z.string().min(1, { message: 'Enter your fullname' }),
|
||||
orgName: z.string().min(1, { message: 'Enter your organisation name' }),
|
||||
siteName: z.string().min(1, { message: 'Enter your site name' }),
|
||||
}),
|
||||
stepTwo: z.object({
|
||||
goal: z
|
||||
.string({
|
||||
required_error: 'Select an option',
|
||||
invalid_type_error: 'Select an option',
|
||||
})
|
||||
.min(1),
|
||||
source: z
|
||||
.string({
|
||||
required_error: 'Select an option',
|
||||
invalid_type_error: 'Select an option',
|
||||
})
|
||||
.min(1),
|
||||
}),
|
||||
};
|
||||
|
||||
export const getConfirmPasswordError = ({ password, confirmPassword }) => {
|
||||
return password > 6 && confirmPassword > 6 && password !== confirmPassword
|
||||
? 'Does not match password'
|
||||
: null;
|
||||
};
|
||||
|
||||
export const processErrors = (error, mapToId) => {
|
||||
const errors = {};
|
||||
|
||||
if (Array.isArray(error?.issues)) {
|
||||
for (const issue of error.issues) {
|
||||
const { message, path } = issue;
|
||||
|
||||
if (!path.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mapToId) {
|
||||
let value = '';
|
||||
path.forEach((p, i) => {
|
||||
const isLast = !path[i + 1];
|
||||
const formatP = isNumber(p) ? `[${p}]` : p;
|
||||
|
||||
value += !value ? formatP : `.${formatP}`;
|
||||
});
|
||||
const key = value.replace('label', 'id');
|
||||
errors[key] = value;
|
||||
} else {
|
||||
errors[path[0]] = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const authValidation = (fields = {}) => {
|
||||
const { error } = authValidationSchema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const resetValidation = (fields = {}) => {
|
||||
const { error } = resetValidationSchema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const forgotValidation = (fields = {}) => {
|
||||
const { error } = forgotValidationSchema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const onboardingValidation = (fields = {}, step) => {
|
||||
const schema =
|
||||
step === 1
|
||||
? onboardingValidationSchema.stepOne
|
||||
: onboardingValidationSchema.stepTwo;
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const createQuizValidation = (fields = {}) => {
|
||||
const { error } = createQuizValidationSchema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const askCommunityValidation = (fields = {}) => {
|
||||
const { error } = askCommunityValidationSchema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const commentInCommunityValidation = (fields = {}) => {
|
||||
const { error } = commentInCommunityValidationSchema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const getDisableSubmit = ({ password, confirmPassword }) => {
|
||||
return !!(password && confirmPassword && password !== confirmPassword);
|
||||
};
|
||||
|
||||
export const validateEmailInString = (emailsStr) => {
|
||||
let error = '';
|
||||
let hasError = false;
|
||||
|
||||
if (!emailsStr) {
|
||||
return {
|
||||
hasError: true,
|
||||
error: 'Enter an email',
|
||||
emails: [],
|
||||
};
|
||||
}
|
||||
|
||||
const emails = emailsStr.split(',').map((_email) => {
|
||||
const email = _email.trim();
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
hasError = true;
|
||||
error += !error.length ? `Not valid email: ${email}` : `, ${email}`;
|
||||
}
|
||||
|
||||
return email;
|
||||
});
|
||||
|
||||
return {
|
||||
hasError,
|
||||
error,
|
||||
emails,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export async function takeAttendance(update) {
|
||||
return supabase.from('group_attendance').upsert(update);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { PostgrestError } from '@supabase/supabase-js';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
// @ts-ignore
|
||||
import { addGroupMember } from './index';
|
||||
// @ts-ignore
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Course, Exercise, Lesson, Profile } from '$lib/utils/types';
|
||||
// @ts-ignore
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
// @ts-ignore
|
||||
import { QUESTION_TYPE } from '$lib/components/Question/constants';
|
||||
|
||||
type GetCourseResponse = {
|
||||
data: Course;
|
||||
error: PostgrestError | null;
|
||||
};
|
||||
|
||||
async function fetchCourseData(courseId: string): Promise<GetCourseResponse> {
|
||||
const { data, error } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
lessons:lesson(
|
||||
*,
|
||||
exercise:id(
|
||||
id, title, description, due_by,
|
||||
questions:question(
|
||||
*,
|
||||
options:option(*),
|
||||
question_type:question_type_id(id, label)
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.match({ id: courseId })
|
||||
.single();
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
async function cloneGroupAndBasicCourse(
|
||||
course: Course,
|
||||
newTitle: string
|
||||
): Promise<{ newCourse: Course }> {
|
||||
const { description } = course;
|
||||
|
||||
const { data: newGroup } = await supabase
|
||||
.from('group')
|
||||
.insert({ name: newTitle, description })
|
||||
.select();
|
||||
|
||||
console.log(`newGroup`, newGroup);
|
||||
if (!Array.isArray(newGroup)) {
|
||||
throw new Error('Group wasnt created successfully');
|
||||
}
|
||||
|
||||
const { id: group_id } = newGroup[0];
|
||||
|
||||
// 2. Create course with group_id
|
||||
const { data: newCourse } = await supabase
|
||||
.from('course')
|
||||
.insert({
|
||||
...course,
|
||||
title: newTitle,
|
||||
description,
|
||||
group_id,
|
||||
lessons: undefined,
|
||||
id: undefined,
|
||||
updated_at: undefined,
|
||||
created_at: undefined,
|
||||
})
|
||||
.select();
|
||||
|
||||
console.log(`newCourse`, newCourse);
|
||||
|
||||
// 3. Add group members
|
||||
|
||||
const profileStoreValue: Profile = get(profile);
|
||||
|
||||
// Get profile of author and add as member
|
||||
await addGroupMember({
|
||||
profile_id: profileStoreValue.id,
|
||||
email: profileStoreValue.email,
|
||||
group_id,
|
||||
role_id: ROLE.TUTOR,
|
||||
});
|
||||
|
||||
console.log('profileStoreValue', profileStoreValue);
|
||||
|
||||
return {
|
||||
newCourse: newCourse?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
async function cloneLessons(lessons: Lesson[], courseId: Course['id']) {
|
||||
const clonedLessons = lessons
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.created_at || '').getTime() -
|
||||
new Date(b.created_at || '').getTime()
|
||||
)
|
||||
.map((lesson, index) => ({
|
||||
call_url: lesson.call_url,
|
||||
course_id: courseId,
|
||||
is_complete: lesson.is_complete,
|
||||
lesson_at: new Date(),
|
||||
note: lesson.note,
|
||||
order: lesson.order || index + 1,
|
||||
public: lesson.public,
|
||||
slide_url: lesson.slide_url,
|
||||
title: lesson.title,
|
||||
video_url: lesson.video_url,
|
||||
}));
|
||||
|
||||
const { data } = await supabase.from('lesson').insert(clonedLessons).select();
|
||||
|
||||
const newLessons = data?.map((dataItem, dataIndex) => {
|
||||
return {
|
||||
...dataItem,
|
||||
// @ts-ignore
|
||||
exercise: lessons[dataIndex].exercise,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('newLessons', newLessons);
|
||||
return { newLessons };
|
||||
}
|
||||
|
||||
async function cloneExercises(lessons: Lesson[]) {
|
||||
console.log('lessons', lessons);
|
||||
|
||||
// Lessons
|
||||
for (const lesson of lessons) {
|
||||
// @ts-ignore
|
||||
const { id: lessonId, exercise = [] } = lesson;
|
||||
|
||||
// Exercises
|
||||
for (const exerciseItem of exercise) {
|
||||
const { title, description, questions = [] } = exerciseItem;
|
||||
// Insert
|
||||
const { data } = await supabase
|
||||
.from('exercise')
|
||||
.insert({
|
||||
description,
|
||||
due_by: new Date(),
|
||||
lesson_id: lessonId,
|
||||
title,
|
||||
})
|
||||
.select();
|
||||
const newExercise = Array.isArray(data) ? data[0] : data;
|
||||
console.log('newExercise', newExercise);
|
||||
|
||||
// Questions
|
||||
for (const question of questions) {
|
||||
const { options = [] } = question;
|
||||
|
||||
// Insert
|
||||
const { data } = await supabase
|
||||
.from('question')
|
||||
.insert({
|
||||
name: question.name,
|
||||
title: question.title,
|
||||
points: question.points,
|
||||
order: question.order,
|
||||
question_type_id: question.question_type_id,
|
||||
exercise_id: newExercise.id,
|
||||
})
|
||||
.select();
|
||||
const newQuestion = Array.isArray(data) ? data[0] : data;
|
||||
console.log('newQuestion', newQuestion);
|
||||
|
||||
// Options: Don't map options for 'Paragraph' questions
|
||||
if (QUESTION_TYPE.TEXTAREA !== newQuestion.question_type_id) {
|
||||
// @ts-ignore
|
||||
const newOptions = options.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
is_correct: option.is_correct,
|
||||
question_id: newQuestion.id,
|
||||
}));
|
||||
|
||||
// Insert[Batch]
|
||||
await supabase.from('option').insert(newOptions).select();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function cloneCourse(
|
||||
courseId: string,
|
||||
newTitle: string
|
||||
): Promise<Course> {
|
||||
const { data: course, error } = await fetchCourseData(courseId);
|
||||
console.log(`course`, course);
|
||||
|
||||
if (error) {
|
||||
console.error('Error when cloning', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Copy course table
|
||||
* 2. Copy lessons table
|
||||
* 3. Copy exercises table
|
||||
* 4. Copy questions table
|
||||
* 5. Copy options table
|
||||
*/
|
||||
|
||||
// tables: group, groupmember, course(group_id)
|
||||
const { newCourse } = await cloneGroupAndBasicCourse(course, newTitle);
|
||||
console.log('returned -> newCourse', newCourse);
|
||||
|
||||
// @ts-ignore
|
||||
const { newLessons } = await cloneLessons(course.lessons, newCourse.id);
|
||||
console.log('returned -> newLessons', newLessons);
|
||||
|
||||
const cloneExercisesRes = await cloneExercises(newLessons || []);
|
||||
console.log('returned -> cloneExercisesRes', cloneExercisesRes);
|
||||
|
||||
return newCourse;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
1. Click on 3 dots to clone course
|
||||
|
||||
2. Show a modal with a new name "Copy of previous course name"
|
||||
|
||||
-> Note the following information will be copied
|
||||
|
||||
3. Things to copy
|
||||
Overview
|
||||
Lessons
|
||||
Exercises - questions, options, everything basically.
|
||||
|
||||
Technical approach
|
||||
- Fetch all the data to the client
|
||||
- Manually call functions to create
|
||||
|
||||
- call a psql function with newname, course_id
|
||||
|
||||
- copy all in course table
|
||||
- copy all in group table by course_id
|
||||
- add only one group member as admin
|
||||
|
||||
- get all lessons in course
|
||||
- copy each one with unique ids
|
||||
|
||||
- get all exercises by course id
|
||||
- copy exercise
|
||||
|
||||
- get questions
|
||||
- copy questions
|
||||
|
||||
- get options
|
||||
- copy options
|
||||
*/
|
||||
@@ -0,0 +1,427 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { isUUID } from '$lib/utils/functions/isUUID';
|
||||
import { QUESTION_TYPE } from '$lib/components/Question/constants';
|
||||
import type {
|
||||
Lesson,
|
||||
Course,
|
||||
Group,
|
||||
Groupmember,
|
||||
Exercise,
|
||||
} from '$lib/utils/types';
|
||||
|
||||
export async function fetchCourse(
|
||||
courseId?: Course['id'],
|
||||
slug?: Course['slug']
|
||||
) {
|
||||
let match: { slug?: string; id?: string } = {};
|
||||
|
||||
if (slug) {
|
||||
match.slug = slug;
|
||||
} else {
|
||||
match.id = courseId;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
overview,
|
||||
logo,
|
||||
group(*,
|
||||
members:groupmember(*,
|
||||
profile(*)
|
||||
)
|
||||
),
|
||||
slug,
|
||||
cost,
|
||||
currency,
|
||||
metadata,
|
||||
is_certificate_downloadable,
|
||||
certificate_theme,
|
||||
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(match)
|
||||
.single();
|
||||
console.log(`error`, error);
|
||||
if (!data || error) {
|
||||
console.log(`data`, data);
|
||||
console.log(`fetchCourse => error`, error);
|
||||
// return this.redirect(301, '/courses');
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchGroup(groupId: Group['id']) {
|
||||
const { data, error } = await supabase
|
||||
.from('group')
|
||||
.select(`*,members:groupmember(*,profile(*))`)
|
||||
.match({ id: groupId })
|
||||
.single();
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setProfileIdOfGroupMember(
|
||||
email: string,
|
||||
profileId: string
|
||||
) {
|
||||
const { data, error } = await supabase
|
||||
.from('groupmember')
|
||||
.update({
|
||||
email: null,
|
||||
profile_id: profileId,
|
||||
})
|
||||
.match({ email: email });
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
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, avatar, {
|
||||
cacheControl: '3600',
|
||||
upsert: false,
|
||||
});
|
||||
|
||||
if (data && data.Key) {
|
||||
const { publicURL } = supabase.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl(filename);
|
||||
|
||||
if (!publicURL) return;
|
||||
|
||||
course.logo = publicURL;
|
||||
}
|
||||
}
|
||||
|
||||
await supabase.from('course').update(course).match({ id: courseId });
|
||||
|
||||
return course.logo;
|
||||
}
|
||||
|
||||
export async function deleteCourse(courseId: Course['id']) {
|
||||
return await supabase.from('course').delete().match({ id: courseId });
|
||||
}
|
||||
|
||||
export function addGroupMember(member: any) {
|
||||
return supabase.from('groupmember').insert(member).select();
|
||||
}
|
||||
|
||||
export function updatedGroupMember(update: any, match: any) {
|
||||
return supabase.from('groupmember').update(update).match(match);
|
||||
}
|
||||
|
||||
export function deleteGroupMember(groupMemberId: Groupmember['id']) {
|
||||
return supabase.from('groupmember').delete().match({ id: groupMemberId });
|
||||
}
|
||||
|
||||
export function fetchLesson(lessonId: Lesson['id']) {
|
||||
return supabase
|
||||
.from('lesson')
|
||||
.select(
|
||||
`id, note, video_url, slide_url, call_url, totalExercises:exercise(count)`
|
||||
)
|
||||
.eq('id', lessonId)
|
||||
.single();
|
||||
}
|
||||
|
||||
export function createLesson(lesson: any) {
|
||||
return supabase.from('lesson').insert(lesson).select();
|
||||
}
|
||||
|
||||
export function updateLesson(lesson: any, lessonId: Lesson['id']) {
|
||||
return supabase
|
||||
.from('lesson')
|
||||
.update({ ...lesson, id: undefined })
|
||||
.match({ id: lessonId });
|
||||
}
|
||||
|
||||
export function deleteLesson(lessonId: Lesson['id']) {
|
||||
// Need to implement soft delete
|
||||
return supabase.from('lesson').delete().match({ id: lessonId });
|
||||
}
|
||||
|
||||
export function createExercise(exercise: any) {
|
||||
return supabase.from('exercise').insert(exercise).select();
|
||||
}
|
||||
|
||||
export function fetchExercisesByMarks(courseId: Course['id']) {
|
||||
return supabase.rpc('get_exercises').eq('course_id', courseId);
|
||||
}
|
||||
|
||||
function isNew(item: any) {
|
||||
return isNaN(item);
|
||||
}
|
||||
|
||||
// TODO: Add questionnaire type
|
||||
export async function upsertExercise(
|
||||
questionnaire: any,
|
||||
exerciseId: Exercise['id']
|
||||
) {
|
||||
const {
|
||||
questions,
|
||||
title,
|
||||
description,
|
||||
due_by,
|
||||
is_title_dirty,
|
||||
is_description_dirty,
|
||||
is_due_by_dirty,
|
||||
} = questionnaire;
|
||||
|
||||
if (is_description_dirty || is_title_dirty || is_due_by_dirty) {
|
||||
await supabase
|
||||
.from('exercise')
|
||||
.update({
|
||||
title,
|
||||
description,
|
||||
due_by,
|
||||
})
|
||||
.match({ id: exerciseId });
|
||||
}
|
||||
|
||||
const updatedQuestions = [];
|
||||
|
||||
for (const question of questions) {
|
||||
const {
|
||||
title,
|
||||
id,
|
||||
name,
|
||||
question_type,
|
||||
options,
|
||||
deleted_at,
|
||||
order,
|
||||
points,
|
||||
is_dirty,
|
||||
} = question;
|
||||
|
||||
// DELETE /delete/:questionId - Don't delete if answer already given
|
||||
if (deleted_at) {
|
||||
// Delete from server only if this question exists in the database
|
||||
if (!isNew(id)) {
|
||||
await supabase.from('option').delete().match({ question_id: id });
|
||||
await supabase
|
||||
.from('question_answer')
|
||||
.delete()
|
||||
.match({ question_id: id });
|
||||
const { error } = await supabase
|
||||
.from('question')
|
||||
.delete()
|
||||
.match({ id });
|
||||
|
||||
if (error) {
|
||||
console.error('Cannot delete this question', error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the remaining operation so we filter out this question from the new questions array
|
||||
continue;
|
||||
}
|
||||
|
||||
// INSERT or UPDATE /update/:questionId or /insert/:questionId
|
||||
const newQuestion = {
|
||||
id: isNew(id) ? undefined : id,
|
||||
name: isNew(id) ? undefined : name,
|
||||
title,
|
||||
points,
|
||||
order,
|
||||
question_type_id: question_type.id,
|
||||
exercise_id: exerciseId,
|
||||
};
|
||||
let questionSupabaseRes;
|
||||
|
||||
if (is_dirty || isNew(id)) {
|
||||
const res = await supabase.from('question').upsert(newQuestion);
|
||||
|
||||
if (res.error) {
|
||||
console.error(`Upsert question`, res.error);
|
||||
}
|
||||
questionSupabaseRes = Array.isArray(res.data) ? res.data[0] : null;
|
||||
} else {
|
||||
questionSupabaseRes = Object.assign(newQuestion);
|
||||
}
|
||||
|
||||
if (questionSupabaseRes) {
|
||||
const { question_type_id, id, name, order } = questionSupabaseRes;
|
||||
|
||||
// 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
|
||||
if (QUESTION_TYPE.TEXTAREA !== question_type_id) {
|
||||
for (const option of options) {
|
||||
const { deleted_at, is_dirty } = option;
|
||||
|
||||
// DELETE /delete/:optionId
|
||||
if (deleted_at) {
|
||||
// if this option exists in the database
|
||||
if (!isNew(option.id)) {
|
||||
supabase.from('option').delete().match({ id: option.id });
|
||||
}
|
||||
|
||||
// Skip the remaining operation so we filter out this option from the new option array
|
||||
continue;
|
||||
}
|
||||
|
||||
// INSERT and UPDATE
|
||||
const newOption = {
|
||||
...option,
|
||||
is_dirty: undefined,
|
||||
id: isNew(option.id) ? undefined : option.id,
|
||||
value: isUUID(option.value) ? option.value : undefined, // this value is of UUID type
|
||||
question_id: newQuestion.id,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatedQuestions.push(newQuestion);
|
||||
}
|
||||
}
|
||||
|
||||
return updatedQuestions;
|
||||
}
|
||||
|
||||
interface LooseObject {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export async function submitExercise(
|
||||
answers: Array<string>,
|
||||
questions: Array<{ name: string; id: string }>,
|
||||
exerciseId: Exercise['id'],
|
||||
courseId: Course['id'],
|
||||
groupMemberId: Groupmember['id'] | undefined
|
||||
) {
|
||||
if (!groupMemberId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const questionsByName = questions.reduce(
|
||||
(acc, q) => ({ ...acc, [q.name]: q.id }),
|
||||
{}
|
||||
) as LooseObject;
|
||||
const questionAnswers = [];
|
||||
|
||||
const { data: submission } = await supabase
|
||||
.from('submission')
|
||||
.insert({
|
||||
submitted_by: groupMemberId,
|
||||
exercise_id: exerciseId,
|
||||
course_id: courseId,
|
||||
})
|
||||
.select();
|
||||
|
||||
for (const questionName in answers) {
|
||||
const value = answers[questionName];
|
||||
|
||||
const questionAnswer = {
|
||||
group_member_id: groupMemberId,
|
||||
question_id: questionsByName[questionName],
|
||||
open_answer: '',
|
||||
answers: [],
|
||||
submission_id: Array.isArray(submission) ? submission[0].id : null,
|
||||
};
|
||||
|
||||
if (typeof value === 'string') {
|
||||
questionAnswer.open_answer = value;
|
||||
} else {
|
||||
questionAnswer.answers = value;
|
||||
}
|
||||
|
||||
questionAnswers.push(questionAnswer);
|
||||
}
|
||||
|
||||
const res = await supabase
|
||||
.from('question_answer')
|
||||
.insert(questionAnswers)
|
||||
.select();
|
||||
console.log(`res`, res);
|
||||
}
|
||||
|
||||
export async function deleteExercise(
|
||||
questions: Array<{ id: string }>,
|
||||
exerciseId: Exercise['id']
|
||||
) {
|
||||
for (const question of questions) {
|
||||
const { id } = question;
|
||||
|
||||
await supabase.from('option').delete().match({ question_id: id });
|
||||
await supabase.from('question_answer').delete().match({ question_id: id });
|
||||
|
||||
await supabase.from('question').delete().match({ id });
|
||||
}
|
||||
|
||||
await supabase.from('submission').delete().match({ exercise_id: exerciseId });
|
||||
await supabase.from('exercise').delete().match({ id: exerciseId });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { UserLessonDataType } from '$lib/utils/types';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export async function fetchUserUpcomingData(
|
||||
profileId: string | null
|
||||
): Promise<UserLessonDataType[] | []> {
|
||||
if (!profileId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { data: userUpcomingData } = await supabase.rpc(
|
||||
'get_user_upcoming_lessons',
|
||||
{
|
||||
profile_id_arg: profileId,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`userUpcomingData`, userUpcomingData);
|
||||
|
||||
if (!Array.isArray(userUpcomingData)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return userUpcomingData;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export async function fetchMarks(courseId) {
|
||||
const { data } = await supabase.rpc('get_marks').eq('course_id', courseId);
|
||||
|
||||
return { data };
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import get from 'lodash/get';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { goto } from '$app/navigation';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { orgs, currentOrg, orgAudience, orgTeam } from '$lib/utils/store/org';
|
||||
import { ROLE, ROLE_LABEL } from '$lib/utils/constants/roles';
|
||||
|
||||
export async function getOrgTeam(orgId) {
|
||||
const { data, error } = await supabase
|
||||
.from('organizationmember')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
email,
|
||||
verified,
|
||||
role_id,
|
||||
profile(
|
||||
id,
|
||||
fullname,
|
||||
email
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('organization_id', orgId)
|
||||
.neq('role_id', ROLE.STUDENT)
|
||||
.order('id', { ascending: false });
|
||||
|
||||
const team = [];
|
||||
if (data?.length) {
|
||||
data.forEach((teamMember) => {
|
||||
team.push({
|
||||
id: teamMember.id,
|
||||
email: teamMember?.profile?.email || teamMember?.email,
|
||||
verified: teamMember?.verified,
|
||||
profileId: teamMember?.profile?.id,
|
||||
role: ROLE_LABEL[teamMember?.role_id] || '',
|
||||
isAdmin: teamMember?.role_id === ROLE.ADMIN,
|
||||
});
|
||||
});
|
||||
|
||||
orgTeam.set(team);
|
||||
}
|
||||
|
||||
return {
|
||||
team,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOrganizations(userId) {
|
||||
const { data, error } = await supabase
|
||||
.from('organizationmember')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
profile_id,
|
||||
organization!organizationmember_organization_id_fkey (
|
||||
id,
|
||||
name,
|
||||
siteName,
|
||||
avatar_url
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('profile_id', userId);
|
||||
|
||||
const orgsArray = [];
|
||||
if (Array.isArray(data) && data.length) {
|
||||
data.forEach((orgMember) => {
|
||||
orgsArray.push({
|
||||
id: orgMember?.organization?.id,
|
||||
name: orgMember?.organization?.name,
|
||||
shortName:
|
||||
orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || '',
|
||||
siteName: orgMember?.organization?.siteName,
|
||||
avatar_url: orgMember?.organization?.avatar_url,
|
||||
memberId: orgMember?.id,
|
||||
});
|
||||
});
|
||||
|
||||
orgs.set(orgsArray);
|
||||
currentOrg.set(orgsArray[0]);
|
||||
}
|
||||
|
||||
return {
|
||||
orgs: orgsArray,
|
||||
currentOrg: orgsArray[0],
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOrgAudience(orgId) {
|
||||
// get all students who are participants in any course belonging to an org
|
||||
const { data, error } = await supabase
|
||||
.from('profile')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
fullname,
|
||||
email,
|
||||
avatar_url,
|
||||
created_at,
|
||||
organizationmember!inner(
|
||||
role_id,
|
||||
organization_id
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('organizationmember.organization_id', orgId)
|
||||
.eq('organizationmember.role_id', 3); // is a student, tutor is 2 and admin is 1
|
||||
|
||||
console.log('data', data);
|
||||
|
||||
const audience = (data || []).map((profile) => ({
|
||||
id: profile.id,
|
||||
name: profile.fullname,
|
||||
email: profile.email,
|
||||
avatar_url: profile.avatar_url,
|
||||
date_joined: new Date(profile.created_at).toDateString(),
|
||||
}));
|
||||
orgAudience.set(audience);
|
||||
|
||||
return {
|
||||
audience: audience,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCourseBySiteName(siteName) {
|
||||
const { data, error } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
group!inner(
|
||||
organization!inner(id, name, siteName, avatar_url)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('group.organization.siteName', siteName)
|
||||
.eq('is_published', true);
|
||||
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
const org = get(data, '[0].group.organization', {
|
||||
id: '',
|
||||
name: 'ClassroomIO',
|
||||
shortName: '',
|
||||
siteName: '',
|
||||
avatar_url: '/logo-192.png',
|
||||
});
|
||||
currentOrg.set(org);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getCurrentOrg(siteName) {
|
||||
const { data, error } = await supabase
|
||||
.from('organization')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
name,
|
||||
siteName,
|
||||
avatar_url
|
||||
`
|
||||
)
|
||||
.eq('siteName', siteName);
|
||||
|
||||
if (error || isEmpty(data)) {
|
||||
console.error('Error getOrganization', error);
|
||||
return goto('/404');
|
||||
}
|
||||
|
||||
currentOrg.set(data[0]);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createQuizModal, deleteModal } from '$lib/utils/store/org';
|
||||
|
||||
export function onEdit(quiz) {}
|
||||
|
||||
export function onShare(quiz) {}
|
||||
|
||||
export function onReport(quiz) {}
|
||||
|
||||
export function onRename(quiz) {
|
||||
createQuizModal.update((m) => ({
|
||||
...m,
|
||||
id: quiz.id,
|
||||
title: quiz.title,
|
||||
openEdit: true,
|
||||
}));
|
||||
}
|
||||
|
||||
export function onMakeCopy(quiz) {}
|
||||
|
||||
export function onDelete(quiz) {
|
||||
deleteModal.update((m) => ({
|
||||
...m,
|
||||
open: true,
|
||||
id: quiz.id,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export function fetchSubmissionStatus() {
|
||||
return supabase.from('submissionstatus').select(`*`);
|
||||
}
|
||||
|
||||
export function fetchSubmissions(course_id) {
|
||||
return supabase
|
||||
.from('submission')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
created_at,
|
||||
answers:question_answer(*),
|
||||
exercise:exercise_id(
|
||||
id, title, due_by,
|
||||
lesson:lesson_id(id, title),
|
||||
questions:question(
|
||||
*,
|
||||
options:option(*),
|
||||
question_type:question_type_id(id, label)
|
||||
)
|
||||
),
|
||||
status_id,
|
||||
course:course_id(*),
|
||||
groupmember:submitted_by(
|
||||
profile(*)
|
||||
)
|
||||
`
|
||||
)
|
||||
.match({
|
||||
course_id,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchSubmission({ courseId, exerciseId, submittedBy }) {
|
||||
return supabase
|
||||
.from('submission')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
answers:question_answer(*),
|
||||
status_id
|
||||
`
|
||||
)
|
||||
.match({
|
||||
submitted_by: submittedBy,
|
||||
exercise_id: exerciseId,
|
||||
course_id: courseId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSubmission({ id, status_id, total }, otherArgs) {
|
||||
const toUpdate = {
|
||||
status_id,
|
||||
};
|
||||
|
||||
if (typeof total === 'number') {
|
||||
toUpdate.total = total;
|
||||
}
|
||||
|
||||
return supabase.from('submission').update(toUpdate, otherArgs).match({ id });
|
||||
}
|
||||
|
||||
export async function updateQuestionAnswer(update, match) {
|
||||
return supabase.from('question_answer').update(update).match(match);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const appStore = writable({
|
||||
isDark: false,
|
||||
isStudentDomain: false,
|
||||
siteNameFromDomain: '',
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const attendance = writable({});
|
||||
@@ -0,0 +1,48 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import { allOptions, STEPS, booleanOptions } from '../constants/quiz';
|
||||
|
||||
export const orgs = writable([]);
|
||||
export const currentOrg = writable({
|
||||
id: '',
|
||||
name: '',
|
||||
shortName: '',
|
||||
siteName: '',
|
||||
avatar_url: '',
|
||||
memberId: '',
|
||||
});
|
||||
export const orgAudience = writable([]);
|
||||
export const orgTeam = writable([]);
|
||||
export const currentOrgPath = derived(
|
||||
currentOrg,
|
||||
($currentOrg) => `/org/${$currentOrg.siteName}`
|
||||
);
|
||||
|
||||
// Quiz
|
||||
export const createQuizModal = writable({
|
||||
open: false,
|
||||
openEdit: false,
|
||||
title: '',
|
||||
id: null,
|
||||
});
|
||||
|
||||
export const deleteModal = writable({
|
||||
id: null,
|
||||
open: false,
|
||||
isQuestion: false,
|
||||
});
|
||||
|
||||
export const quizesStore = writable([]);
|
||||
|
||||
export const quizStore = writable({
|
||||
uuid: '',
|
||||
title: '',
|
||||
questions: [],
|
||||
timelimit: '10s',
|
||||
theme: 'standard',
|
||||
pin: '',
|
||||
});
|
||||
|
||||
export const playQuizStore = writable({
|
||||
step: STEPS.CONNECT_TO_PLAY,
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isMobile = writable(false);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const user = writable({
|
||||
openAuthModal: false,
|
||||
fetchingUser: false,
|
||||
isLoggedIn: false,
|
||||
currentSession: null,
|
||||
expiresAt: 0,
|
||||
});
|
||||
|
||||
export const profile = writable({
|
||||
id: null,
|
||||
fullname: '',
|
||||
avatar_url:
|
||||
'https://pbs.twimg.com/profile_images/1416443682157473795/dGtFbtht_normal.jpg',
|
||||
username: 'Elon',
|
||||
user_id: '',
|
||||
email: '',
|
||||
role: '',
|
||||
goal: '',
|
||||
source: '',
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export type ConfigType = {
|
||||
supabaseConfig: { bucketPath: string; url: string; anonKey: string };
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export type UserLessonDataType = {
|
||||
call_url: string;
|
||||
course_id: string;
|
||||
course_title: string;
|
||||
lesson_at: string;
|
||||
lesson_id: string;
|
||||
lesson_title: string;
|
||||
is_complete: boolean;
|
||||
};
|
||||
|
||||
type LessonByDayType = {
|
||||
[dayIndex: number]: UserLessonDataType[];
|
||||
};
|
||||
|
||||
export type LessonsByMonthIndexType = {
|
||||
[monthIndex: number]: LessonByDayType;
|
||||
};
|
||||
@@ -0,0 +1,231 @@
|
||||
export * from './config';
|
||||
export * from './dashboard';
|
||||
|
||||
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;
|
||||
};
|
||||
reviews?: Array<Review>;
|
||||
}
|
||||
|
||||
// Generated from https://supabase-schema.vercel.app/
|
||||
export interface Organization {
|
||||
id: string /* primary key */;
|
||||
name: any; // type unknown;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
id: string /* primary key */;
|
||||
fullname: string;
|
||||
username: string;
|
||||
avatar_url?: string;
|
||||
created_at?: string;
|
||||
updated_at?: 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: any; // type unknown;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
typename?: any; // type unknown;
|
||||
}
|
||||
|
||||
export interface Submissionstatus {
|
||||
id: number /* primary key */;
|
||||
label: any; // type unknown;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string /* primary key */;
|
||||
name: any; // type unknown;
|
||||
description?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
organization_id?: string /* foreign key to organization.id */;
|
||||
organization?: Organization;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
type: any; // type unknown;
|
||||
description?: any; // type unknown;
|
||||
id: number /* primary key */;
|
||||
updated_at?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface Waitinglist {
|
||||
id: number /* primary key */;
|
||||
email: any; // type unknown;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
title: any; // type unknown;
|
||||
description: any; // type unknown;
|
||||
overview?: any; // type unknown;
|
||||
id: string /* primary key */;
|
||||
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;
|
||||
is_certificate_downloadable?: boolean;
|
||||
certificate_theme?: string;
|
||||
}
|
||||
|
||||
export interface Groupmember {
|
||||
id: string /* primary key */;
|
||||
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?: any; // type unknown;
|
||||
created_at?: string;
|
||||
assigned_student_id?: any; // type unknown;
|
||||
group?: Group;
|
||||
role?: Role;
|
||||
profile?: Profile;
|
||||
}
|
||||
|
||||
export interface Organizationmember {
|
||||
id: number /* primary key */;
|
||||
organization_id: string /* foreign key to organization.id */;
|
||||
role_id: number /* foreign key to role.id */;
|
||||
profile_id: string /* foreign key to profile.id */;
|
||||
organization?: Organization;
|
||||
role?: Role;
|
||||
profile?: Profile;
|
||||
}
|
||||
|
||||
export interface Group_attendance {
|
||||
id: number /* primary key */;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
course_id?: string /* foreign key to course.id */;
|
||||
student_id?: string /* foreign key to groupmember.id */;
|
||||
is_present?: boolean;
|
||||
lesson_id: string;
|
||||
course?: Course;
|
||||
groupmember?: Groupmember;
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
note?: any; // type unknown;
|
||||
video_url?: any; // type unknown;
|
||||
slide_url?: any; // type unknown;
|
||||
course_id: string /* foreign key to course.id */;
|
||||
id: string /* primary key */;
|
||||
created_at?: string;
|
||||
updated_at?: 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;
|
||||
updated_at?: string;
|
||||
id: string /* primary key */;
|
||||
due_by?: string;
|
||||
lesson?: Lesson;
|
||||
}
|
||||
|
||||
export interface Question {
|
||||
id: number /* primary key */;
|
||||
question_type_id: number /* foreign key to question_type.id */;
|
||||
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 {
|
||||
id: string /* primary key */;
|
||||
reviewer_id?: number;
|
||||
status_id?: number /* foreign key to submissionstatus.id */;
|
||||
total?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
exercise_id: string /* foreign key to exercise.id */;
|
||||
submitted_by?: string /* foreign key to groupmember.id */;
|
||||
course_id?: string /* foreign key to course.id */;
|
||||
submissionstatus?: Submissionstatus;
|
||||
exercise?: Exercise;
|
||||
groupmember?: Groupmember;
|
||||
course?: Course;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
id: number /* primary key */;
|
||||
label: any; // type unknown;
|
||||
is_correct: boolean;
|
||||
question_id: number /* foreign key to question.id */;
|
||||
value?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
question?: Question;
|
||||
}
|
||||
|
||||
export interface Question_answer {
|
||||
id: number /* primary key */;
|
||||
answers?: any[];
|
||||
question_id: number /* foreign key to question.id */;
|
||||
open_answer?: string;
|
||||
group_member_id: string /* foreign key to groupmember.id */;
|
||||
submission_id?: string /* foreign key to submission.id */;
|
||||
point?: number;
|
||||
question?: Question;
|
||||
groupmember?: Groupmember;
|
||||
submission?: Submission;
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
id: number;
|
||||
hide: boolean;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
rating: number;
|
||||
created_at: number;
|
||||
description: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export interface ProfileType {}
|
||||
Reference in New Issue
Block a user