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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user