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:
Rotimi Best
2025-11-01 15:49:29 +01:00
committed by GitHub
parent 97fb446df5
commit b96d91cd7c
801 changed files with 22279 additions and 10834 deletions
@@ -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);
}