feat: setup drizzle (#594)
* chore: migration to SvelteKit 2 * stage 1: migrate to svelte 5 and fix rerendering issues * stage 2: fix more $: reactivity across dashboard * fix merge conflicts * chore: migrate code in main to svelte 5 * chore: update preventDefault * chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte * chore: enforce .ts across dashboard * rename from classroomio-com to website * stage-3: Replace carbon icon svelte with lucide icon * update lock file * chore: finish icon replacement * fix: settings and setup page multiple rerender * stage 3: More icon and rerendering fixes * feat: setup drizzle and better auth * fix: onboarding validation error showing translation key #586 (#593) * fix: onboarding validation error showing translation key #586 * fix: remove debug log for profile update errors * chore: add a clean command * fix: remove message from email verification response
This commit is contained in:
@@ -1,30 +1,34 @@
|
||||
export const blockedSubdomain = [
|
||||
'academy',
|
||||
'app',
|
||||
'apps',
|
||||
'play',
|
||||
'support',
|
||||
'demo',
|
||||
'help',
|
||||
'teachers',
|
||||
'church',
|
||||
'tech',
|
||||
'blog',
|
||||
'build',
|
||||
'campaign',
|
||||
'church',
|
||||
'cloud',
|
||||
'community',
|
||||
'conference',
|
||||
'campaign',
|
||||
'blog',
|
||||
'forum',
|
||||
'mobile',
|
||||
'course',
|
||||
'courses',
|
||||
'demo',
|
||||
'dev',
|
||||
'stage',
|
||||
'prod',
|
||||
'forum',
|
||||
'group',
|
||||
'groups',
|
||||
'academy',
|
||||
'schools',
|
||||
'training',
|
||||
'cloud',
|
||||
'help',
|
||||
'launch',
|
||||
'launchweek',
|
||||
'course',
|
||||
'launch'
|
||||
'mobile',
|
||||
'play',
|
||||
'prod',
|
||||
'production',
|
||||
'schools',
|
||||
'stage',
|
||||
'staging',
|
||||
'support',
|
||||
'teacher',
|
||||
'teachers',
|
||||
'tech',
|
||||
'training'
|
||||
];
|
||||
|
||||
+2
-6
@@ -1,16 +1,12 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
export const themeImages = {
|
||||
standard: {
|
||||
card: 'https://pgrest.classroomio.com/storage/v1/object/public/avatars/quizes/standard-quiz-card.svg',
|
||||
editor:
|
||||
'https://pgrest.classroomio.com/storage/v1/object/public/avatars/quizes/standard-quiz-editor.svg',
|
||||
editor: 'https://pgrest.classroomio.com/storage/v1/object/public/avatars/quizes/standard-quiz-editor.svg',
|
||||
play: 'https://pgrest.classroomio.com/storage/v1/object/public/avatars/quizes/standard-quiz-play.svg'
|
||||
},
|
||||
mainland: {
|
||||
card: 'https://pgrest.classroomio.com/storage/v1/object/public/avatars/quizes/mainstream-quiz-card.svg',
|
||||
editor:
|
||||
'https://pgrest.classroomio.com/storage/v1/object/public/avatars/quizes/mainstream-quiz-editor.svg',
|
||||
editor: 'https://pgrest.classroomio.com/storage/v1/object/public/avatars/quizes/mainstream-quiz-editor.svg',
|
||||
play: 'https://pgrest.classroomio.com/storage/v1/object/public/avatars/quizes/mainstream-quiz-play.svg'
|
||||
}
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import isSubmissionEarly from './isSubmissionEarly';
|
||||
|
||||
describe('isSubmissionEarly', () => {
|
||||
test('should return true when createdAt is earlier than dueDate', () => {
|
||||
const dueDate = Date.now();
|
||||
const createdAt = new Date('2023-12-15');
|
||||
const result = isSubmissionEarly(createdAt, dueDate);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true when both createdAt and dueDate are null', () => {
|
||||
const dueDate = null;
|
||||
const createdAt = null;
|
||||
const result = isSubmissionEarly(createdAt, dueDate);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
test('should return true when createdAt is the same as dueDate', () => {
|
||||
const dueDate = new Date('2023-12-15');
|
||||
const createdAt = new Date('2023-12-15');
|
||||
const result = isSubmissionEarly(createdAt, dueDate);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when createdAt is later than dueDate', () => {
|
||||
const dueDate = new Date('2023-12-15');
|
||||
const createdAt = new Date('2023-12-16');
|
||||
const result = isSubmissionEarly(createdAt, dueDate);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -83,7 +83,7 @@ export async function getProfile({
|
||||
}
|
||||
|
||||
// Check if user has profile
|
||||
let {
|
||||
const {
|
||||
data: profileData,
|
||||
error,
|
||||
status
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { isCourseFree } from './course';
|
||||
|
||||
describe('course.ts', () => {
|
||||
test('Should return True when cost is 0', () => {
|
||||
const result = isCourseFree(0);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Should return true when cost is negative', () => {
|
||||
const result = isCourseFree(-10);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Should return True when cost is Not A Number', () => {
|
||||
const result = isCourseFree(NaN);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { calDateDiff, getGreeting } from './date';
|
||||
|
||||
describe('calDateDiff', () => {
|
||||
test('should return the correct time difference when the input date is in the past', () => {
|
||||
const inputDate = new Date('2022-01-01');
|
||||
const expectedOutput = '2 years ago';
|
||||
|
||||
const result = calDateDiff(inputDate);
|
||||
|
||||
expect(result).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should return the correct time difference when the input date is a number', () => {
|
||||
const inputDate = new Date(1640995200000); // January 1, 2022
|
||||
const expectedOutput = '2 years ago';
|
||||
|
||||
const result = calDateDiff(inputDate);
|
||||
|
||||
expect(result).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGreeting', () => {
|
||||
test("should return 'Good Morning' when the current time is before 12pm", () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-12-10T00:21:01.691Z'));
|
||||
expect(getGreeting()).toBe('Good Morning');
|
||||
});
|
||||
test("should return 'Good Afternoon' when the current time is after 12pm", () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-12-10T14:21:01.691Z'));
|
||||
expect(getGreeting()).toBe('Good Afternoon');
|
||||
});
|
||||
test("should return 'Good Evening' when the current time is after 6pm", () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-12-10T19:21:01.691Z'));
|
||||
expect(getGreeting()).toBe('Good Evening');
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { formatYoutubeVideo, getEmbedId } from './formatYoutubeVideo';
|
||||
|
||||
describe('format youtube video', () => {
|
||||
test('should return the same url when given a valid youtube embed url', () => {
|
||||
const url = 'https://www.youtube.com/embed/qajK1J1neAM';
|
||||
const errors = { video: '' };
|
||||
const result = formatYoutubeVideo(url, errors);
|
||||
expect(result).toBe(url);
|
||||
});
|
||||
|
||||
test('should return the correct embed url when given a youtube url with a start time parameter', () => {
|
||||
const url = 'https://www.youtube.com/watch?v=qajK1J1neAM&start=123';
|
||||
const errors = { video: '' };
|
||||
const result = formatYoutubeVideo(url, errors);
|
||||
expect(result).toBe('https://www.youtube.com/embed/qajK1J1neAM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmbedId', () => {
|
||||
test('should return the correct embed id when the url contains "embed"', () => {
|
||||
const url = 'https://www.youtube.com/embed/qajK1J1neAM';
|
||||
const expectedEmbedId = 'qajK1J1neAM';
|
||||
|
||||
const result = getEmbedId(url);
|
||||
|
||||
expect(result).toBe(expectedEmbedId);
|
||||
});
|
||||
|
||||
test('should return an empty string when the url does not contain "watch" or "embed" and is not in the format "youtu.be/"', () => {
|
||||
const url = 'https://www.youtube.com/invalid-url';
|
||||
const expectedEmbedId = '';
|
||||
|
||||
const result = getEmbedId(url);
|
||||
|
||||
expect(result).toBe(expectedEmbedId);
|
||||
});
|
||||
|
||||
test('should return an empty string when the url is an empty string', () => {
|
||||
const url = '';
|
||||
const expectedEmbedId = '';
|
||||
|
||||
const result = getEmbedId(url);
|
||||
|
||||
expect(result).toBe(expectedEmbedId);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import genUniqueId from './genUniqueId';
|
||||
|
||||
describe('Generate Unique Ids', () => {
|
||||
test('should start with letter', () => {
|
||||
const result = genUniqueId();
|
||||
const firstChar = result.charAt(0);
|
||||
expect(firstChar).toMatch(/[a-zA-Z]/);
|
||||
});
|
||||
|
||||
test('should not contain special characters', () => {
|
||||
const result = genUniqueId();
|
||||
expect(result).toMatch(/^[a-zA-Z0-9]+$/);
|
||||
});
|
||||
|
||||
test('should return different ids on multiple calls', () => {
|
||||
const result1 = genUniqueId();
|
||||
const result2 = genUniqueId();
|
||||
expect(result1).not.toEqual(result2);
|
||||
});
|
||||
test('multiple calls in closer time frames should have same first 3 letters', () => {
|
||||
const result1 = genUniqueId();
|
||||
const result2 = genUniqueId();
|
||||
expect(result1.substring(0, 4)).toEqual(result2.substring(0, 4));
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import generateSlug from './generateSlug';
|
||||
|
||||
describe('generateSlug', () => {
|
||||
test('should return a string with lowercase letters and hyphens when given a valid title', () => {
|
||||
const title = 'This is a Valid Title';
|
||||
const expected = 'this-is-a-valid-title' + `-${new Date().getTime()}`;
|
||||
const result = generateSlug(title);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('should include the current timestamp at the end of the string', () => {
|
||||
const title = 'This is a Valid Title';
|
||||
const result = generateSlug(title);
|
||||
const timestamp = result.split('-').pop();
|
||||
expect(Number(timestamp)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import generateUUID from './generateUUID';
|
||||
import { isUUID } from './isUUID';
|
||||
|
||||
// This test suite tests both the generateUUID and isUUID functions
|
||||
describe('GenerateUUID', () => {
|
||||
test('should be true when tested with the uuid function', () => {
|
||||
const result = generateUUID();
|
||||
expect(isUUID(result)).toBe(true);
|
||||
});
|
||||
|
||||
test('should generate unique UUIDs each time', () => {
|
||||
const uuid1 = generateUUID();
|
||||
const uuid2 = generateUUID();
|
||||
expect(uuid1).not.toBe(uuid2);
|
||||
});
|
||||
|
||||
test('should return true for a valid UUID', () => {
|
||||
const uuid = '123e4567-e89b-12d3-a456-426614174000';
|
||||
expect(isUUID(uuid)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for an invalid UUID', () => {
|
||||
const uuid = 'invalid-uuid';
|
||||
expect(isUUID(uuid)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
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
|
||||
let d = new Date().getTime(); //Timestamp
|
||||
let 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
|
||||
let r = Math.random() * 16; //random number between 0 and 16
|
||||
if (d > 0) {
|
||||
//Use timestamp until depleted
|
||||
r = (d + r) % 16 | 0;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import getCurrencyFormatter from './getCurrencyFormatter';
|
||||
|
||||
describe('GetCurrencyFormatter', () => {
|
||||
test('should return a new instance of Intl.NumberFormat with the correct locale and currency options when currency is "NGN"', () => {
|
||||
const result = getCurrencyFormatter('NGN');
|
||||
expect(result).toBeInstanceOf(Intl.NumberFormat);
|
||||
expect(result.resolvedOptions().locale).toBe('en-NG');
|
||||
expect(result.resolvedOptions().currency).toBe('NGN');
|
||||
expect(result.resolvedOptions().minimumFractionDigits).toBe(2);
|
||||
});
|
||||
|
||||
test('should return a new instance of Intl.NumberFormat with the correct locale and currency options when currency is "USD"', () => {
|
||||
const result = getCurrencyFormatter('USD');
|
||||
expect(result).toBeInstanceOf(Intl.NumberFormat);
|
||||
expect(result.resolvedOptions().locale).toBe('en-US');
|
||||
expect(result.resolvedOptions().currency).toBe('USD');
|
||||
expect(result.resolvedOptions().minimumFractionDigits).toBe(2);
|
||||
});
|
||||
|
||||
test('should return a new instance of Intl.NumberFormat with the correct locale and currency options when currency is "EUR"', () => {
|
||||
const result = getCurrencyFormatter('EUR');
|
||||
expect(result).toBeInstanceOf(Intl.NumberFormat);
|
||||
expect(result.resolvedOptions().locale).toBe('en-US');
|
||||
expect(result.resolvedOptions().currency).toBe('EUR');
|
||||
expect(result.resolvedOptions().minimumFractionDigits).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { isObject } from './isObject';
|
||||
|
||||
describe('isObject', () => {
|
||||
test('should return true for an empty object', () => {
|
||||
const obj = {};
|
||||
expect(isObject(obj)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for a non-object', () => {
|
||||
const obj = 'not an object';
|
||||
expect(isObject(obj)).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for a nested object', () => {
|
||||
const obj = { prop1: { prop2: 'value2' } };
|
||||
expect(isObject(obj)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return true for an object with properties', () => {
|
||||
const obj = { prop1: 'value1', prop2: 'value2' };
|
||||
expect(isObject(obj)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export default (created_at, due_date) => {
|
||||
return !!due_date ? !!(new Date(created_at).getTime() <= new Date(due_date).getTime()) : true;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export default (created_at, due_date) => {
|
||||
return due_date ? !!(new Date(created_at).getTime() <= new Date(due_date).getTime()) : true;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
export function isUUID(uuid: string) {
|
||||
let s = '' + uuid;
|
||||
const 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}$'
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { genQuizPin, generateSitename } from './org';
|
||||
|
||||
describe('Generate Quiz Pin', () => {
|
||||
test('should return a random 6-digit pin', () => {
|
||||
const result = genQuizPin();
|
||||
expect(result.toString().length).toBe(6);
|
||||
});
|
||||
|
||||
test('should return random pin each time', () => {
|
||||
const result1 = genQuizPin();
|
||||
const result2 = genQuizPin();
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
|
||||
test('should generate a pin between 100000 and 999999', () => {
|
||||
const result = genQuizPin();
|
||||
expect(result).toBeGreaterThanOrEqual(100000);
|
||||
expect(result).toBeLessThanOrEqual(999999);
|
||||
});
|
||||
|
||||
test('should not generate a negative pin', () => {
|
||||
const result = genQuizPin();
|
||||
expect(result).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should generate an integer pin', () => {
|
||||
const result = genQuizPin();
|
||||
expect(Number.isInteger(result)).toBe(true);
|
||||
});
|
||||
|
||||
test('should not generate a zero pin', () => {
|
||||
const result = genQuizPin();
|
||||
expect(result).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generate Site name', () => {
|
||||
test('should return slug-like string', () => {
|
||||
const result = generateSitename('classroom org international');
|
||||
const expected = 'classroom-org-international';
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
test('should remove special characters', () => {
|
||||
const result = generateSitename('classroom@ org! international');
|
||||
const expected = 'classroom-org-international';
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('should return empty string when orgName is an empty string', () => {
|
||||
const result = generateSitename('');
|
||||
const expected = '';
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
test('should return slug-like string with all uppercase characters converted to lowercase', () => {
|
||||
const result = generateSitename('Classroom Org International');
|
||||
const expected = 'classroom-org-international';
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export function genQuizPin(): number {
|
||||
var minm = 100000;
|
||||
var maxm = 999999;
|
||||
const minm = 100000;
|
||||
const maxm = 999999;
|
||||
return Math.floor(Math.random() * (maxm - minm + 1)) + minm;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { removeDuplicate } from './removeDuplicate';
|
||||
|
||||
describe('removeDuplicate', () => {
|
||||
test('should remove duplicate elements of an array', () => {
|
||||
const result = removeDuplicate([1, 1, 2, 3, 3, 4, 5]);
|
||||
const expected = [1, 2, 3, 4, 5];
|
||||
expect(result).toStrictEqual(expected);
|
||||
expect(result.length).toBe(5);
|
||||
});
|
||||
|
||||
test('should return an empty array', () => {
|
||||
const result = removeDuplicate([]);
|
||||
const expected = [];
|
||||
expect(result).toStrictEqual(expected);
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should return an array with the same element', () => {
|
||||
const result = removeDuplicate([1]);
|
||||
const expected = [1];
|
||||
expect(result).toStrictEqual(expected);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should return an array with only one element', () => {
|
||||
const result = removeDuplicate([1, 1, 1, 1]);
|
||||
const expected = [1];
|
||||
expect(result).toStrictEqual(expected);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should return an array with the unique elements in the same order as the original array', () => {
|
||||
const result = removeDuplicate([1, 2, 3, 4, 5, 1, 2, 3, 4, 5]);
|
||||
const expected = [1, 2, 3, 4, 5];
|
||||
expect(result).toStrictEqual(expected);
|
||||
expect(result.length).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import showAppsSideBar from './showAppsSideBar';
|
||||
describe('ShowAppSideBar', () => {
|
||||
test("should return true when path includes '/lessons/' and does not include 'landingpage'", () => {
|
||||
const path = '/lessons/example';
|
||||
const result = showAppsSideBar(path);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false when path does not include '/lessons/'", () => {
|
||||
const path = '/home';
|
||||
const result = showAppsSideBar(path);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false when path includes 'landingpage'", () => {
|
||||
const path = '/lessons/landingpage';
|
||||
const result = showAppsSideBar(path);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false when path is empty', () => {
|
||||
const path = '';
|
||||
const result = showAppsSideBar(path);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { capitalizeFirstLetter } from './string';
|
||||
|
||||
describe('capitalize first letter', () => {
|
||||
test('should return the same string with the first letter capitalized', () => {
|
||||
const result = capitalizeFirstLetter('classroom');
|
||||
expect(result).toEqual('Classroom');
|
||||
});
|
||||
|
||||
test('should return an empty string when given an empty string', () => {
|
||||
const result = capitalizeFirstLetter('');
|
||||
expect(result).toEqual('');
|
||||
});
|
||||
|
||||
test('should return the same string when given a string with all uppercase letters', () => {
|
||||
const result = capitalizeFirstLetter('UPPERCASE');
|
||||
expect(result).toEqual('UPPERCASE');
|
||||
});
|
||||
|
||||
test('should return the same string when given a string with a non-alphabetical first character', () => {
|
||||
const result = capitalizeFirstLetter('123abc');
|
||||
expect(result).toEqual('123abc');
|
||||
});
|
||||
|
||||
test('should return the first letter of the first word capitalized when given a sentence', () => {
|
||||
const result = capitalizeFirstLetter('classroomio is an open source project');
|
||||
expect(result).toEqual('Classroomio is an open source project');
|
||||
});
|
||||
});
|
||||
@@ -12,17 +12,13 @@ export let supabase: SupabaseClient;
|
||||
export const getServerSupabase = () => {
|
||||
if (supabase) return supabase;
|
||||
|
||||
supabase = createClient(
|
||||
config.supabaseConfig.url,
|
||||
env.PRIVATE_SUPABASE_SERVICE_ROLE || 'some-key-here',
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false
|
||||
}
|
||||
supabase = createClient(config.supabaseConfig.url, env.PRIVATE_SUPABASE_SERVICE_ROLE || 'some-key-here', {
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
detectSessionInUrl: false
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return supabase;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
export function once(fn) {
|
||||
return function (event) {
|
||||
// @ts-ignore
|
||||
if (fn) fn.call(this, event);
|
||||
fn = null;
|
||||
};
|
||||
}
|
||||
|
||||
export function preventDefault(fn?: Function) {
|
||||
return function (event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!fn) return;
|
||||
|
||||
// @ts-ignore
|
||||
fn.call(this, event);
|
||||
};
|
||||
}
|
||||
|
||||
export function stopPropagation(fn?: Function) {
|
||||
return function (event: Event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!fn) return;
|
||||
|
||||
// @ts-ignore
|
||||
fn.call(this, event);
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { env } from '$env/dynamic/public';
|
||||
|
||||
var win, output, textarea;
|
||||
let win, output, textarea;
|
||||
|
||||
// Follow this guide to get source code: https://editor.codecogs.com/docs/3-Tiny_MCE_v6.php
|
||||
export const addMathPlugin = (tinymce: any) => {
|
||||
tinymce.PluginManager.add('eqneditor', function (editor, url) {
|
||||
// Load necessary javascript for editor from CodeCogs
|
||||
var sl = new tinymce.dom.ScriptLoader();
|
||||
const sl = new tinymce.dom.ScriptLoader();
|
||||
sl.add('https://assets.cdn.clsrio.com/eqneditor.api.min.js');
|
||||
var loaded = sl.loadQueue();
|
||||
const loaded = sl.loadQueue();
|
||||
|
||||
// Load custom CSS
|
||||
tinymce.DOM.loadCSS('https://assets.cdn.clsrio.com/eqneditor_1.css');
|
||||
@@ -44,7 +44,7 @@ export const addMathPlugin = (tinymce: any) => {
|
||||
);
|
||||
|
||||
function showDialog(inp = '') {
|
||||
var http = 'https:' == document.location.protocol ? 'https://' : 'http://';
|
||||
const http = 'https:' == document.location.protocol ? 'https://' : 'http://';
|
||||
|
||||
win = editor.windowManager.open({
|
||||
title: 'Equation Editor',
|
||||
@@ -124,8 +124,8 @@ export const addMathPlugin = (tinymce: any) => {
|
||||
.addHistoryMenu(new EqEditor.History('history'));
|
||||
EqEditor.Toolbar.link('toolbar').addTextArea(textarea);
|
||||
|
||||
var nodes = document.getElementById('history')?.childNodes || [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
const nodes = document.getElementById('history')?.childNodes || [];
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
nodes[i].style.padding = 'revert';
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export const addMathPlugin = (tinymce: any) => {
|
||||
|
||||
editor.on('dblclick', function (e) {
|
||||
if (e.target.nodeName.toLowerCase() == 'img') {
|
||||
var sName = e.target.src.match(/(gif|svg)\.image\?(.*)/);
|
||||
const sName = e.target.src.match(/(gif|svg)\.image\?(.*)/);
|
||||
if (sName != null) showDialog(sName[2]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -59,8 +59,7 @@ export const config = {
|
||||
]
|
||||
};
|
||||
|
||||
export const { t, loading, locales, locale, initialized, translations, loadTranslations } =
|
||||
new i18n(config);
|
||||
export const { t, loading, locales, locale, initialized, translations, loadTranslations } = new i18n(config);
|
||||
|
||||
export const selectedLocale = writable<string>('en');
|
||||
|
||||
@@ -73,7 +72,7 @@ loading.subscribe(async ($loading) => {
|
||||
}
|
||||
});
|
||||
|
||||
export function handleLocaleChange(newLocale: string) {
|
||||
export function handleLocaleChange(newLocale: LOCALE) {
|
||||
if (!newLocale) {
|
||||
return;
|
||||
}
|
||||
@@ -83,11 +82,7 @@ export function handleLocaleChange(newLocale: string) {
|
||||
selectedLocale.set(newLocale);
|
||||
}
|
||||
|
||||
export function lessonFallbackNote(
|
||||
note: string,
|
||||
translation: Record<LOCALE, string>,
|
||||
locale: LOCALE
|
||||
) {
|
||||
export function lessonFallbackNote(note: string, translation: Record<LOCALE, string>, locale: LOCALE) {
|
||||
if (!translation) {
|
||||
return note;
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { validateEmail } from './validateEmail';
|
||||
|
||||
describe('validate email', () => {
|
||||
test('should return true for a valid email format', () => {
|
||||
const result = validateEmail('[email protected]');
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should return false for an empty email', () => {
|
||||
const result = validateEmail('');
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should return true for a valid email with uppercase letters', () => {
|
||||
const result = validateEmail('[email protected]');
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should return false for an invalid email format', () => {
|
||||
const result = validateEmail('test@example');
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should return true for a valid email with special characters', () => {
|
||||
const result = validateEmail('[email protected]');
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -2,141 +2,40 @@ import z from 'zod';
|
||||
import { get } from 'svelte/store';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import { validateEmail } from './validateEmail';
|
||||
import { t as _t, initialized } from '$lib/utils/functions/translations';
|
||||
import { t, initialized } from '$lib/utils/functions/translations';
|
||||
|
||||
const t = {
|
||||
get: (key: string) => {
|
||||
const isInitialized = get(initialized);
|
||||
if (!isInitialized) return key;
|
||||
|
||||
return _t.get(key);
|
||||
}
|
||||
};
|
||||
|
||||
const getOrgNameValidation = () =>
|
||||
z
|
||||
function getOrgNameValidation() {
|
||||
return z
|
||||
.string()
|
||||
.min(5, { message: `${t.get('validations.organization_name.min_char')}` })
|
||||
.refine((val) => !/^[-]|[-]$/.test(val), {
|
||||
message: `${t.get('validations.organization_name.hyphen_rule')}`
|
||||
});
|
||||
}
|
||||
|
||||
const getSiteNameValidation = () =>
|
||||
z
|
||||
function getSiteNameValidation() {
|
||||
return z
|
||||
.string()
|
||||
.min(5, { message: `${t.get('validations.site_name.min_char')}` })
|
||||
.refine((val) => !/^[-]|[-]$/.test(val), {
|
||||
message: `${t.get('validations.site_name.hyphen_rule')}`
|
||||
});
|
||||
|
||||
const getNewsfeedValidation = () =>
|
||||
z.string().min(5, { message: `${t.get('validations.news_feed.min_char')}` });
|
||||
|
||||
const lessonSchema = z.object({
|
||||
title: z.string().nonempty({ message: `${t.get('validations.lesson_schema.empty_title')}` }),
|
||||
lesson_at: z.string().optional(),
|
||||
call_url: z.string().nullable().optional(),
|
||||
is_unlocked: z.boolean().optional()
|
||||
});
|
||||
|
||||
const createQuizValidationSchema = z.object({
|
||||
title: z.string().min(6, {
|
||||
message: `${t.get('validations.course_payment.full_name.min_char')}`
|
||||
})
|
||||
});
|
||||
|
||||
const askCommunityValidationSchema = z.object({
|
||||
title: z.string().min(6, {
|
||||
message: `${t.get('validations.ask_community.title.min_char')}`
|
||||
}),
|
||||
body: z.string().min(20, {
|
||||
message: `${t.get('validations.ask_community.body.min_char')}`
|
||||
}),
|
||||
courseId: z.string().min(36, {
|
||||
message: `${t.get('validations.ask_community.course_id.select_course')}`
|
||||
})
|
||||
});
|
||||
const commentInCommunityValidationSchema = z.object({
|
||||
comment: z.string().min(6, {
|
||||
message: `${t.get('validations.comment_in_community.min_char')}`
|
||||
})
|
||||
});
|
||||
|
||||
const orgLandingpageValidationSchema = z.object({
|
||||
name: z.string().min(6, {
|
||||
message: `${t.get('validations.org_landing_page.name.min_char')}`
|
||||
}),
|
||||
email: z.string().email({
|
||||
message: `${t.get('validations.org_landing_page.email.invalid_email')}`
|
||||
}),
|
||||
phone: z.string().min(6, {
|
||||
message: `${t.get('validations.org_landing_page.phone.min_char')}`
|
||||
}),
|
||||
message: z.string().min(20, {
|
||||
message: `${t.get('validations.org_landing_page.message.min_char')}`
|
||||
})
|
||||
});
|
||||
|
||||
const forgotValidationSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: `${t.get('validations.forgot.invalid_email')}`
|
||||
})
|
||||
});
|
||||
|
||||
const authValidationSchema = z.object({
|
||||
email: z.string().email({
|
||||
message: 'validations.auth.email.invalid_email'
|
||||
}),
|
||||
password: z.string().min(6, {
|
||||
message: 'validations.auth.password.min_char'
|
||||
})
|
||||
});
|
||||
|
||||
const resetValidationSchema = z.object({
|
||||
password: z.string().min(6, {
|
||||
message: `${t.get('validations.reset.password.min_char')}`
|
||||
})
|
||||
});
|
||||
|
||||
const onboardingValidationSchema = {
|
||||
stepOne: z.object({
|
||||
fullname: z
|
||||
.string()
|
||||
.min(5, { message: `${t.get('validations.onboarding.step_one.full_name.min_char')}` }),
|
||||
orgName: getOrgNameValidation(),
|
||||
siteName: getSiteNameValidation()
|
||||
}),
|
||||
stepTwo: z.object({
|
||||
goal: z
|
||||
.string({
|
||||
required_error: `${t.get('validations.onboarding.step_two.goal.required')}`
|
||||
})
|
||||
.min(1),
|
||||
source: z
|
||||
.string({
|
||||
required_error: `${t.get('validations.onboarding.step_two.source.required')}`
|
||||
})
|
||||
.min(1)
|
||||
})
|
||||
};
|
||||
|
||||
const saveCertificateSchema = z.object({
|
||||
description: z.string().max(200, 'course.navItem.certificates.description_error'),
|
||||
is_certificate_downloadable: z.boolean(),
|
||||
certificate_theme: z.string()
|
||||
});
|
||||
}
|
||||
function getNewsfeedValidation() {
|
||||
return z.string().min(5, { message: `${t.get('validations.news_feed.min_char')}` });
|
||||
}
|
||||
|
||||
export const saveCertificateValidation = (fields = {}) => {
|
||||
const { error } = saveCertificateSchema.safeParse(fields);
|
||||
const schema = z.object({
|
||||
description: z.string().max(200, 'course.navItem.certificates.description_error'),
|
||||
is_certificate_downloadable: z.boolean(),
|
||||
certificate_theme: z.string()
|
||||
});
|
||||
|
||||
const { error } = schema.safeParse(fields);
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
const updateProfileValidationSchema = z.object({
|
||||
email: z.string().email({ message: 'validations.user_profile.email' }),
|
||||
username: z.string().nonempty({ message: 'validations.user_profile.username' }),
|
||||
fullname: z.string().min(5, { message: 'validations.user_profile.fullname' })
|
||||
});
|
||||
|
||||
export const getConfirmPasswordError = ({ password, confirmPassword }) => {
|
||||
return password > 6 && confirmPassword > 6 && password !== confirmPassword
|
||||
? `${t.get('validations.confirm_password.not_match')}`
|
||||
@@ -173,12 +72,30 @@ export const processErrors = (error, mapToId?: boolean) => {
|
||||
};
|
||||
|
||||
export const authValidation = (fields = {}) => {
|
||||
const { error } = authValidationSchema.safeParse(fields);
|
||||
const schema = z.object({
|
||||
email: z.string().email({
|
||||
message: 'validations.auth.email.invalid_email'
|
||||
}),
|
||||
password: z.string().min(6, {
|
||||
message: 'validations.auth.password.min_char'
|
||||
})
|
||||
});
|
||||
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const lessonValidation = (lesson = {}) => {
|
||||
const { error } = lessonSchema.safeParse(lesson);
|
||||
const schema = z.object({
|
||||
title: z.string().nonempty({ message: `${t.get('validations.lesson_schema.empty_title')}` }),
|
||||
lesson_at: z.string().optional(),
|
||||
call_url: z.string().nullable().optional(),
|
||||
is_unlocked: z.boolean().optional()
|
||||
});
|
||||
|
||||
const { error } = schema.safeParse(lesson);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
@@ -197,24 +114,70 @@ export const coursePaymentValidation = (fields = {}) => {
|
||||
};
|
||||
|
||||
export const resetValidation = (fields = {}) => {
|
||||
const { error } = resetValidationSchema.safeParse(fields);
|
||||
const schema = z.object({
|
||||
password: z.string().min(6, {
|
||||
message: `${t.get('validations.reset.password.min_char')}`
|
||||
})
|
||||
});
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const forgotValidation = (fields = {}) => {
|
||||
const { error } = forgotValidationSchema.safeParse(fields);
|
||||
const schema = z.object({
|
||||
email: z.string().email({
|
||||
message: `${t.get('validations.forgot.invalid_email')}`
|
||||
})
|
||||
});
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const orgLandingpageValidation = (fields = {}) => {
|
||||
const { error } = orgLandingpageValidationSchema.safeParse(fields);
|
||||
const schema = z.object({
|
||||
name: z.string().min(6, {
|
||||
message: `${t.get('validations.org_landing_page.name.min_char')}`
|
||||
}),
|
||||
email: z.string().email({
|
||||
message: `${t.get('validations.org_landing_page.email.invalid_email')}`
|
||||
}),
|
||||
phone: z.string().min(6, {
|
||||
message: `${t.get('validations.org_landing_page.phone.min_char')}`
|
||||
}),
|
||||
message: z.string().min(20, {
|
||||
message: `${t.get('validations.org_landing_page.message.min_char')}`
|
||||
})
|
||||
});
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const onboardingValidation = (fields = {}, step) => {
|
||||
const onboardingValidationSchema = {
|
||||
stepOne: z.object({
|
||||
fullname: z
|
||||
.string()
|
||||
.min(5, { message: `${t.get('validations.onboarding.step_one.full_name.min_char')}` }),
|
||||
orgName: getOrgNameValidation(),
|
||||
siteName: getSiteNameValidation()
|
||||
}),
|
||||
stepTwo: z.object({
|
||||
goal: z
|
||||
.string({
|
||||
required_error: `${t.get('validations.onboarding.step_two.goal.required')}`
|
||||
})
|
||||
.min(1),
|
||||
source: z
|
||||
.string({
|
||||
required_error: `${t.get('validations.onboarding.step_two.source.required')}`
|
||||
})
|
||||
.min(1)
|
||||
})
|
||||
};
|
||||
|
||||
const schema =
|
||||
step === 1 ? onboardingValidationSchema.stepOne : onboardingValidationSchema.stepTwo;
|
||||
const { error } = schema.safeParse(fields);
|
||||
@@ -223,7 +186,11 @@ export const onboardingValidation = (fields = {}, step) => {
|
||||
};
|
||||
|
||||
export const updateProfileValidation = (fields = {}) => {
|
||||
const schema = updateProfileValidationSchema;
|
||||
const schema = z.object({
|
||||
email: z.string().email({ message: 'validations.user_profile.email' }),
|
||||
username: z.string().nonempty({ message: 'validations.user_profile.username' }),
|
||||
fullname: z.string().min(5, { message: 'validations.user_profile.fullname' })
|
||||
});
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
@@ -297,19 +264,42 @@ export const updateProfileEmailValidation = (email) => {
|
||||
};
|
||||
|
||||
export const createQuizValidation = (fields = {}) => {
|
||||
const { error } = createQuizValidationSchema.safeParse(fields);
|
||||
const schema = z.object({
|
||||
title: z.string().min(6, {
|
||||
message: `${t.get('validations.course_payment.full_name.min_char')}`
|
||||
})
|
||||
});
|
||||
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const askCommunityValidation = (fields = {}) => {
|
||||
const { error } = askCommunityValidationSchema.safeParse(fields);
|
||||
const schema = z.object({
|
||||
title: z.string().min(6, {
|
||||
message: `${t.get('validations.ask_community.title.min_char')}`
|
||||
}),
|
||||
body: z.string().min(20, {
|
||||
message: `${t.get('validations.ask_community.body.min_char')}`
|
||||
}),
|
||||
courseId: z.string().min(36, {
|
||||
message: `${t.get('validations.ask_community.course_id.select_course')}`
|
||||
})
|
||||
});
|
||||
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
export const commentInCommunityValidation = (fields = {}) => {
|
||||
const { error } = commentInCommunityValidationSchema.safeParse(fields);
|
||||
const schema = z.object({
|
||||
comment: z.string().min(6, {
|
||||
message: `${t.get('validations.comment_in_community.min_char')}`
|
||||
})
|
||||
});
|
||||
const { error } = schema.safeParse(fields);
|
||||
|
||||
return processErrors(error);
|
||||
};
|
||||
|
||||
@@ -13,10 +13,7 @@ class ApiClient {
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
private async makeRequest(
|
||||
input: RequestInfo | URL,
|
||||
requestConfig: RequestConfig = {}
|
||||
): Promise<Response> {
|
||||
private async makeRequest(input: RequestInfo | URL, requestConfig: RequestConfig = {}): Promise<Response> {
|
||||
console.log('makeRequest', input, requestConfig);
|
||||
const {
|
||||
timeout = this.config.timeout,
|
||||
@@ -38,11 +35,7 @@ class ApiClient {
|
||||
|
||||
// Handle body serialization and content-type
|
||||
let requestBody = fetchConfig.body;
|
||||
if (
|
||||
fetchConfig.body &&
|
||||
typeof fetchConfig.body === 'object' &&
|
||||
!(fetchConfig.body instanceof FormData)
|
||||
) {
|
||||
if (fetchConfig.body && typeof fetchConfig.body === 'object' && !(fetchConfig.body instanceof FormData)) {
|
||||
// Auto-stringify objects (except FormData)
|
||||
requestBody = JSON.stringify(fetchConfig.body);
|
||||
if (!headers.has('Content-Type')) {
|
||||
@@ -96,22 +89,12 @@ class ApiClient {
|
||||
|
||||
if (response.status === 401) {
|
||||
await this.config.onAuthError();
|
||||
throw new ApiError(
|
||||
'Authentication failed',
|
||||
response.status,
|
||||
response.statusText,
|
||||
response
|
||||
);
|
||||
throw new ApiError('Authentication failed', response.status, response.statusText, response);
|
||||
}
|
||||
|
||||
if (response.status >= 400 && response.status < 500) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error');
|
||||
throw new ApiError(
|
||||
`Client error: ${errorText}`,
|
||||
response.status,
|
||||
response.statusText,
|
||||
response
|
||||
);
|
||||
throw new ApiError(`Client error: ${errorText}`, response.status, response.statusText, response);
|
||||
}
|
||||
|
||||
if (response.status >= 500) {
|
||||
@@ -132,12 +115,7 @@ class ApiClient {
|
||||
}
|
||||
|
||||
// Other status codes
|
||||
throw new ApiError(
|
||||
`Unexpected status: ${response.statusText}`,
|
||||
response.status,
|
||||
response.statusText,
|
||||
response
|
||||
);
|
||||
throw new ApiError(`Unexpected status: ${response.statusText}`, response.status, response.statusText, response);
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
|
||||
@@ -17,12 +17,7 @@ export const handleJsonResponse = async <T = any>(response: Response): Promise<T
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
throw new ApiError(
|
||||
'Failed to parse JSON response',
|
||||
response.status,
|
||||
response.statusText,
|
||||
response
|
||||
);
|
||||
throw new ApiError('Failed to parse JSON response', response.status, response.statusText, response);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,9 +64,7 @@ export const isTimeoutError = (error: unknown): boolean => {
|
||||
export type Result<T, E = ApiError> = { success: true; data: T } | { success: false; error: E };
|
||||
|
||||
// Generic safe request wrapper
|
||||
export const safeRequest = async <T = any>(
|
||||
requestFn: () => Promise<Response>
|
||||
): Promise<Result<T>> => {
|
||||
export const safeRequest = async <T = any>(requestFn: () => Promise<Response>): Promise<Result<T>> => {
|
||||
try {
|
||||
const response = await requestFn();
|
||||
const data = await handleJsonResponse<T>(response);
|
||||
@@ -88,9 +81,7 @@ export const safeRequest = async <T = any>(
|
||||
};
|
||||
|
||||
// Safe request wrapper for text responses
|
||||
export const safeRequestText = async (
|
||||
requestFn: () => Promise<Response>
|
||||
): Promise<Result<string>> => {
|
||||
export const safeRequestText = async (requestFn: () => Promise<Response>): Promise<Result<string>> => {
|
||||
try {
|
||||
const response = await requestFn();
|
||||
const data = await handleTextResponse(response);
|
||||
@@ -107,9 +98,7 @@ export const safeRequestText = async (
|
||||
};
|
||||
|
||||
// Safe request wrapper that returns the raw response
|
||||
export const safeRequestRaw = async (
|
||||
requestFn: () => Promise<Response>
|
||||
): Promise<Result<Response>> => {
|
||||
export const safeRequestRaw = async (requestFn: () => Promise<Response>): Promise<Result<Response>> => {
|
||||
try {
|
||||
const response = await requestFn();
|
||||
return { success: true, data: response };
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { PostgrestError, PostgrestSingleResponse } from '@supabase/supabase
|
||||
|
||||
import { GenericUploader } from './presign';
|
||||
import { QUESTION_TYPE } from '$lib/components/Question/constants';
|
||||
import type { Question } from '$lib/components/Course/types';
|
||||
import { STATUS } from '$lib/utils/constants/course';
|
||||
import { get } from 'svelte/store';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
@@ -61,10 +62,7 @@ export async function fetchProfileCourseProgress(
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
export async function checkExercisesComplete(
|
||||
lessonId: Lesson['id'],
|
||||
groupMemberId: Groupmember['id']
|
||||
) {
|
||||
export async function checkExercisesComplete(lessonId: Lesson['id'], groupMemberId: Groupmember['id']) {
|
||||
const { data, error } = await supabase.rpc('check_if_student_completed_exercises', {
|
||||
lesson_id_arg: lessonId,
|
||||
groupmember_id_arg: groupMemberId
|
||||
@@ -206,11 +204,7 @@ export async function uploadAvatar(courseId: string, avatar: string) {
|
||||
return logo;
|
||||
}
|
||||
|
||||
export async function updateCourse(
|
||||
courseId: Course['id'],
|
||||
avatar: string | undefined,
|
||||
course: Partial<Course>
|
||||
) {
|
||||
export async function updateCourse(courseId: Course['id'], avatar: string | undefined, course: Partial<Course>) {
|
||||
if (avatar && courseId) {
|
||||
const filename = `course/${courseId + Date.now()}.webp`;
|
||||
|
||||
@@ -237,7 +231,7 @@ export async function deleteCourse(courseId: Course['id']) {
|
||||
return await supabase.from('course').update({ status: 'DELETED' }).match({ id: courseId });
|
||||
}
|
||||
|
||||
export function addGroupMember(member: any) {
|
||||
export function addGroupMember(member) {
|
||||
return supabase.from('groupmember').insert(member).select();
|
||||
}
|
||||
|
||||
@@ -245,7 +239,7 @@ export function addDefaultNewsFeed(feed) {
|
||||
return supabase.from('course_newsfeed').insert(feed);
|
||||
}
|
||||
|
||||
export function updatedGroupMember(update: any, match: any) {
|
||||
export function updatedGroupMember(update, match) {
|
||||
return supabase.from('groupmember').update(update).match(match);
|
||||
}
|
||||
|
||||
@@ -284,8 +278,7 @@ export async function fetchLesson(lessonId: Lesson['id']) {
|
||||
.single();
|
||||
|
||||
if (data) {
|
||||
const videoKeys =
|
||||
data.videos?.filter((video) => video.type === 'upload')?.map((video) => video.key) || [];
|
||||
const videoKeys = data.videos?.filter((video) => video.type === 'upload')?.map((video) => video.key) || [];
|
||||
|
||||
const docKeys = data.documents?.map((doc) => doc.key) || [];
|
||||
|
||||
@@ -324,8 +317,12 @@ export function fetchLesssonLanguageHistory(lessonId: string, locale: string, en
|
||||
.order('timestamp', { ascending: false });
|
||||
}
|
||||
|
||||
export function createLesson(lesson: any) {
|
||||
return supabase.from('lesson').insert(lesson).select();
|
||||
export async function createLesson(
|
||||
lesson: Partial<Lesson>
|
||||
): Promise<{ data: Lesson[] | null; error: PostgrestError | null }> {
|
||||
const { data, error } = await supabase.from('lesson').insert(lesson).select();
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
export function createLessonSection(section: any) {
|
||||
return supabase.from('lesson_section').insert(section).select();
|
||||
@@ -414,15 +411,8 @@ export async function createExerciseFromTemplate(
|
||||
}
|
||||
|
||||
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;
|
||||
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
|
||||
@@ -438,8 +428,7 @@ export async function upsertExercise(questionnaire: any, exerciseId: Exercise['i
|
||||
const updatedQuestions = [];
|
||||
|
||||
for (const question of questions) {
|
||||
const { title, id, name, question_type, options, deleted_at, order, points, is_dirty } =
|
||||
question;
|
||||
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) {
|
||||
@@ -546,8 +535,8 @@ interface LooseObject {
|
||||
}
|
||||
|
||||
export async function submitExercise(
|
||||
answers: Array<string>,
|
||||
questions: Array<{ name: string; id: string }>,
|
||||
answers: Record<string, string>,
|
||||
questions: Question[],
|
||||
exerciseId: Exercise['id'],
|
||||
courseId: Course['id'],
|
||||
groupMemberId: Groupmember['id'] | undefined
|
||||
@@ -556,10 +545,7 @@ export async function submitExercise(
|
||||
return;
|
||||
}
|
||||
|
||||
const questionsByName = questions.reduce(
|
||||
(acc, q) => ({ ...acc, [q.name]: q.id }),
|
||||
{}
|
||||
) as LooseObject;
|
||||
const questionsByName = questions.reduce((acc, q) => ({ ...acc, [q.name]: q.id }), {}) as LooseObject;
|
||||
const questionAnswers = [];
|
||||
|
||||
const { data: submission } = await supabase
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { type Writable, get } from 'svelte/store';
|
||||
import {
|
||||
lessonDocUpload,
|
||||
lessonVideoUpload
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { lessonDocUpload, lessonVideoUpload } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
import axios from 'axios';
|
||||
import { classroomio } from '$lib/utils/services/api';
|
||||
@@ -22,9 +19,7 @@ export class GenericUploader {
|
||||
|
||||
async getDownloadPresignedUrl(keys: string[], type = this.uploadType) {
|
||||
const endpoint =
|
||||
type === 'document'
|
||||
? classroomio.course.presign.document.download
|
||||
: classroomio.course.presign.video.download;
|
||||
type === 'document' ? classroomio.course.presign.document.download : classroomio.course.presign.video.download;
|
||||
|
||||
const response = await endpoint.$post({
|
||||
json: {
|
||||
|
||||
@@ -46,10 +46,7 @@ interface FetchLMSExercisesResponse {
|
||||
error: PostgrestError | null;
|
||||
}
|
||||
|
||||
export async function fetchLMSExercises(
|
||||
profileId: string,
|
||||
orgId: string
|
||||
): Promise<FetchLMSExercisesResponse> {
|
||||
export async function fetchLMSExercises(profileId: string, orgId: string): Promise<FetchLMSExercisesResponse> {
|
||||
const { data, error } = await supabase
|
||||
.from('exercise')
|
||||
.select(
|
||||
|
||||
@@ -61,19 +61,11 @@ export async function createNewFeed(post: {
|
||||
}
|
||||
|
||||
export async function handleEditFeed(feedId: string, content: string) {
|
||||
const response = await supabase
|
||||
.from('course_newsfeed')
|
||||
.update({ content: content })
|
||||
.match({ id: feedId })
|
||||
.select();
|
||||
const response = await supabase.from('course_newsfeed').update({ content: content }).match({ id: feedId }).select();
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function createComment(comment: {
|
||||
content: string;
|
||||
author_id: string;
|
||||
course_newsfeed_id: string;
|
||||
}) {
|
||||
export async function createComment(comment: { content: string; author_id: string; course_newsfeed_id: string }) {
|
||||
const response = await supabase
|
||||
.from('course_newsfeed_comment')
|
||||
.insert({
|
||||
@@ -109,11 +101,7 @@ export async function deleteNewsFeed(feedId: string) {
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function getFeedForNotification(params: {
|
||||
feedId: string;
|
||||
authorId: string;
|
||||
supabase: typeof supabase;
|
||||
}) {
|
||||
export async function getFeedForNotification(params: { feedId: string; authorId: string; supabase: typeof supabase }) {
|
||||
const { data, error } = await params.supabase
|
||||
.from('course_newsfeed')
|
||||
.select(
|
||||
|
||||
@@ -19,8 +19,7 @@ const NAME_TO_PATH = {
|
||||
[NOTIFICATION_NAME.VERIFY_EMAIL]: '/api/email/verify_email',
|
||||
[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.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',
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// Code from https://github.com/vercel/platforms/blob/1ed55b13bcae97b037c69ec40b4c32df21c2412c/lib/domains.ts
|
||||
|
||||
import { env } from '$env/dynamic/private';
|
||||
import type {
|
||||
DomainConfigResponse,
|
||||
DomainResponse,
|
||||
DomainVerificationResponse
|
||||
} from '$lib/utils/types/org';
|
||||
import type { DomainConfigResponse, DomainResponse, DomainVerificationResponse } from '$lib/utils/types/org';
|
||||
|
||||
export const addDomainToVercel = async (domain: string) => {
|
||||
return await fetch(
|
||||
@@ -45,9 +41,7 @@ export const removeDomainFromVercelProject = async (domain: string) => {
|
||||
|
||||
export const removeDomainFromVercelTeam = async (domain: string) => {
|
||||
return await fetch(
|
||||
`https://api.vercel.com/v6/domains/${domain}${
|
||||
env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : ''
|
||||
}`,
|
||||
`https://api.vercel.com/v6/domains/${domain}${env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : ''}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.AUTH_BEARER_TOKEN}`
|
||||
@@ -78,9 +72,7 @@ export const getDomainResponse = async (
|
||||
|
||||
export const getConfigResponse = async (domain: string): Promise<DomainConfigResponse> => {
|
||||
return await fetch(
|
||||
`https://api.vercel.com/v6/domains/${domain}/config${
|
||||
env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : ''
|
||||
}`,
|
||||
`https://api.vercel.com/v6/domains/${domain}/config${env.TEAM_ID_VERCEL ? `?teamId=${env.TEAM_ID_VERCEL}` : ''}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
@@ -293,10 +293,7 @@ export async function createOrgPlan(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelOrgPlan(params: {
|
||||
subscriptionId: string;
|
||||
data: OrganizationPlan['payload'];
|
||||
}) {
|
||||
export async function cancelOrgPlan(params: { subscriptionId: string; data: OrganizationPlan['payload'] }) {
|
||||
return await supabase
|
||||
.from('organization_plan')
|
||||
.update({
|
||||
|
||||
@@ -23,7 +23,7 @@ export const initPosthog = (): void => {
|
||||
|
||||
export const initOrgAnalytics = (siteId: string) => {
|
||||
if (dev) return;
|
||||
|
||||
|
||||
// add js to head of page
|
||||
const script1 = document.createElement('script');
|
||||
script1.textContent = `
|
||||
@@ -35,9 +35,9 @@ export const initOrgAnalytics = (siteId: string) => {
|
||||
`;
|
||||
document.head.appendChild(script1);
|
||||
|
||||
const script2 = document.createElement('script');
|
||||
const script2 = document.createElement('script');
|
||||
script2.id = 'counterscale-script';
|
||||
script2.src = 'https://be13a4b3.counterscale-5jn.pages.dev/tracker.js';
|
||||
script2.defer = true;
|
||||
document.head.appendChild(script2);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -81,12 +81,7 @@ export async function fetchSubmission({
|
||||
}
|
||||
|
||||
export async function updateSubmission(
|
||||
{
|
||||
id,
|
||||
status_id,
|
||||
total,
|
||||
feedback
|
||||
}: { id: string; status_id?: number; total?: number; feedback?: string },
|
||||
{ id, status_id, total, feedback }: { id: string; status_id?: number; total?: number; feedback?: string },
|
||||
otherArgs?: Record<string, string>
|
||||
) {
|
||||
const toUpdate: {
|
||||
@@ -109,9 +104,6 @@ export async function deleteSubmission(id: string) {
|
||||
return supabase.from('submission').delete().match({ id });
|
||||
}
|
||||
|
||||
export async function updateQuestionAnswer(
|
||||
update: Record<string, string>,
|
||||
match: Record<string, string>
|
||||
) {
|
||||
export async function updateQuestionAnswer(update: Record<string, string>, match: Record<string, string>) {
|
||||
return supabase.from('question_answer').update(update).match(match);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { CurrentOrg, OrgAudience, OrgTeamMember } from '../types/org';
|
||||
import { browser, dev } from '$app/environment';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
import { PLAN } from 'shared/src/plans/constants';
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import { STEPS } from '../constants/quiz';
|
||||
|
||||
@@ -10,7 +10,7 @@ interface UserStore {
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
interface ProfileStore {
|
||||
export interface ProfileStore {
|
||||
id: string | undefined;
|
||||
fullname: string;
|
||||
avatar_url: string;
|
||||
|
||||
@@ -854,7 +854,8 @@
|
||||
"heading": "Beskrivelse",
|
||||
"describe": "Beskriv øvelsen til din studerende",
|
||||
"no": "Ingen beskrivelse"
|
||||
}
|
||||
},
|
||||
"add_question": "Tilføj et spørgsmål"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "Ny øvelse",
|
||||
|
||||
@@ -679,7 +679,8 @@
|
||||
},
|
||||
"no_answer": "Es wurde keine Antwort gegeben",
|
||||
"check": "Überprüfen",
|
||||
"address": "Geben Sie hier Ihre Adresse ein"
|
||||
"address": "Geben Sie hier Ihre Adresse ein",
|
||||
"add_question": "Fügen Sie eine Frage hinzu"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "Neue Übung",
|
||||
|
||||
@@ -779,6 +779,7 @@
|
||||
"submissions": "Submissions",
|
||||
"save": "Save",
|
||||
"preview": "Preview",
|
||||
"add_question": "Add a question",
|
||||
"reorder": "Reorder Questions",
|
||||
"delete_exercise": "Delete Exercise",
|
||||
"order_questions": "Order questions",
|
||||
|
||||
@@ -679,7 +679,8 @@
|
||||
},
|
||||
"no_answer": "No se proporcionó respuesta",
|
||||
"check": "Controlar",
|
||||
"address": "Escribe tu dirección aquí"
|
||||
"address": "Escribe tu dirección aquí",
|
||||
"add_question": "Añadir una pregunta"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "Nuevo ejercicio",
|
||||
|
||||
@@ -679,7 +679,8 @@
|
||||
},
|
||||
"no_answer": "Aucune réponse n'a été fournie",
|
||||
"check": "Vérifier",
|
||||
"address": "Écrivez votre adresse ici"
|
||||
"address": "Écrivez votre adresse ici",
|
||||
"add_question": "Ajouter une question"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "Nouvel exercice",
|
||||
|
||||
@@ -679,7 +679,8 @@
|
||||
},
|
||||
"no_answer": "कोई उत्तर नहीं दिया गया",
|
||||
"check": "जाँच करना",
|
||||
"address": "अपना पता यहां लिखें"
|
||||
"address": "अपना पता यहां लिखें",
|
||||
"add_question": "एक प्रश्न जोड़ें"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "नया व्यायाम",
|
||||
|
||||
@@ -847,7 +847,8 @@
|
||||
"describe": "Opisz ćwiczenie swoim uczniowi",
|
||||
"no": "Brak opisu"
|
||||
},
|
||||
"address": "Wpisz tutaj swój adres"
|
||||
"address": "Wpisz tutaj swój adres",
|
||||
"add_question": "Dodaj pytanie"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "Nowe ćwiczenie",
|
||||
|
||||
@@ -679,7 +679,8 @@
|
||||
},
|
||||
"no_answer": "Nenhuma resposta foi fornecida",
|
||||
"check": "Verificar",
|
||||
"address": "Escreva seu endereço aqui"
|
||||
"address": "Escreva seu endereço aqui",
|
||||
"add_question": "Adicione uma pergunta"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "Novo exercício",
|
||||
|
||||
@@ -679,7 +679,8 @@
|
||||
},
|
||||
"no_answer": "Ответ не был предоставлен",
|
||||
"check": "Проверять",
|
||||
"address": "Напишите здесь свой адрес"
|
||||
"address": "Напишите здесь свой адрес",
|
||||
"add_question": "Добавить вопрос"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "Новое упражнение",
|
||||
|
||||
@@ -679,7 +679,8 @@
|
||||
},
|
||||
"no_answer": "Không có câu trả lời nào được đưa ra",
|
||||
"check": "Kiểm tra",
|
||||
"address": "Viết địa chỉ của bạn ở đây"
|
||||
"address": "Viết địa chỉ của bạn ở đây",
|
||||
"add_question": "Thêm câu hỏi"
|
||||
},
|
||||
"new_exercise_modal": {
|
||||
"heading": "Bài tập mới",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// type T must be a HTMLElement (e.g. HTMLTextAreaElement extends HTMLElement)
|
||||
export type HTMLElementEvent<T extends HTMLElement> = Event & {
|
||||
target: T;
|
||||
currentTarget: T;
|
||||
};
|
||||
@@ -301,7 +301,7 @@ export type LessonDocument = {
|
||||
|
||||
export interface LessonPage {
|
||||
id?: string | null;
|
||||
title: '';
|
||||
title: string;
|
||||
totalExercises: number;
|
||||
totalComments: number;
|
||||
locale: LOCALE;
|
||||
@@ -313,7 +313,7 @@ export interface LessonPage {
|
||||
videos: LessonVideo[];
|
||||
documents: LessonDocument[];
|
||||
};
|
||||
exercises: [];
|
||||
exercises: Exercise[];
|
||||
lesson_completion: LessonCompletion[];
|
||||
}
|
||||
|
||||
@@ -329,6 +329,7 @@ export interface LessonCompletion {
|
||||
export interface Lesson {
|
||||
note?: any; // type unknown;
|
||||
videos?: []; // type unknown;
|
||||
documents?: []; // type unknown;
|
||||
slide_url?: any; // type unknown;
|
||||
course_id: string /* foreign key to course.id */;
|
||||
section_id?: string /* foreign key to course.id */;
|
||||
|
||||
@@ -1,3 +1,79 @@
|
||||
export interface OrgLandingPageJson {
|
||||
header: {
|
||||
title: string;
|
||||
titleHighlight: string;
|
||||
subtitle: string;
|
||||
action: {
|
||||
label: string;
|
||||
link: string;
|
||||
redirect: boolean;
|
||||
};
|
||||
banner: {
|
||||
video: string;
|
||||
image: string;
|
||||
type: string;
|
||||
show: boolean;
|
||||
};
|
||||
background: {
|
||||
image: string;
|
||||
show: boolean;
|
||||
};
|
||||
show: boolean;
|
||||
};
|
||||
aboutUs: {
|
||||
title: string;
|
||||
content: string;
|
||||
imageUrl: string;
|
||||
show: boolean;
|
||||
};
|
||||
courses: {
|
||||
title: string;
|
||||
titleHighlight: string;
|
||||
subtitle: string;
|
||||
show: boolean;
|
||||
};
|
||||
faq: {
|
||||
title: string;
|
||||
questions: Array<{
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
}>;
|
||||
show: boolean;
|
||||
};
|
||||
contact: {
|
||||
title: string;
|
||||
titleHighlight: string;
|
||||
subtitle: string;
|
||||
address: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
show: boolean;
|
||||
};
|
||||
mailinglist: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
buttonLabel: string;
|
||||
show: boolean;
|
||||
};
|
||||
customLinks: {
|
||||
show: boolean;
|
||||
links: Array<{
|
||||
id: number;
|
||||
label: string;
|
||||
url: string;
|
||||
openInNewTab: boolean;
|
||||
}>;
|
||||
};
|
||||
footer: {
|
||||
facebook: string;
|
||||
instagram: string;
|
||||
twitter: string;
|
||||
linkedin: string;
|
||||
show: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrgCustomization {
|
||||
dashboard: {
|
||||
community: boolean;
|
||||
@@ -23,9 +99,7 @@ export interface CurrentOrg {
|
||||
avatar_url: string;
|
||||
memberId: string;
|
||||
role_id: number;
|
||||
landingpage: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
landingpage: OrgLandingPageJson;
|
||||
customization: OrgCustomization;
|
||||
theme: string;
|
||||
organization_plan: {
|
||||
|
||||
Reference in New Issue
Block a user