feat: 🚀 😃 convert repo into a monorepo 🚀 😃 (#51)
* Merge main into turbo-monrepo * feat: rename apps * feat: remove supbase videos and serve all from project * feat: move supabase to root * feat: update README * docs: update link
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
export const blockedSubdomain = [
|
||||
'app',
|
||||
'apps',
|
||||
'play',
|
||||
'support',
|
||||
'demo',
|
||||
'help',
|
||||
'teachers',
|
||||
'church',
|
||||
'tech',
|
||||
'build',
|
||||
'community',
|
||||
'conference',
|
||||
'campaign',
|
||||
'blog',
|
||||
'forum',
|
||||
'mobile',
|
||||
'dev',
|
||||
'stage',
|
||||
'prod',
|
||||
'group',
|
||||
'groups',
|
||||
'academy',
|
||||
'schools',
|
||||
'training',
|
||||
'cloud'
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
export const SIGNUP_FIELDS = {
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
};
|
||||
|
||||
export const RESET_FIELDS = {
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
};
|
||||
|
||||
export const LOGIN_FIELDS = {
|
||||
email: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
export const FORGOT_PASSWORD_FIELDS = {
|
||||
email: ''
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum STATUS {
|
||||
ACTIVE,
|
||||
ARCHIVE,
|
||||
DELETED
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
edit: 'edit',
|
||||
view: 'view'
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "Web Development Courses for",
|
||||
"titleHighlight": "Everyone",
|
||||
"subtitle": "Practical project-based courses that are easy to understand and straight to the point",
|
||||
"action": {
|
||||
"label": "Start learning Today",
|
||||
"link": "#contact",
|
||||
"redirect": false
|
||||
},
|
||||
"banner": {
|
||||
"video": "https://youtube.com/sosf24lsfgs",
|
||||
"image": "",
|
||||
"show": false
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"aboutUs": {
|
||||
"title": "Our Story",
|
||||
"content": "Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.",
|
||||
"imageUrl": "https://some-link",
|
||||
"show": true
|
||||
},
|
||||
"courses": {
|
||||
"title": "Explore our",
|
||||
"titleHighlight": "Courses",
|
||||
"subtitle": "Find courses you will love from best teachers all over the world🌎.",
|
||||
"show": true
|
||||
},
|
||||
"faq": {
|
||||
"title": "Frequently Asked Questions.",
|
||||
"questions": [
|
||||
{
|
||||
"title": "Lorem ipsum dolor sit, amet con",
|
||||
"content": "Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur."
|
||||
},
|
||||
{
|
||||
"title": "Lorem ipsum dolor sit, amet con",
|
||||
"content": "Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur."
|
||||
},
|
||||
{
|
||||
"title": "Lorem ipsum dolor sit, amet con",
|
||||
"content": "Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur."
|
||||
}
|
||||
],
|
||||
"show": true
|
||||
},
|
||||
"contact": {
|
||||
"title": "Have you got any",
|
||||
"titleHighlight": "Question?",
|
||||
"subtitle": "Reach out to us with any question you might have.",
|
||||
"address": "Ashwoodfield House Farm, Kidderminster Rd, Stourbridge, Kingswinford DY6 0AA",
|
||||
"phone": "+4407339904995",
|
||||
"email": "help@digdippa.com",
|
||||
"show": true
|
||||
},
|
||||
"mailinglist": {
|
||||
"title": "Join our mailing list",
|
||||
"subtitle": "We are constantly releasing new courses and sharing them with our email list. Subscribe to get notified once we release a new course",
|
||||
"buttonLabel": "Subscribe",
|
||||
"show": true
|
||||
},
|
||||
"footer": {
|
||||
"facebook": "#",
|
||||
"instagram": "#",
|
||||
"linkedin": "#"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
export const themeImages = {
|
||||
standard: {
|
||||
card: 'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-card.svg',
|
||||
editor:
|
||||
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-editor.svg',
|
||||
play: 'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-play.svg'
|
||||
},
|
||||
mainland: {
|
||||
card: 'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-card.svg',
|
||||
editor:
|
||||
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-editor.svg',
|
||||
play: 'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-play.svg'
|
||||
}
|
||||
};
|
||||
|
||||
export const defOption = {
|
||||
id: 1,
|
||||
label: '',
|
||||
options: []
|
||||
};
|
||||
|
||||
export const optionImage = {
|
||||
circle:
|
||||
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/circle.svg',
|
||||
spade:
|
||||
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/spade.svg',
|
||||
square:
|
||||
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/square.svg',
|
||||
pentagon:
|
||||
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/pentagon.svg',
|
||||
hexagon:
|
||||
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/hexagon.svg',
|
||||
triangle:
|
||||
'https://tapaozmyjsuykgerrfkt.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://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/quizes/triangle.svg'
|
||||
},
|
||||
{
|
||||
id: 'square',
|
||||
label: 'False',
|
||||
image:
|
||||
'https://tapaozmyjsuykgerrfkt.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,6 @@
|
||||
export const NavClasses = {
|
||||
active:
|
||||
'bg-gray-200 dark:bg-neutral-800 hover:dark:text-white text-black dark:text-white transition delay-150 duration-300 ease-in-out',
|
||||
background: '',
|
||||
item: 'hover:bg-gray-200 dark:hover:bg-neutral-800 hover:text-black hover:dark:text-white rounded'
|
||||
};
|
||||
@@ -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: 'Filter',
|
||||
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,40 @@
|
||||
export const ROUTE = {
|
||||
HOME: '/',
|
||||
DASHBOARD: '/dashboard',
|
||||
ONBOARDING: '/onboarding',
|
||||
COURSES: '/courses',
|
||||
PAGES: '/pages',
|
||||
COURSE: '/course',
|
||||
LOGIN: '/login',
|
||||
SIGN_UP: '/signup',
|
||||
INVITE_TEACHER: '/invite/t',
|
||||
INVITE_STUDENT: '/invite/s',
|
||||
PROFILE: '/profile',
|
||||
PEOPLE: '/people',
|
||||
DISCUSSIONS: '/discussions',
|
||||
ASK: '/ask',
|
||||
FORGOT: '/forgot',
|
||||
RESET: '/reset'
|
||||
};
|
||||
|
||||
export const PUBLIC_ROUTES = [
|
||||
`^${ROUTE.HOME}$`,
|
||||
ROUTE.LOGIN,
|
||||
ROUTE.SIGN_UP,
|
||||
`^${ROUTE.INVITE_TEACHER}/.*`,
|
||||
ROUTE.FORGOT,
|
||||
ROUTE.RESET,
|
||||
`^${ROUTE.PAGES}/.*`,
|
||||
`^${ROUTE.COURSE}/.*`,
|
||||
'/404'
|
||||
];
|
||||
|
||||
export const ROUTES_TO_HIDE_NAV = [
|
||||
ROUTE.LOGIN,
|
||||
ROUTE.SIGN_UP,
|
||||
`^${ROUTE.INVITE_TEACHER}/.*`,
|
||||
`^${ROUTE.INVITE_STUDENT}/.*`,
|
||||
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 toggleBodyByMode(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,41 @@
|
||||
import { browser } from '$app/environment';
|
||||
import type { Course } from '../types';
|
||||
|
||||
export const isCourseFree = (cost: number) => !(Number(cost) > 0);
|
||||
|
||||
export const getStudentInviteLink = (_course: Course, orgSiteName: string) => {
|
||||
const hash = encodeURIComponent(
|
||||
btoa(
|
||||
JSON.stringify({
|
||||
groupId: _course.group?.id,
|
||||
name: _course.title,
|
||||
description: _course.description,
|
||||
orgSiteName
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return `https://${orgSiteName}.classroomio.com/invite/s/${hash}`;
|
||||
};
|
||||
|
||||
export const getTextFromHTML = (html: string): string => {
|
||||
if (!browser) return html;
|
||||
|
||||
const dummyDiv = document.createElement('div');
|
||||
dummyDiv.innerHTML = html;
|
||||
|
||||
return dummyDiv.textContent?.trim() || '';
|
||||
};
|
||||
|
||||
const tagsToReplace = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
export function replaceHTMLTag(text: string) {
|
||||
return text
|
||||
.split('')
|
||||
.map((char) => tagsToReplace[char] || char)
|
||||
.join('');
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const calDateDiff = (date: string | number | Date): string => {
|
||||
return dayjs(date).fromNow(true) + ` ago`;
|
||||
};
|
||||
|
||||
export const getGreeting = () => {
|
||||
const date = new Date();
|
||||
const hours = date.getHours();
|
||||
return hours < 12 ? 'Good Morning' : hours < 18 ? 'Good Afternoon' : 'Good Evening';
|
||||
};
|
||||
@@ -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 @@
|
||||
export default () => Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
@@ -0,0 +1,10 @@
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.match(/[^\W\s\d]*/g, '')
|
||||
.filter((v = '') => !!v.trim())
|
||||
.join('-')
|
||||
.toLowerCase()
|
||||
.concat(`-${new Date().getTime()}`);
|
||||
}
|
||||
|
||||
export default generateSlug;
|
||||
@@ -0,0 +1,18 @@
|
||||
export default function generateUUID() {
|
||||
// Public Domain/MIT
|
||||
var d = new Date().getTime(); //Timestamp
|
||||
var d2 = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0; //Time in microseconds since page-load or 0 if unsupported
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16; //random number between 0 and 16
|
||||
if (d > 0) {
|
||||
//Use timestamp until depleted
|
||||
r = (d + r) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
} else {
|
||||
//Use microseconds since page-load if supported
|
||||
r = (d2 + r) % 16 | 0;
|
||||
d2 = Math.floor(d2 / 16);
|
||||
}
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default (currency: string | undefined) => {
|
||||
const locale = currency == 'NGN' ? 'en-NG' : 'en-US';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isObject(obj) {
|
||||
return obj === Object(obj);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
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,12 @@
|
||||
export function genQuizPin(): number {
|
||||
var minm = 100000;
|
||||
var maxm = 999999;
|
||||
return Math.floor(Math.random() * (maxm - minm + 1)) + minm;
|
||||
}
|
||||
|
||||
export function generateSitename(orgName: string): string {
|
||||
return orgName
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-zA-Z0-9-]/g, '');
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function removeDuplicate(arr) {
|
||||
let s = new Set(arr);
|
||||
let it = s.values();
|
||||
return Array.from(it);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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 { ROUTES_TO_HIDE_NAV } from '$lib/utils/constants/routes';
|
||||
|
||||
export default (route: string): boolean => {
|
||||
return ROUTES_TO_HIDE_NAV.some((publicRoute) => {
|
||||
const regex = new RegExp(publicRoute, 'g');
|
||||
return regex.test(route);
|
||||
});
|
||||
};
|
||||
@@ -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,3 @@
|
||||
export default (path: string): boolean => {
|
||||
return ['login', 'signup', 'onboarding'].some((r) => path.includes(r)) || path === '';
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
function showAppsSideBar(path) {
|
||||
return path.includes('/lessons/') && !path.includes('landingpage');
|
||||
}
|
||||
|
||||
export default showAppsSideBar;
|
||||
@@ -0,0 +1,3 @@
|
||||
export function capitalizeFirstLetter(val: string): string {
|
||||
return val.charAt(0).toUpperCase() + val.slice(1);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
PUBLIC_SUPABASE_ANON_KEY,
|
||||
PUBLIC_SUPABASE_URL,
|
||||
PUBLIC_SUPABASE_BUCKET_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: PUBLIC_SUPABASE_BUCKET_URL + '/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,14 @@
|
||||
export function setTheme(theme: string) {
|
||||
// In case the default theme is added but another theme exists
|
||||
if (!theme && document.body.className.includes('theme-')) {
|
||||
const regex = /theme-[\w]+/gi;
|
||||
document.body.className = document.body.className.replace(regex, theme);
|
||||
return;
|
||||
}
|
||||
|
||||
// In case theme already exists in dom, don't add
|
||||
if (document.body.className.includes(theme)) return;
|
||||
|
||||
localStorage.setItem('theme', theme);
|
||||
document.body.className = document.body.className.concat(' ', theme);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import type { Profile } from '$lib/components/Course/components/People/types';
|
||||
|
||||
export async function getProfile(email: string): Promise<Profile | null> {
|
||||
const { data } = await supabase.from('profile').select('*').eq('email', email).single();
|
||||
console.log('getProfile data', data);
|
||||
return data;
|
||||
}
|
||||
@@ -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,259 @@
|
||||
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 orgLandingpageValidationSchema = z.object({
|
||||
name: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty'
|
||||
}),
|
||||
email: z.string().email({
|
||||
message: 'Invalid email address',
|
||||
invalid_type_error: 'Must not be empty'
|
||||
}),
|
||||
phone: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty'
|
||||
}),
|
||||
message: z.string().min(20, {
|
||||
message: 'Must be 10 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'
|
||||
: undefined;
|
||||
};
|
||||
|
||||
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 coursePaymentValidation = (fields = {}) => {
|
||||
const schema = z.object({
|
||||
fullname: z.string().min(6, {
|
||||
message: 'Must be 6 or more characters long',
|
||||
invalid_type_error: 'Must not be empty'
|
||||
}),
|
||||
email: z.string().email({
|
||||
message: 'Invalid email address',
|
||||
invalid_type_error: 'Must not be empty'
|
||||
})
|
||||
});
|
||||
const { error } = schema.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 orgLandingpageValidation = (fields = {}) => {
|
||||
const { error } = orgLandingpageValidationSchema.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 createTemplateExerciseValidation = (fields = {}) => {
|
||||
// const schema = z.object({
|
||||
// orgName: z.string().min(5, {
|
||||
// message: 'Must be 5 or more characters long',
|
||||
// }),
|
||||
// siteName: z.string().min(5, {
|
||||
// message: 'Must be 5 or more characters long',
|
||||
// })
|
||||
// });
|
||||
// const { error } = schema.safeParse(fields);
|
||||
|
||||
// return processErrors(error);
|
||||
// };
|
||||
|
||||
export const createOrgValidation = (fields = {}) => {
|
||||
const schema = z.object({
|
||||
orgName: z.string().min(5, {
|
||||
message: 'Must be 5 or more characters long',
|
||||
invalid_type_error: 'Must not be empty'
|
||||
}),
|
||||
siteName: z.string().min(5, {
|
||||
message: 'Must be 5 or more characters long',
|
||||
invalid_type_error: 'Must not be empty'
|
||||
})
|
||||
});
|
||||
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).select();
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
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_unlocked: lesson.is_unlocked,
|
||||
lesson_at: new Date(),
|
||||
note: lesson.note,
|
||||
order: lesson.order || index + 1,
|
||||
public: lesson.public,
|
||||
slide_url: lesson.slide_url,
|
||||
title: lesson.title,
|
||||
videos: lesson.videos
|
||||
}));
|
||||
|
||||
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,415 @@
|
||||
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,
|
||||
LessonCompletion,
|
||||
ExerciseTemplate
|
||||
} from '$lib/utils/types';
|
||||
import { STATUS } from '$lib/utils/constants/course';
|
||||
import type { PostgrestSingleResponse } from '@supabase/supabase-js';
|
||||
|
||||
export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug']) {
|
||||
let match: { slug?: string; id?: string; status?: string } = {};
|
||||
|
||||
if (slug) {
|
||||
match.slug = slug;
|
||||
} else {
|
||||
match.id = courseId;
|
||||
}
|
||||
|
||||
match.status = STATUS[STATUS.ACTIVE];
|
||||
|
||||
const response: PostgrestSingleResponse<Course | null> = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
overview,
|
||||
logo,
|
||||
is_published,
|
||||
group(*,
|
||||
members:groupmember(*,
|
||||
profile(*)
|
||||
)
|
||||
),
|
||||
slug,
|
||||
cost,
|
||||
currency,
|
||||
metadata,
|
||||
is_certificate_downloadable,
|
||||
certificate_theme,
|
||||
lessons:lesson(
|
||||
id, title,public, lesson_at, call_url, is_unlocked, order, created_at,
|
||||
note, videos, slide_url, call_url, totalExercises:exercise(count), totalComments:lesson_comment(count),
|
||||
profile:teacher_id(id, avatar_url, fullname),
|
||||
lesson_completion(id, profile_id, is_complete)
|
||||
),
|
||||
attendance:group_attendance(*),
|
||||
polls:apps_poll(status)
|
||||
`
|
||||
)
|
||||
.match(match)
|
||||
.single();
|
||||
|
||||
const { data, error } = response;
|
||||
|
||||
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 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) {
|
||||
const { data } = supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
if (!data.publicUrl) return;
|
||||
|
||||
logo = data.publicUrl;
|
||||
}
|
||||
|
||||
return logo;
|
||||
}
|
||||
|
||||
export async function updateCourse(
|
||||
courseId: Course['id'],
|
||||
avatar: string | undefined,
|
||||
course: Partial<Course>
|
||||
) {
|
||||
if (avatar && courseId) {
|
||||
const filename = `course/${courseId + Date.now()}.webp`;
|
||||
|
||||
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const { data: response } = supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
if (!response.publicUrl) return;
|
||||
|
||||
course.logo = response.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').update({ status: 'DELETED' }).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, videos, slide_url, call_url, totalExercises:exercise(count), totalComments:lesson_comment(count), lesson_completion(id, profile_id, is_complete)`
|
||||
)
|
||||
.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 updateLessonCompletion(completion: LessonCompletion) {
|
||||
return supabase.from('lesson_completion').upsert(completion);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export async function createExerciseFromTemplate(
|
||||
lessonId: string,
|
||||
template: ExerciseTemplate
|
||||
): Promise<any | undefined> {
|
||||
const { data, error } = await createExercise({
|
||||
title: template.title,
|
||||
description: template.description,
|
||||
lesson_id: lessonId
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Something went wrong', error);
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = data[0] || {};
|
||||
if (!id) {
|
||||
console.error('Something went wrong, no id', error);
|
||||
return;
|
||||
}
|
||||
|
||||
await upsertExercise(template.questionnaire, id);
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
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).select();
|
||||
|
||||
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, label: question_type.label || '' };
|
||||
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).select();
|
||||
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,82 @@
|
||||
import type { ExerciseTemplate, CustomQuestionType } from '$lib/utils/types';
|
||||
|
||||
const QuestionTypes: CustomQuestionType[] = [
|
||||
{
|
||||
id: 1,
|
||||
label: 'RADIO'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: 'CHECKBOX'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: 'TEXTAREA'
|
||||
}
|
||||
];
|
||||
|
||||
const quiz: ExerciseTemplate = {
|
||||
title: 'HTML Elements Quiz',
|
||||
description: 'Test your knowledge of HTML elements.',
|
||||
questionnaire: {
|
||||
questions: [
|
||||
{
|
||||
title: 'What does the <p> element stand for?',
|
||||
name: 'p_element',
|
||||
points: 2,
|
||||
order: 1,
|
||||
question_type: QuestionTypes[0], // RADIO
|
||||
options: [
|
||||
{ label: 'Paragraph', is_correct: true },
|
||||
{ label: 'Page', is_correct: false },
|
||||
{ label: 'Preformatted', is_correct: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Which HTML element is used for creating an unordered list?',
|
||||
name: 'ul_element',
|
||||
points: 2,
|
||||
order: 2,
|
||||
question_type: QuestionTypes[0], // RADIO
|
||||
options: [
|
||||
{ label: '<ul>', is_correct: true },
|
||||
{ label: '<ol>', is_correct: false },
|
||||
{ label: '<li>', is_correct: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'What does the <a> element do?',
|
||||
name: 'a_element',
|
||||
points: 2,
|
||||
order: 3,
|
||||
question_type: QuestionTypes[1], // CHECKBOX
|
||||
options: [
|
||||
{ label: 'Defines a hyperlink', is_correct: true },
|
||||
{ label: 'Creates a table', is_correct: false },
|
||||
{ label: 'Adds an image', is_correct: false },
|
||||
{ label: 'Defines a form', is_correct: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Which HTML element is used for creating a line break?',
|
||||
name: 'br_element',
|
||||
points: 2,
|
||||
order: 4,
|
||||
question_type: QuestionTypes[0], // RADIO
|
||||
options: [
|
||||
{ label: '<br>', is_correct: true },
|
||||
{ label: '<p>', is_correct: false },
|
||||
{ label: '<hr>', is_correct: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'What does the <h1> to <h6> elements represent?',
|
||||
name: 'h_elements',
|
||||
points: 2,
|
||||
order: 5,
|
||||
question_type: QuestionTypes[2], // TEXTAREA
|
||||
options: []
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { UserLessonDataType } from '$lib/utils/types';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
function getFirstAndLastDayOfMonth(): { firstDay: string; lastDay: string } {
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
|
||||
// Create a date object for the first day of the month
|
||||
const firstDay = new Date(year, month, 1).toISOString();
|
||||
|
||||
// Find the last day of the next month and subtract one day to get the last day of the current month
|
||||
const nextMonth: any = new Date(year, month + 1, 1);
|
||||
const lastDay = new Date(nextMonth - 1).toISOString();
|
||||
|
||||
return {
|
||||
firstDay,
|
||||
lastDay
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchUserUpcomingData(
|
||||
profileId: string | null,
|
||||
orgId: string
|
||||
): Promise<UserLessonDataType[] | []> {
|
||||
if (!profileId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { lastDay, firstDay } = getFirstAndLastDayOfMonth();
|
||||
|
||||
const { data: userUpcomingData } = await supabase
|
||||
.rpc('get_user_upcoming_lessons', {
|
||||
profile_id_arg: profileId,
|
||||
org_id_arg: orgId
|
||||
})
|
||||
.filter('lesson_at', 'gte', firstDay)
|
||||
.filter('lesson_at', 'lte', lastDay);
|
||||
|
||||
console.log(`userUpcomingData`, userUpcomingData);
|
||||
|
||||
if (!Array.isArray(userUpcomingData)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return userUpcomingData;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import type { PostgrestError } from '@supabase/supabase-js';
|
||||
|
||||
export interface LMSExercise {
|
||||
id: string;
|
||||
title: string;
|
||||
updated_at: string;
|
||||
questions: {
|
||||
points: number;
|
||||
}[];
|
||||
submission: {
|
||||
status_id: number;
|
||||
updated_at: string;
|
||||
total: number;
|
||||
groupmember: {
|
||||
id: string;
|
||||
profile: {
|
||||
id: string;
|
||||
}[];
|
||||
}[];
|
||||
}[];
|
||||
lesson: {
|
||||
id: string;
|
||||
title: string;
|
||||
order: number;
|
||||
course: {
|
||||
id: string;
|
||||
title: string;
|
||||
group: {
|
||||
organisation: {
|
||||
id: string;
|
||||
}[];
|
||||
}[];
|
||||
groupmember: {
|
||||
id: string;
|
||||
profile: {
|
||||
id: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface FetchLMSExercisesResponse {
|
||||
exercises: LMSExercise[] | null;
|
||||
error: PostgrestError | null;
|
||||
}
|
||||
|
||||
export async function fetchLMSExercises(
|
||||
profileId: string,
|
||||
orgId: string
|
||||
): Promise<FetchLMSExercisesResponse> {
|
||||
const { data, error } = await supabase
|
||||
.from('exercise')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
title,
|
||||
updated_at,
|
||||
questions:question(points),
|
||||
submission (
|
||||
status_id,
|
||||
updated_at,
|
||||
total,
|
||||
groupmember:submitted_by!inner (
|
||||
id,
|
||||
profile:profile_id!inner (
|
||||
id
|
||||
)
|
||||
)
|
||||
),
|
||||
lesson!inner (
|
||||
id,
|
||||
title,
|
||||
order,
|
||||
is_unlocked,
|
||||
course!inner (
|
||||
id,
|
||||
title,
|
||||
group:group_id!inner (
|
||||
organisation:organization_id!inner (
|
||||
id
|
||||
),
|
||||
groupmember!inner(
|
||||
id,
|
||||
profile:profile_id!inner (
|
||||
id
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('lesson.is_unlocked', true)
|
||||
.eq('lesson.course.group.organisation.id', orgId)
|
||||
.eq('lesson.course.group.groupmember.profile.id', profileId)
|
||||
.eq('submission.groupmember.profile.id', profileId);
|
||||
|
||||
return {
|
||||
exercises: data as LMSExercise[] | null,
|
||||
error
|
||||
};
|
||||
}
|
||||
@@ -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,4 @@
|
||||
1. Welcome new user into classroomio
|
||||
2. Sends teacher welcome to course
|
||||
3. Notify teacher someone wants to buy course
|
||||
4
|
||||
@@ -0,0 +1,58 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
export const NOTIFICATION_NAME = {
|
||||
WELCOME_TO_APP: 'WELCOME TO APP',
|
||||
INVITE_TEACHER: 'INVITE TEACHER',
|
||||
WELCOME_TEACHER_TO_COURSE: 'WELCOME TEACHER TO COURSE',
|
||||
SEND_TEACHER_STUDENT_BUY_REQUEST: 'SEND TEACHER STUDENT BUY REQUEST',
|
||||
STUDENT_PROOVE_COURSE_PAYMENT: 'STUDENT PROOVE COURSE PAYMENT',
|
||||
STUDENT_COURSE_WELCOME: 'STUDENT COURSE WELCOME',
|
||||
TEACHER_STUDENT_JOINED: 'TEACHER STUDENT JOINED',
|
||||
SUBMISSION_UPDATE: 'SUBMISSION UPDATE',
|
||||
EXERCISE_SUBMISSION_UPDATE: 'EXERCISE SUBMISSION UPDATE'
|
||||
};
|
||||
|
||||
const NAME_TO_PATH = {
|
||||
[NOTIFICATION_NAME.WELCOME_TO_APP]: '/api/email/welcome',
|
||||
[NOTIFICATION_NAME.INVITE_TEACHER]: '/api/email/invite',
|
||||
[NOTIFICATION_NAME.WELCOME_TEACHER_TO_COURSE]: '/api/email/course/teacher_welcome',
|
||||
[NOTIFICATION_NAME.SEND_TEACHER_STUDENT_BUY_REQUEST]:
|
||||
'/api/email/course/teacher_student_buycourse',
|
||||
[NOTIFICATION_NAME.STUDENT_PROOVE_COURSE_PAYMENT]: '/api/email/course/student_prove_payment',
|
||||
[NOTIFICATION_NAME.STUDENT_COURSE_WELCOME]: '/api/email/course/student_welcome',
|
||||
[NOTIFICATION_NAME.TEACHER_STUDENT_JOINED]: '/api/email/course/teacher_student_joined',
|
||||
[NOTIFICATION_NAME.SUBMISSION_UPDATE]: '/api/email/course/submission_update',
|
||||
[NOTIFICATION_NAME.EXERCISE_SUBMISSION_UPDATE]: '/api/email/course/exercise_submission_update'
|
||||
};
|
||||
|
||||
const getAccessToken = async () => {
|
||||
const { data } = await supabase.auth.getSession();
|
||||
return data.session?.access_token || '';
|
||||
};
|
||||
|
||||
export const triggerSendEmail = async (name: string, body: { [k: string]: any }) => {
|
||||
try {
|
||||
const accessToken = await getAccessToken();
|
||||
const path = NAME_TO_PATH[name];
|
||||
|
||||
if (!path) {
|
||||
console.error('Could not trigger send');
|
||||
return;
|
||||
}
|
||||
const response = await fetch(path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: accessToken
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.log(response);
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
const result = await response.json();
|
||||
} catch (error) {
|
||||
console.error(name, error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { withEmailTemplate } from './template';
|
||||
import {
|
||||
SMTP_HOST,
|
||||
SMTP_USER,
|
||||
SMTP_PASSWORD,
|
||||
SMTP_USER_HELP,
|
||||
SMTP_PASSWORD_HELP
|
||||
} from '$env/static/private';
|
||||
|
||||
const content = `
|
||||
<div>
|
||||
<h1>Welcome</h1>
|
||||
<br/>
|
||||
<p>Thank you so much for trying out ClassroomIO</p>
|
||||
<a href="https://app.classroomio.com/login">Login</a><br/>
|
||||
<br/>
|
||||
Your ClassroomIO Team
|
||||
</div>
|
||||
`;
|
||||
|
||||
let transporter: nodemailer.Transporter;
|
||||
|
||||
const getTransporter = async (isPersonal?: boolean): Promise<boolean> => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
transporter = nodemailer.createTransport({
|
||||
host: SMTP_HOST,
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: isPersonal ? SMTP_USER : SMTP_USER_HELP,
|
||||
pass: isPersonal ? SMTP_PASSWORD : SMTP_PASSWORD_HELP
|
||||
}
|
||||
});
|
||||
|
||||
transporter.verify(function (error, success) {
|
||||
if (error) {
|
||||
console.log('Transporter error', error);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log('Server is ready to take our messages');
|
||||
resolve(success);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const sendEmail = async ({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
content,
|
||||
isPersonalEmail,
|
||||
replyTo
|
||||
}: {
|
||||
from?: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
isPersonalEmail?: boolean;
|
||||
replyTo?: string;
|
||||
}) => {
|
||||
await getTransporter(isPersonalEmail);
|
||||
const info = await transporter.sendMail({
|
||||
from: from || '"Best from ClassroomIO" <best@classroomio.com>', // sender address
|
||||
to, // list of receivers
|
||||
subject, // Subject line
|
||||
replyTo,
|
||||
html: withEmailTemplate(content) // html body
|
||||
});
|
||||
|
||||
console.log('Message sent: %s', info.messageId);
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
export const withEmailTemplate = (content: string) =>
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1"
|
||||
/>
|
||||
<base target="_blank" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: "Poppins", "Helvetica Neue", "Segoe UI", Helvetica,
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
line-height: 26px;
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f4f4f4;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px dashed #94a3b8;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
table td {
|
||||
padding: 5px;
|
||||
}
|
||||
.socialicons {
|
||||
max-width: 200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 27px;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
background-color: #f1f5f9;
|
||||
padding: 30px;
|
||||
max-width: 525px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.brandcolor {
|
||||
color: #00c4b8;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
background-color: #f1f5f9;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
color: #475569;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tooltip a {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-top:12px;
|
||||
background: #0233BD;
|
||||
border-radius: 8px;
|
||||
text-decoration: none !important;
|
||||
color: #fff !important;
|
||||
font-weight: 500;
|
||||
padding: 10px 30px;
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.button:hover {
|
||||
background: #0233BD;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
.footer a {
|
||||
color: #cbd5e1;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.gutter img {
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0233BD;
|
||||
}
|
||||
a:hover {
|
||||
color: #0233BD;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-weight: 600;
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
.wrap {
|
||||
max-width: auto;
|
||||
}
|
||||
.gutter {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
style="
|
||||
background-color: #fff;
|
||||
font-family: 'Poppins', 'Helvetica Neue', 'Segoe UI', Helvetica,
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 26px;
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
"
|
||||
>
|
||||
<div class="gutter" style="padding: 30px">
|
||||
<a href="https://classroomio.com" target="_blank">
|
||||
<img
|
||||
src="https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/classroomio-logo-full.png"
|
||||
alt="ClassroomIO Logo"
|
||||
/></a>
|
||||
</div>
|
||||
<div
|
||||
class="wrap"
|
||||
style="
|
||||
background-color: #f1f5f9;
|
||||
padding: 30px;
|
||||
max-width: 525px;
|
||||
margin: 0 auto;
|
||||
border-radius: 12px;
|
||||
"
|
||||
>
|
||||
${content}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="footer"
|
||||
style="text-align: center; font-size: 12px; color: #cbd5e1"
|
||||
>
|
||||
<table class="socialicons">
|
||||
<tr>
|
||||
<td>
|
||||
<a target="_blank" href="https://twitter.com/classroomio"
|
||||
><img
|
||||
title="X"
|
||||
src="https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/classroomio-x.png"
|
||||
alt="X"
|
||||
width="32"
|
||||
/></a>
|
||||
</td>
|
||||
<td>
|
||||
<a target="_blank" href="https://www.youtube.com/@ClassroomIO"
|
||||
><img
|
||||
title="Youtube"
|
||||
src="https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/classroomio-youtube.png"
|
||||
alt="Youtube"
|
||||
width="32"
|
||||
/></a>
|
||||
</td>
|
||||
<td>
|
||||
<a target="_blank" href="https://www.facebook.com/classroomiohq"
|
||||
><img
|
||||
title="Facebook"
|
||||
src="https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/classroomio-facebook.png"
|
||||
alt="Facebook"
|
||||
width="32"
|
||||
/></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="padding-top: 8px; line-height: initial">
|
||||
ClassroomIO ${new Date().getFullYear()}. All rights reserved.<br />
|
||||
<a
|
||||
style="text-decoration: none"
|
||||
href="https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594"
|
||||
target="_blank"
|
||||
>Terms</a
|
||||
>
|
||||
|
|
||||
<a
|
||||
style="text-decoration: none"
|
||||
href="https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594"
|
||||
target="_blank"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
@@ -0,0 +1,232 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
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';
|
||||
import type { CurrentOrg, OrgTeamMember, OrgAudience } from '$lib/utils/types/org';
|
||||
|
||||
export async function getOrgTeam(orgId: string) {
|
||||
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 })
|
||||
.returns<
|
||||
{
|
||||
id: number;
|
||||
email: string;
|
||||
verified: boolean;
|
||||
role_id: number;
|
||||
profile: {
|
||||
id: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
};
|
||||
}[]
|
||||
>();
|
||||
|
||||
const team: OrgTeamMember[] = [];
|
||||
if (data?.length) {
|
||||
data.forEach((teamMember) => {
|
||||
team.push({
|
||||
id: teamMember.id,
|
||||
email: teamMember?.profile?.email || teamMember.email,
|
||||
verified: teamMember.verified,
|
||||
profileId: teamMember?.profile?.id,
|
||||
fullname: teamMember?.profile?.fullname || '',
|
||||
role: ROLE_LABEL[teamMember?.role_id] || '',
|
||||
isAdmin: teamMember?.role_id === ROLE.ADMIN
|
||||
});
|
||||
});
|
||||
|
||||
orgTeam.set(team);
|
||||
}
|
||||
|
||||
return {
|
||||
team: get(orgTeam),
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOrganizations(userId: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('organizationmember')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
profile_id,
|
||||
role_id,
|
||||
created_at,
|
||||
organization!organizationmember_organization_id_fkey (
|
||||
id,
|
||||
name,
|
||||
siteName,
|
||||
avatar_url,
|
||||
landingpage,
|
||||
theme,
|
||||
created_at
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('profile_id', userId)
|
||||
.order('id', { ascending: false })
|
||||
.returns<
|
||||
{
|
||||
id: string;
|
||||
profile_id: string;
|
||||
role_id: string;
|
||||
created_at: string;
|
||||
organization: CurrentOrg;
|
||||
}[]
|
||||
>();
|
||||
|
||||
const orgsArray: CurrentOrg[] = [];
|
||||
|
||||
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,
|
||||
theme: orgMember?.organization?.theme,
|
||||
avatar_url: orgMember?.organization?.avatar_url,
|
||||
memberId: orgMember?.id,
|
||||
role_id: orgMember?.role_id,
|
||||
landingpage: orgMember?.organization?.landingpage
|
||||
});
|
||||
});
|
||||
|
||||
orgs.set(orgsArray);
|
||||
|
||||
if (localStorage) {
|
||||
const lastOrgSiteName = localStorage.getItem('classroomio_org_sitename');
|
||||
|
||||
const lastOrg = orgsArray.find((org) => org.siteName === lastOrgSiteName);
|
||||
|
||||
if (lastOrg) {
|
||||
currentOrg.set(lastOrg);
|
||||
}
|
||||
}
|
||||
|
||||
const _currentOrg = get(currentOrg);
|
||||
if (!_currentOrg.siteName) {
|
||||
currentOrg.set(orgsArray[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
orgs: orgsArray,
|
||||
currentOrg: get(currentOrg),
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOrgAudience(orgId: string) {
|
||||
// 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,
|
||||
groupmember!inner(
|
||||
role_id,
|
||||
group_id:group!inner(
|
||||
organization_id
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('groupmember.group.organization_id', orgId)
|
||||
.eq('groupmember.role_id', ROLE.STUDENT);
|
||||
|
||||
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: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
lessons:lesson(count),
|
||||
group!inner(
|
||||
organization!inner(id, name, siteName, avatar_url)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('group.organization.siteName', siteName)
|
||||
.eq('status', 'ACTIVE')
|
||||
.eq('is_published', true);
|
||||
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getCurrentOrg(siteName: string, justGet = false) {
|
||||
const { data, error } = await supabase
|
||||
.from('organization')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
name,
|
||||
siteName,
|
||||
avatar_url,
|
||||
landingpage,
|
||||
theme
|
||||
`
|
||||
)
|
||||
.eq('siteName', siteName)
|
||||
.returns<CurrentOrg[]>();
|
||||
|
||||
const isDataEmpty = !data?.[0];
|
||||
|
||||
if (!justGet && (error || isDataEmpty)) {
|
||||
console.error('Error getOrganization', error);
|
||||
return goto('/404');
|
||||
}
|
||||
|
||||
if (!justGet) {
|
||||
if (isDataEmpty) return;
|
||||
|
||||
currentOrg.set(data[0]);
|
||||
} else if (!isDataEmpty) {
|
||||
return 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,22 @@
|
||||
import { dev } from '$app/environment';
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
export const capturePosthogEvent = (event: string, properties?: Record<string, any>): void => {
|
||||
if (dev) return;
|
||||
|
||||
posthog.capture(event, properties);
|
||||
};
|
||||
|
||||
export const identifyPosthogUser = (id: string, properties?: Record<string, any>): void => {
|
||||
if (dev) return;
|
||||
|
||||
posthog.identify(id, properties);
|
||||
};
|
||||
|
||||
export const initPosthog = (): void => {
|
||||
if (dev) return;
|
||||
|
||||
posthog.init('phc_JfdHOZ6v0cVlGELBYx1Tmoen2nxNOrAzvgvrPA6Ksov', {
|
||||
api_host: 'https://eu.posthog.com'
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { dev } from '$app/environment';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { CaptureConsole } from '@sentry/integrations';
|
||||
|
||||
export const initSentry = (): void => {
|
||||
if (dev) return;
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://c966f7e8cb1d4306be20b26bb4f0cc96@o476906.ingest.sentry.io/5999999',
|
||||
integrations: [
|
||||
new CaptureConsole({
|
||||
levels: ['error']
|
||||
}),
|
||||
new Sentry.Replay()
|
||||
],
|
||||
environment: 'production',
|
||||
replaysSessionSampleRate: 0.5,
|
||||
replaysOnErrorSampleRate: 1.0
|
||||
});
|
||||
};
|
||||
|
||||
export const setSentryUser = (user: Record<string, any>): void => {
|
||||
if (dev) return;
|
||||
|
||||
Sentry.setUser(user);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { PostgrestSingleResponse } from '@supabase/supabase-js';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import type { ExerciseSubmissions } from '$lib/utils/types';
|
||||
|
||||
export function fetchSubmissionStatus() {
|
||||
return supabase.from('submissionstatus').select(`*`);
|
||||
}
|
||||
|
||||
export function fetchSubmissions(course_id: string) {
|
||||
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
|
||||
}: {
|
||||
exerciseId: string;
|
||||
courseId?: string;
|
||||
submittedBy?: string;
|
||||
}) {
|
||||
const query: {
|
||||
exercise_id: string;
|
||||
course_id?: string;
|
||||
submitted_by?: string;
|
||||
} = {
|
||||
exercise_id: exerciseId
|
||||
};
|
||||
|
||||
if (courseId) {
|
||||
query.course_id = courseId;
|
||||
}
|
||||
if (submittedBy) {
|
||||
query.submitted_by = submittedBy;
|
||||
}
|
||||
|
||||
return supabase
|
||||
.from('submission')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
answers:question_answer(*),
|
||||
status_id,
|
||||
submitted_by:groupmember!inner(
|
||||
profile!inner(
|
||||
id,
|
||||
fullname,
|
||||
avatar_url
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.match(query)
|
||||
.returns<ExerciseSubmissions[]>();
|
||||
}
|
||||
|
||||
export async function updateSubmission(
|
||||
{ id, status_id, total }: { id?: number; status_id?: number; total?: number },
|
||||
otherArgs?: any
|
||||
) {
|
||||
const toUpdate: {
|
||||
status_id?: number;
|
||||
total?: number;
|
||||
} = {
|
||||
status_id
|
||||
};
|
||||
|
||||
if (typeof total === 'number') {
|
||||
toUpdate.total = total;
|
||||
}
|
||||
|
||||
return supabase.from('submission').update(toUpdate, otherArgs).match({ id });
|
||||
}
|
||||
|
||||
export async function updateQuestionAnswer(update: any, match: any) {
|
||||
return supabase.from('question_answer').update(update).match(match);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const appStore = writable({
|
||||
isDark: false,
|
||||
isOrgSite: false,
|
||||
orgSiteName: ''
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const attendance = writable<{
|
||||
[key: string]: {
|
||||
[key: string]: {
|
||||
is_present: boolean;
|
||||
id: number;
|
||||
};
|
||||
};
|
||||
}>({});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import { dev, browser } from '$app/environment';
|
||||
import { STEPS } from '../constants/quiz';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { CurrentOrg, OrgTeamMember, OrgAudience } from '../types/org';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import type { UserLessonDataType } from '$lib/utils/types';
|
||||
|
||||
export const orgs = writable<CurrentOrg[]>([]);
|
||||
export const currentOrg: Writable<CurrentOrg> = writable({
|
||||
id: '',
|
||||
name: '',
|
||||
shortName: '',
|
||||
siteName: '',
|
||||
avatar_url: '',
|
||||
memberId: '',
|
||||
role_id: '',
|
||||
landingpage: {},
|
||||
theme: ''
|
||||
});
|
||||
export const orgAudience = writable<OrgAudience[]>([]);
|
||||
export const orgTeam = writable<OrgTeamMember[]>([]);
|
||||
export const isOrgAdmin = derived(
|
||||
currentOrg,
|
||||
($currentOrg) => parseInt($currentOrg.role_id) === ROLE.ADMIN
|
||||
);
|
||||
export const currentOrgPath = derived(currentOrg, ($currentOrg) =>
|
||||
$currentOrg.siteName ? `/org/${$currentOrg.siteName}` : ''
|
||||
);
|
||||
export const currentOrgDomain = derived(currentOrg, ($currentOrg) => {
|
||||
const browserOrigin = dev && browser && window.location.origin;
|
||||
return browserOrigin
|
||||
? browserOrigin
|
||||
: $currentOrg.siteName
|
||||
? `https://${$currentOrg.siteName}.classroomio.com`
|
||||
: '';
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
export const userUpcomingData = writable<UserLessonDataType[]>([]);
|
||||
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isMobile = writable(false);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
export const user = writable<{
|
||||
openAuthModal: boolean;
|
||||
fetchingUser: boolean;
|
||||
isLoggedIn: boolean;
|
||||
currentSession: User | undefined;
|
||||
expiresAt: number;
|
||||
}>({
|
||||
openAuthModal: false,
|
||||
fetchingUser: false,
|
||||
isLoggedIn: false,
|
||||
currentSession: undefined,
|
||||
expiresAt: 0
|
||||
});
|
||||
|
||||
export const profile = writable<{
|
||||
id: string | undefined;
|
||||
fullname: string;
|
||||
avatar_url: string;
|
||||
profile_url?: string;
|
||||
username: string;
|
||||
user_id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
goal: string;
|
||||
source: string;
|
||||
telegram_chat_id: number | null;
|
||||
}>({
|
||||
id: undefined,
|
||||
fullname: '',
|
||||
avatar_url: 'https://pbs.twimg.com/profile_images/1416443682157473795/dGtFbtht_normal.jpg',
|
||||
username: 'Elon',
|
||||
user_id: '',
|
||||
email: '',
|
||||
role: '',
|
||||
goal: '',
|
||||
source: '',
|
||||
telegram_chat_id: null
|
||||
});
|
||||
@@ -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_unlocked: boolean;
|
||||
};
|
||||
|
||||
type LessonByDayType = {
|
||||
[dayIndex: number]: UserLessonDataType[];
|
||||
};
|
||||
|
||||
export type LessonsByMonthIndexType = {
|
||||
[monthIndex: number]: LessonByDayType;
|
||||
};
|
||||
@@ -0,0 +1,362 @@
|
||||
export * from './config';
|
||||
export * from './dashboard';
|
||||
|
||||
//===============Custom Type===============
|
||||
export interface GroupPerson {
|
||||
assigned_student_id: number | null;
|
||||
created_at: string;
|
||||
email: string | null;
|
||||
group_id: string;
|
||||
id: string;
|
||||
profile: Profile;
|
||||
profile_id: string;
|
||||
role_id: Role['id'];
|
||||
fullname?: string;
|
||||
}
|
||||
|
||||
export interface CustomQuestionType {
|
||||
id: number;
|
||||
label: any;
|
||||
}
|
||||
|
||||
export interface ExerciseTemplate {
|
||||
title: string;
|
||||
description: string;
|
||||
questionnaire: {
|
||||
questions: {
|
||||
title: string;
|
||||
name: string;
|
||||
points: number;
|
||||
order: number;
|
||||
question_type: CustomQuestionType;
|
||||
options: {
|
||||
label: string;
|
||||
is_correct: boolean;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
export interface ExerciseSubmissions {
|
||||
id: string;
|
||||
status_id: number;
|
||||
submitted_by: {
|
||||
profile: {
|
||||
id: string;
|
||||
fullname: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
};
|
||||
answers: {
|
||||
answers: string[];
|
||||
group_member_id: string;
|
||||
id: number;
|
||||
open_answer: string;
|
||||
point: number;
|
||||
question_id: number;
|
||||
submission_id: string;
|
||||
}[];
|
||||
}
|
||||
//===========================================
|
||||
|
||||
interface CourseMetadata {
|
||||
requirements?: string;
|
||||
description?: string;
|
||||
goals?: string;
|
||||
videoUrl?: '';
|
||||
showDiscount?: false;
|
||||
discount?: 0;
|
||||
paymentLink?: string;
|
||||
reward?: {
|
||||
show: boolean;
|
||||
description: string;
|
||||
};
|
||||
instructor?: {
|
||||
name: string;
|
||||
role: string;
|
||||
coursesNo: number;
|
||||
description: string;
|
||||
imgUrl: string;
|
||||
};
|
||||
reviews?: Array<Review>;
|
||||
lessonTabsOrder?: Array<Tabs>;
|
||||
grading?: boolean;
|
||||
lessonDownload?: boolean;
|
||||
allowNewStudent: boolean;
|
||||
}
|
||||
|
||||
export interface LessonCommentInsertPayload {
|
||||
id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
lesson_id: string;
|
||||
groupmember_id: string;
|
||||
comment: string;
|
||||
}
|
||||
export interface LessonComment {
|
||||
name: string;
|
||||
avatar: string;
|
||||
comment: string;
|
||||
commentAt: string | Date;
|
||||
}
|
||||
|
||||
// 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;
|
||||
tutors: Profile[];
|
||||
members: Groupmember[];
|
||||
}
|
||||
|
||||
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: string; // 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;
|
||||
status: string;
|
||||
is_published?: boolean;
|
||||
progress_rate?: number;
|
||||
total_lessons?: number;
|
||||
attendance: {
|
||||
student_id: string;
|
||||
lesson_id: string;
|
||||
is_present: boolean;
|
||||
id: number;
|
||||
}[];
|
||||
lessons?: Lesson[];
|
||||
polls: { status: 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 enum VideoType {
|
||||
youtube,
|
||||
muse
|
||||
}
|
||||
|
||||
export interface LessonPage {
|
||||
id?: string | null;
|
||||
totalExercises: number;
|
||||
totalComments: number;
|
||||
isSaving: Boolean;
|
||||
materials: {
|
||||
note: string;
|
||||
slide_url: string;
|
||||
videos: Array<{
|
||||
type: string;
|
||||
link: string;
|
||||
metadata?: {
|
||||
svid?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
exercises: [];
|
||||
lesson_completion: LessonCompletion[];
|
||||
}
|
||||
|
||||
export interface LessonCompletion {
|
||||
id?: number;
|
||||
lesson_id: string;
|
||||
profile_id: string;
|
||||
is_complete: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
note?: any; // type unknown;
|
||||
videos?: []; // 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: string; // type unknown;
|
||||
public?: boolean;
|
||||
lesson_at?: string;
|
||||
teacher_id?: string /* foreign key to profile.id */;
|
||||
is_unlocked?: boolean;
|
||||
call_url?: string;
|
||||
order?: number;
|
||||
course?: Course;
|
||||
profile?: Profile;
|
||||
lesson_completion: LessonCompletion[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
interface Tabs {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export interface CurrentOrg {
|
||||
id: string;
|
||||
name: string;
|
||||
shortName: string;
|
||||
siteName: string;
|
||||
avatar_url: string;
|
||||
memberId: string;
|
||||
role_id: string;
|
||||
landingpage: {
|
||||
[key: string]: any;
|
||||
};
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export interface OrgTeamMember {
|
||||
id: number;
|
||||
email: string;
|
||||
verified: boolean;
|
||||
profileId?: string;
|
||||
fullname: string;
|
||||
role: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export interface OrgAudience {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
date_joined: 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