feat: clone course (#582)
* feat: clone course work * feat: clone and delete a course * feat: clone and delete a course * feat: clean up * feat: attending to reviews * feat: attended to revies
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
## Supabase - DB
|
## Supabase - DB
|
||||||
PUBLIC_SUPABASE_ANON_KEY=
|
PUBLIC_SUPABASE_ANON_KEY=
|
||||||
PUBLIC_SUPABASE_URL=
|
PUBLIC_SUPABASE_URL=
|
||||||
|
PRIVATE_SUPABASE_SERVICE_ROLE=
|
||||||
|
|
||||||
## Cloudflare - Video uploads
|
## Cloudflare - Video uploads
|
||||||
# Not required - https://cdn.example.com
|
# Not required - https://cdn.example.com
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const envSchema = z.object({
|
|||||||
PORT: z.string().optional(),
|
PORT: z.string().optional(),
|
||||||
PUBLIC_SUPABASE_ANON_KEY: z.string(),
|
PUBLIC_SUPABASE_ANON_KEY: z.string(),
|
||||||
PUBLIC_SUPABASE_URL: z.string(),
|
PUBLIC_SUPABASE_URL: z.string(),
|
||||||
|
PRIVATE_SUPABASE_SERVICE_ROLE: z.string(),
|
||||||
REDIS_URL: z.string().optional(),
|
REDIS_URL: z.string().optional(),
|
||||||
SMTP_HOST: z.string().optional(),
|
SMTP_HOST: z.string().optional(),
|
||||||
SMTP_PASSWORD: z.string().optional(),
|
SMTP_PASSWORD: z.string().optional(),
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Hono } from 'hono';
|
||||||
|
import { ZCourseClone, TCourseClone } from '$src/types/course';
|
||||||
|
import { zValidator } from '@hono/zod-validator';
|
||||||
|
import { cloneCourse } from '$src/services/course/clone';
|
||||||
|
import { authMiddleware } from '$src/middlewares/auth';
|
||||||
|
import type { CloneCourseResponse, CloneCourseErrorResponse } from '$src/types/database';
|
||||||
|
import type { User } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
type Variables = {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cloneRouter = new Hono<{ Variables: Variables }>().post(
|
||||||
|
'/',
|
||||||
|
authMiddleware,
|
||||||
|
zValidator('json', ZCourseClone),
|
||||||
|
async (c) => {
|
||||||
|
try {
|
||||||
|
const validatedData = (c.req.valid as (key: 'json') => TCourseClone)('json');
|
||||||
|
const { id, title, description, slug, organizationId } = validatedData;
|
||||||
|
|
||||||
|
// Get user from context (set by authMiddleware)
|
||||||
|
const user = c.get('user');
|
||||||
|
|
||||||
|
// Clone the course
|
||||||
|
const newCourse = await cloneCourse(id, title, user.id, description, slug, organizationId);
|
||||||
|
|
||||||
|
const successResponse: CloneCourseResponse = {
|
||||||
|
success: true,
|
||||||
|
course: newCourse
|
||||||
|
};
|
||||||
|
return c.json(successResponse, 200);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cloning course:', error);
|
||||||
|
const errorResponse: CloneCourseErrorResponse = {
|
||||||
|
error: 'Failed to clone course',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
};
|
||||||
|
return c.json(errorResponse, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ZCertificateDownload, ZCourseDownloadContent } from '$src/types/course';
|
import { TCourseClone,TCourseDownloadContent, TCertificateDownload, ZCertificateDownload, ZCourseDownloadContent } from '$src/types/course';
|
||||||
|
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { generateCertificate } from '$src/utils/certificate';
|
import { generateCertificate } from '$src/utils/certificate';
|
||||||
@@ -6,11 +6,12 @@ import { generateCoursePdf } from '$src/utils/course';
|
|||||||
import { katexRouter } from '$src/routes/course/katex';
|
import { katexRouter } from '$src/routes/course/katex';
|
||||||
import { lessonRouter } from '$src/routes/course/lesson';
|
import { lessonRouter } from '$src/routes/course/lesson';
|
||||||
import { presignRouter } from '$src/routes/course/presign';
|
import { presignRouter } from '$src/routes/course/presign';
|
||||||
|
import { cloneRouter } from '$src/routes/course/clone';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
|
|
||||||
export const courseRouter = new Hono()
|
export const courseRouter = new Hono()
|
||||||
.post('/download/certificate', zValidator('json', ZCertificateDownload), async (c) => {
|
.post('/download/certificate', zValidator('json', ZCertificateDownload), async (c) => {
|
||||||
const validatedData = c.req.valid('json');
|
const validatedData = (c.req.valid as (key: 'json') => TCertificateDownload)('json');
|
||||||
|
|
||||||
const result = await generateCertificate(validatedData);
|
const result = await generateCertificate(validatedData);
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ export const courseRouter = new Hono()
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.post('/download/content', zValidator('json', ZCourseDownloadContent), async (c) => {
|
.post('/download/content', zValidator('json', ZCourseDownloadContent), async (c) => {
|
||||||
const validatedData = c.req.valid('json');
|
const validatedData = (c.req.valid as (key: 'json') => TCourseDownloadContent)('json');
|
||||||
|
|
||||||
const pdfBuffer = await generateCoursePdf(validatedData);
|
const pdfBuffer = await generateCoursePdf(validatedData);
|
||||||
|
|
||||||
@@ -47,4 +48,5 @@ export const courseRouter = new Hono()
|
|||||||
})
|
})
|
||||||
.route('/katex', katexRouter)
|
.route('/katex', katexRouter)
|
||||||
.route('/lesson', lessonRouter)
|
.route('/lesson', lessonRouter)
|
||||||
.route('/presign', presignRouter);
|
.route('/presign', presignRouter)
|
||||||
|
.route('/clone', cloneRouter);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { ZLessonDownloadContent } from '$src/types/course/lesson';
|
import { TLessonDownloadContent, ZLessonDownloadContent } from '$src/types/course/lesson';
|
||||||
import { generateLessonPdf } from '$src/utils/lesson';
|
import { generateLessonPdf } from '$src/utils/lesson';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ export const lessonRouter = new Hono().post(
|
|||||||
'/download/pdf',
|
'/download/pdf',
|
||||||
zValidator('json', ZLessonDownloadContent),
|
zValidator('json', ZLessonDownloadContent),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const validatedData = c.req.valid('json');
|
const validatedData = (c.req.valid as (key: 'json') => TLessonDownloadContent)('json');
|
||||||
|
|
||||||
const pdfBuffer = await generateLessonPdf(validatedData);
|
const pdfBuffer = await generateLessonPdf(validatedData);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,369 @@
|
|||||||
|
import { getSupabase } from '$src/utils/supabase';
|
||||||
|
import type { Course, Lesson, LessonSection, GroupMember } from '$src/types/database';
|
||||||
|
import type { PostgrestError } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
const supabase = getSupabase();
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const QUESTION_TYPE_TEXTAREA = 2; // Paragraph question type
|
||||||
|
const ROLE_TUTOR = 2; // Tutor role ID
|
||||||
|
|
||||||
|
// Reusable deep select query for lessons with all nested relations
|
||||||
|
const LESSON_DEEP_SELECT = `
|
||||||
|
*,
|
||||||
|
exercise!exercise_lesson_id_fkey(
|
||||||
|
id, title, description, due_by,
|
||||||
|
question!question_exercise_id_fkey(
|
||||||
|
*,
|
||||||
|
option!option_question_id_fkey(*),
|
||||||
|
question_type!question_question_type_id_fkey(id, label)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
lesson_language!public_lesson_language_lesson_id_fkey(id, content, locale)
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface FetchCourseDataResponse {
|
||||||
|
data: Course | null;
|
||||||
|
error: PostgrestError | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CloneGroupResponse {
|
||||||
|
newCourse: Course;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CloneSectionsResponse {
|
||||||
|
newSections: LessonSection[];
|
||||||
|
sectionIdMap?: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CloneLessonsResponse {
|
||||||
|
newLessons: Lesson[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddGroupMemberParams {
|
||||||
|
profile_id: string;
|
||||||
|
email: string;
|
||||||
|
group_id: string;
|
||||||
|
role_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetches course data with backward compatibility for courses that is either V1 or V2
|
||||||
|
async function fetchCourseData(courseId: string): Promise<FetchCourseDataResponse> {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('course')
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
*,
|
||||||
|
lesson_section(
|
||||||
|
id, title, order, created_at,
|
||||||
|
lessons:lesson(${LESSON_DEEP_SELECT})
|
||||||
|
),
|
||||||
|
lessons:lesson(${LESSON_DEEP_SELECT})
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.eq('id', courseId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return { data, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addGroupMember(params: AddGroupMemberParams): Promise<{
|
||||||
|
data: GroupMember[] | null;
|
||||||
|
error: PostgrestError | null;
|
||||||
|
}> {
|
||||||
|
const { data, error } = await supabase.from('groupmember').insert(params).select();
|
||||||
|
return { data, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneGroupAndBasicCourse(
|
||||||
|
course: Course,
|
||||||
|
newTitle: string,
|
||||||
|
userId: string,
|
||||||
|
newDescription?: string,
|
||||||
|
newSlug?: string,
|
||||||
|
organizationId?: string
|
||||||
|
): Promise<CloneGroupResponse> {
|
||||||
|
const { description } = course;
|
||||||
|
const finalDescription = newDescription || description;
|
||||||
|
const finalSlug = newSlug || null; // Use provided slug or null
|
||||||
|
|
||||||
|
// 1. Create group
|
||||||
|
const { data: newGroup, error: groupError } = await supabase
|
||||||
|
.from('group')
|
||||||
|
.insert({
|
||||||
|
name: newTitle,
|
||||||
|
description: finalDescription,
|
||||||
|
organization_id: organizationId
|
||||||
|
})
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (groupError) {
|
||||||
|
console.error('Group creation error:', groupError);
|
||||||
|
throw new Error(`Group creation failed: ${groupError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newGroup || newGroup.length === 0) {
|
||||||
|
throw new Error('Group was not created successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: group_id } = newGroup[0];
|
||||||
|
|
||||||
|
// Remove nested relations and metadata that shouldn't be inserted
|
||||||
|
const { lessons, lesson_section, id, updated_at, created_at, ...courseData } = course as any;
|
||||||
|
|
||||||
|
// 2. Create course with group_id
|
||||||
|
const { data: newCourse, error: courseError } = await supabase
|
||||||
|
.from('course')
|
||||||
|
.insert({
|
||||||
|
...courseData,
|
||||||
|
title: newTitle,
|
||||||
|
description: finalDescription,
|
||||||
|
slug: finalSlug,
|
||||||
|
group_id
|
||||||
|
})
|
||||||
|
.select();
|
||||||
|
|
||||||
|
if (courseError) {
|
||||||
|
console.error('Course creation error:', courseError);
|
||||||
|
throw new Error(`Course creation failed: ${courseError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newCourse || newCourse.length === 0) {
|
||||||
|
throw new Error('Course was not created successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Add group member (the user who is cloning)
|
||||||
|
await addGroupMember({
|
||||||
|
profile_id: userId,
|
||||||
|
email: '', // Email will be fetched from profile
|
||||||
|
group_id,
|
||||||
|
role_id: ROLE_TUTOR
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
newCourse: newCourse[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneLessonSections(
|
||||||
|
sections: LessonSection[],
|
||||||
|
courseId: string
|
||||||
|
): Promise<CloneSectionsResponse> {
|
||||||
|
if (!sections || sections.length === 0) {
|
||||||
|
return { newSections: [], sectionIdMap: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const clonedSections = sections
|
||||||
|
.sort((a, b) => new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime())
|
||||||
|
.map((section) => ({
|
||||||
|
title: section.title,
|
||||||
|
order: section.order,
|
||||||
|
course_id: courseId
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { data: newSections } = await supabase
|
||||||
|
.from('lesson_section')
|
||||||
|
.insert(clonedSections)
|
||||||
|
.select();
|
||||||
|
|
||||||
|
// Map old section IDs to new section IDs
|
||||||
|
const sectionIdMap = new Map<string, string>();
|
||||||
|
sections.forEach((oldSection, index) => {
|
||||||
|
if (newSections && newSections[index]) {
|
||||||
|
sectionIdMap.set(oldSection.id, newSections[index].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { newSections: newSections || [], sectionIdMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneLessons(
|
||||||
|
lessons: Lesson[],
|
||||||
|
courseId: string,
|
||||||
|
sectionIdMap?: Map<string, string>
|
||||||
|
): Promise<CloneLessonsResponse> {
|
||||||
|
const clonedLessons = lessons
|
||||||
|
.sort((a, b) => new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime())
|
||||||
|
.map((lesson, index) => ({
|
||||||
|
call_url: lesson.call_url,
|
||||||
|
course_id: courseId,
|
||||||
|
section_id: lesson.section_id && sectionIdMap ? sectionIdMap.get(lesson.section_id) : null,
|
||||||
|
is_unlocked: lesson.is_unlocked,
|
||||||
|
lesson_at: new Date().toISOString(),
|
||||||
|
note: lesson.note,
|
||||||
|
order: lesson.order || index + 1,
|
||||||
|
public: lesson.public,
|
||||||
|
slide_url: lesson.slide_url,
|
||||||
|
title: lesson.title,
|
||||||
|
videos: lesson.videos
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { data } = await supabase.from('lesson').insert(clonedLessons).select();
|
||||||
|
|
||||||
|
const newLessons = data?.map((dataItem, dataIndex) => {
|
||||||
|
return {
|
||||||
|
...dataItem,
|
||||||
|
exercise: lessons[dataIndex].exercise,
|
||||||
|
lesson_language: lessons[dataIndex].lesson_language
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { newLessons };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneLessonLanguages(lessons: Lesson[]): Promise<boolean> {
|
||||||
|
for (const lesson of lessons) {
|
||||||
|
const { id: lessonId, lesson_language = [] } = lesson;
|
||||||
|
|
||||||
|
if (!lesson_language || lesson_language.length === 0) continue;
|
||||||
|
|
||||||
|
const clonedLanguages = lesson_language.map((lang: any) => ({
|
||||||
|
lesson_id: lessonId,
|
||||||
|
content: lang.content,
|
||||||
|
locale: lang.locale
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { error } = await supabase.from('lesson_language').insert(clonedLanguages).select();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error cloning lesson languages:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneExercises(lessons: Lesson[]): Promise<boolean> {
|
||||||
|
// Clone exercises, questions, and options
|
||||||
|
for (const lesson of lessons) {
|
||||||
|
const { id: lessonId, exercise = [] } = lesson;
|
||||||
|
|
||||||
|
// Exercises
|
||||||
|
for (const exerciseItem of exercise) {
|
||||||
|
const { title, description, questions = [] } = exerciseItem;
|
||||||
|
|
||||||
|
// Insert exercise
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('exercise')
|
||||||
|
.insert({
|
||||||
|
description,
|
||||||
|
due_by: new Date().toISOString(),
|
||||||
|
lesson_id: lessonId,
|
||||||
|
title
|
||||||
|
})
|
||||||
|
.select();
|
||||||
|
|
||||||
|
const newExercise = Array.isArray(data) ? data[0] : data;
|
||||||
|
|
||||||
|
if (!newExercise) continue;
|
||||||
|
|
||||||
|
// Questions
|
||||||
|
for (const question of questions) {
|
||||||
|
const { options = [] } = question;
|
||||||
|
|
||||||
|
// Insert question
|
||||||
|
const { data: questionData } = await supabase
|
||||||
|
.from('question')
|
||||||
|
.insert({
|
||||||
|
name: question.name,
|
||||||
|
title: question.title,
|
||||||
|
points: question.points,
|
||||||
|
order: question.order,
|
||||||
|
question_type_id: question.question_type_id,
|
||||||
|
exercise_id: newExercise.id
|
||||||
|
})
|
||||||
|
.select();
|
||||||
|
|
||||||
|
const newQuestion = Array.isArray(questionData) ? questionData[0] : questionData;
|
||||||
|
|
||||||
|
if (!newQuestion) continue;
|
||||||
|
|
||||||
|
// Options: Don't map options for 'Paragraph' questions
|
||||||
|
if (QUESTION_TYPE_TEXTAREA !== newQuestion.question_type_id && options.length > 0) {
|
||||||
|
const newOptions = options.map((option: any) => ({
|
||||||
|
value: option.value,
|
||||||
|
label: option.label,
|
||||||
|
is_correct: option.is_correct,
|
||||||
|
question_id: newQuestion.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Insert options in batch
|
||||||
|
await supabase.from('option').insert(newOptions).select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cloneCourse(
|
||||||
|
courseId: string,
|
||||||
|
newTitle: string,
|
||||||
|
userId: string,
|
||||||
|
newDescription?: string,
|
||||||
|
newSlug?: string,
|
||||||
|
organizationId?: string
|
||||||
|
): Promise<Course> {
|
||||||
|
const { data: course, error } = await fetchCourseData(courseId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error when cloning', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!course) {
|
||||||
|
throw new Error('Course not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Clone course and group
|
||||||
|
const { newCourse } = await cloneGroupAndBasicCourse(
|
||||||
|
course,
|
||||||
|
newTitle,
|
||||||
|
userId,
|
||||||
|
newDescription,
|
||||||
|
newSlug,
|
||||||
|
organizationId
|
||||||
|
);
|
||||||
|
|
||||||
|
let sectionIdMap: Map<string, string> | undefined = undefined;
|
||||||
|
let allLessons: Lesson[] = [];
|
||||||
|
|
||||||
|
// 2. Clone sections if this is a V2 course with sections
|
||||||
|
// @ts-ignore - lesson_section comes from the query
|
||||||
|
if (course.lesson_section && course.lesson_section.length > 0) {
|
||||||
|
// @ts-ignore
|
||||||
|
const { sectionIdMap: map } = await cloneLessonSections(course.lesson_section, newCourse.id);
|
||||||
|
sectionIdMap = map;
|
||||||
|
|
||||||
|
// Collect all lessons from sections
|
||||||
|
// @ts-ignore
|
||||||
|
course.lesson_section.forEach((section: any) => {
|
||||||
|
if (section.lessons && section.lessons.length > 0) {
|
||||||
|
allLessons = [...allLessons, ...section.lessons];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Also add lessons that are not in sections (V1 courses or orphaned lessons)
|
||||||
|
// @ts-ignore - lessons comes from the query
|
||||||
|
if (course.lessons && course.lessons.length > 0) {
|
||||||
|
// @ts-ignore
|
||||||
|
const orphanedLessons = course.lessons.filter(
|
||||||
|
(lesson: Lesson) => !lesson.section_id || !sectionIdMap
|
||||||
|
);
|
||||||
|
allLessons = [...allLessons, ...orphanedLessons];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Clone lessons
|
||||||
|
const { newLessons } = await cloneLessons(allLessons, newCourse.id, sectionIdMap);
|
||||||
|
|
||||||
|
// 5. Clone lesson languages
|
||||||
|
await cloneLessonLanguages(newLessons || []);
|
||||||
|
|
||||||
|
// 6. Clone exercises, questions, and options
|
||||||
|
await cloneExercises(newLessons || []);
|
||||||
|
|
||||||
|
return newCourse;
|
||||||
|
}
|
||||||
@@ -48,3 +48,13 @@ export const ZCourseDownloadPresignedUrl = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type TCourseDownloadPresignedUrl = z.infer<typeof ZCourseDownloadPresignedUrl>;
|
export type TCourseDownloadPresignedUrl = z.infer<typeof ZCourseDownloadPresignedUrl>;
|
||||||
|
|
||||||
|
export const ZCourseClone = z.object({
|
||||||
|
id: z.string().min(1, 'Course ID is required'),
|
||||||
|
title: z.string().min(1, 'Course title is required'),
|
||||||
|
description: z.string().optional(),
|
||||||
|
slug: z.string().min(1, 'Course slug is required'),
|
||||||
|
organizationId: z.string().min(1, 'Organization ID is required')
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCourseClone = z.infer<typeof ZCourseClone>;
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// Database types for course cloning
|
||||||
|
|
||||||
|
export interface Course {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
version: 'V1' | 'V2';
|
||||||
|
overview?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
group_id?: string;
|
||||||
|
is_template?: boolean;
|
||||||
|
organization_id?: string;
|
||||||
|
logo?: string;
|
||||||
|
slug?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
cost?: number;
|
||||||
|
currency?: string;
|
||||||
|
is_certificate_downloadable?: boolean;
|
||||||
|
certificate_theme?: string;
|
||||||
|
status?: string;
|
||||||
|
is_published?: boolean;
|
||||||
|
lesson_section?: LessonSection[];
|
||||||
|
lessons?: Lesson[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LessonSection {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
order: number;
|
||||||
|
course_id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at?: string;
|
||||||
|
lessons?: Lesson[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Lesson {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
note?: string;
|
||||||
|
videos?: unknown[];
|
||||||
|
slide_url?: string;
|
||||||
|
course_id: string;
|
||||||
|
section_id?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at?: string;
|
||||||
|
public?: boolean;
|
||||||
|
lesson_at?: string;
|
||||||
|
teacher_id?: string;
|
||||||
|
is_unlocked?: boolean;
|
||||||
|
call_url?: string;
|
||||||
|
order?: number;
|
||||||
|
exercise?: Exercise[];
|
||||||
|
lesson_language?: LessonLanguage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LessonLanguage {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
lesson_id: string;
|
||||||
|
locale: 'en' | 'hi' | 'fr' | 'pt' | 'de' | 'vi' | 'ru' | 'es' | 'pl' | 'da';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Exercise {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
lesson_id?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
due_by?: string;
|
||||||
|
questions?: Question[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Question {
|
||||||
|
id: number;
|
||||||
|
question_type_id: number;
|
||||||
|
title: string;
|
||||||
|
name?: string;
|
||||||
|
points?: number;
|
||||||
|
order?: number;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
exercise_id: string;
|
||||||
|
question_type?: QuestionType;
|
||||||
|
options?: Option[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionType {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
typename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
is_correct: boolean;
|
||||||
|
question_id: number;
|
||||||
|
value?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Group {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
organization_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupMember {
|
||||||
|
id: string;
|
||||||
|
group_id: string;
|
||||||
|
role_id: number;
|
||||||
|
profile_id?: string;
|
||||||
|
email?: string;
|
||||||
|
created_at?: string;
|
||||||
|
assigned_student_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Profile {
|
||||||
|
id: string;
|
||||||
|
fullname: string;
|
||||||
|
username: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
email?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response types
|
||||||
|
export interface CloneCourseResponse {
|
||||||
|
success: boolean;
|
||||||
|
course: Course;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CloneCourseErrorResponse {
|
||||||
|
error: string;
|
||||||
|
details?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Re-export all types
|
||||||
|
export * from './course';
|
||||||
|
export * from './database';
|
||||||
|
export * from './mail';
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ import { SupabaseClient, createClient } from '@supabase/supabase-js';
|
|||||||
|
|
||||||
import { env } from '$src/config/env';
|
import { env } from '$src/config/env';
|
||||||
|
|
||||||
const { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } = env;
|
const { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL, PRIVATE_SUPABASE_SERVICE_ROLE } = env;
|
||||||
|
|
||||||
export let supabase: SupabaseClient | null = null;
|
export let supabase: SupabaseClient | null = null;
|
||||||
|
|
||||||
@@ -16,15 +16,9 @@ interface SupabaseConfig {
|
|||||||
export function getSupabase(config?: SupabaseConfig): SupabaseClient {
|
export function getSupabase(config?: SupabaseConfig): SupabaseClient {
|
||||||
if (supabase) return supabase;
|
if (supabase) return supabase;
|
||||||
|
|
||||||
const finalConfig = config ?? {
|
|
||||||
supabaseConfig: {
|
|
||||||
url: PUBLIC_SUPABASE_URL,
|
|
||||||
anonKey: PUBLIC_SUPABASE_ANON_KEY
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
supabase = createClient(finalConfig.supabaseConfig.url, finalConfig.supabaseConfig.anonKey, {
|
// Use service role key to bypass RLS policies
|
||||||
|
supabase = createClient(PUBLIC_SUPABASE_URL, PRIVATE_SUPABASE_SERVICE_ROLE, {
|
||||||
auth: { persistSession: false }
|
auth: { persistSession: false }
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -34,3 +28,4 @@ export function getSupabase(config?: SupabaseConfig): SupabaseClient {
|
|||||||
|
|
||||||
return supabase;
|
return supabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import GrowthIcon from 'carbon-icons-svelte/lib/Growth.svelte';
|
import GrowthIcon from 'carbon-icons-svelte/lib/Growth.svelte';
|
||||||
import RadioButtonChecked from 'carbon-icons-svelte/lib/RadioButtonChecked.svelte';
|
import RadioButtonChecked from 'carbon-icons-svelte/lib/RadioButtonChecked.svelte';
|
||||||
import UserProfileIcon from 'carbon-icons-svelte/lib/UserProfile.svelte';
|
import UserProfileIcon from 'carbon-icons-svelte/lib/UserProfile.svelte';
|
||||||
|
import { copyCourseModal, deleteCourseModal } from '$lib/components/Courses/store';
|
||||||
export let bannerImage: string | undefined;
|
export let bannerImage: string | undefined;
|
||||||
export let id = '';
|
export let id = '';
|
||||||
export let slug = '';
|
export let slug = '';
|
||||||
@@ -43,8 +43,10 @@
|
|||||||
$: formatter = getCurrencyFormatter(currency);
|
$: formatter = getCurrencyFormatter(currency);
|
||||||
|
|
||||||
function handleCloneCourse() {
|
function handleCloneCourse() {
|
||||||
// TODO: Clone course functionality
|
$copyCourseModal.open = true;
|
||||||
alert('WIP: Clone course');
|
$copyCourseModal.id = id;
|
||||||
|
$copyCourseModal.title = title;
|
||||||
|
$copyCourseModal.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShareCourse() {
|
function handleShareCourse() {
|
||||||
@@ -56,7 +58,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteCourse() {
|
function handleDeleteCourse() {
|
||||||
goto(`/courses/${id}/settings#delete`);
|
$deleteCourseModal.open = true;
|
||||||
|
$deleteCourseModal.id = id;
|
||||||
|
$deleteCourseModal.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COURSE_TAG: Record<
|
const COURSE_TAG: Record<
|
||||||
@@ -122,7 +126,10 @@
|
|||||||
>
|
>
|
||||||
<OverflowMenuItem
|
<OverflowMenuItem
|
||||||
text={$t('courses.course_card.context_menu.clone')}
|
text={$t('courses.course_card.context_menu.clone')}
|
||||||
on:click={handleCloneCourse}
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCloneCourse();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<OverflowMenuItem
|
<OverflowMenuItem
|
||||||
text={$t('courses.course_card.context_menu.share')}
|
text={$t('courses.course_card.context_menu.share')}
|
||||||
@@ -135,7 +142,10 @@
|
|||||||
<OverflowMenuItem
|
<OverflowMenuItem
|
||||||
danger
|
danger
|
||||||
text={$t('courses.course_card.context_menu.delete')}
|
text={$t('courses.course_card.context_menu.delete')}
|
||||||
on:click={handleDeleteCourse}
|
on:click={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteCourse();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,23 +1,56 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import Modal from '$lib/components/Modal/index.svelte';
|
import Modal from '$lib/components/Modal/index.svelte';
|
||||||
import { snackbar } from '$lib/components/Snackbar/store';
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
import TextField from '$lib/components/Form/TextField.svelte';
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
|
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||||
import { copyCourseModalInitialState, copyCourseModal } from '$lib/components/Courses/store';
|
import { copyCourseModalInitialState, copyCourseModal } from '$lib/components/Courses/store';
|
||||||
import { cloneCourse } from '$lib/utils/services/courses/clone';
|
import { classroomio } from '$lib/utils/services/api';
|
||||||
|
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
|
||||||
|
interface CloneCourseResponse {
|
||||||
|
success: boolean;
|
||||||
|
course: { id: string };
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// clone, show spinner and redirect to new course
|
// clone, show spinner and redirect to new course
|
||||||
async function createCourse() {
|
async function createCourse() {
|
||||||
|
// Prevent double submission (Option 1: Early Return Guard)
|
||||||
|
if ($copyCourseModal.isSaving) return;
|
||||||
|
|
||||||
console.log('Create course');
|
console.log('Create course');
|
||||||
$copyCourseModal.isSaving = true;
|
$copyCourseModal.isSaving = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newCourse = await cloneCourse($copyCourseModal.id, $copyCourseModal.title);
|
// Generate unique slug from the title
|
||||||
|
const slug = generateSlug($copyCourseModal.title);
|
||||||
|
|
||||||
goto(`/courses/${newCourse.id}`);
|
const response = await classroomio.course.clone.$post({
|
||||||
|
json: {
|
||||||
|
id: $copyCourseModal.id,
|
||||||
|
title: $copyCourseModal.title,
|
||||||
|
description: $copyCourseModal.description,
|
||||||
|
slug,
|
||||||
|
organizationId: $currentOrg.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = (await response.json()) as CloneCourseResponse;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.error || $t('courses.copy_course.error.failed_to_clone'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 3: Close modal and reset state before navigation
|
||||||
copyCourseModal.set(copyCourseModalInitialState);
|
copyCourseModal.set(copyCourseModalInitialState);
|
||||||
|
|
||||||
|
// Navigate to the new course
|
||||||
|
goto(`/courses/${result.course.id}`);
|
||||||
|
$copyCourseModal.open = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
@@ -36,25 +69,34 @@
|
|||||||
onClose={() => ($copyCourseModal.open = false)}
|
onClose={() => ($copyCourseModal.open = false)}
|
||||||
bind:open={$copyCourseModal.open}
|
bind:open={$copyCourseModal.open}
|
||||||
width="w-96"
|
width="w-96"
|
||||||
modalHeading="Create a Course"
|
modalHeading={$t('courses.copy_course.title')}
|
||||||
>
|
>
|
||||||
<form on:submit|preventDefault={createCourse}>
|
<form on:submit|preventDefault={createCourse}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Course name"
|
label={$t('courses.copy_course.course_name_label')}
|
||||||
bind:value={$copyCourseModal.title}
|
bind:value={$copyCourseModal.title}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
placeholder="Your course name"
|
placeholder={$t('courses.copy_course.course_name_placeholder')}
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
isRequired={true}
|
isRequired={true}
|
||||||
autoComplete={false}
|
autoComplete={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="mt-5 flex items-center flex-row-reverse">
|
<TextArea
|
||||||
|
label={$t('courses.copy_course.course_description_label')}
|
||||||
|
bind:value={$copyCourseModal.description}
|
||||||
|
placeholder={$t('courses.copy_course.course_description_placeholder')}
|
||||||
|
className="mb-4"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-5 flex flex-row-reverse items-center">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="px-6 py-3"
|
className="px-6 py-3"
|
||||||
label="Create"
|
label={$t('courses.copy_course.create_button')}
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={$copyCourseModal.isSaving}
|
isLoading={$copyCourseModal.isSaving}
|
||||||
|
isDisabled={$copyCourseModal.isSaving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
StructuredListBody
|
StructuredListBody
|
||||||
} from 'carbon-components-svelte';
|
} from 'carbon-components-svelte';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import CopyCourseModal from './components/CopyCourseModal/index.svelte';
|
||||||
|
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
|
||||||
|
import { deleteCourse } from '$lib/utils/services/courses';
|
||||||
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
|
import { deleteCourseModal, deleteCourseModalInitialState, courses as coursesStore } from './store';
|
||||||
|
|
||||||
export let courses: Course[] = [];
|
export let courses: Course[] = [];
|
||||||
export let emptyTitle = $t('courses.course_card.empty_title');
|
export let emptyTitle = $t('courses.course_card.empty_title');
|
||||||
@@ -28,9 +33,39 @@
|
|||||||
|
|
||||||
return Math.round((progressRate / totalLessons) * 100);
|
return Math.round((progressRate / totalLessons) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteCourse() {
|
||||||
|
if (!$deleteCourseModal.id) return;
|
||||||
|
|
||||||
|
$deleteCourseModal.isDeleting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteCourse($deleteCourseModal.id);
|
||||||
|
|
||||||
|
// Remove the course from the courses store
|
||||||
|
$coursesStore = $coursesStore.filter(course => course.id !== $deleteCourseModal.id);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
snackbar.success('snackbar.course_deleted');
|
||||||
|
|
||||||
|
// Close modal and reset state
|
||||||
|
deleteCourseModal.set(deleteCourseModalInitialState);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting course:', error);
|
||||||
|
snackbar.error('snackbar.course_settings.error.went_wrong');
|
||||||
|
|
||||||
|
// Stop deleting state on error
|
||||||
|
$deleteCourseModal.isDeleting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- <CopyCourseModal /> -->
|
<CopyCourseModal />
|
||||||
|
<DeleteModal
|
||||||
|
onDelete={handleDeleteCourse}
|
||||||
|
bind:open={$deleteCourseModal.open}
|
||||||
|
isLoading={$deleteCourseModal.isDeleting}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="mx-auto my-4 w-full">
|
<div class="mx-auto my-4 w-full">
|
||||||
{#if $courseMetaDeta.isLoading}
|
{#if $courseMetaDeta.isLoading}
|
||||||
|
|||||||
@@ -40,8 +40,18 @@ export const copyCourseModalInitialState = {
|
|||||||
courseIdToCopy: null,
|
courseIdToCopy: null,
|
||||||
id: '',
|
id: '',
|
||||||
title: '',
|
title: '',
|
||||||
|
description: '',
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
error: null
|
error: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyCourseModal = writable({ ...copyCourseModalInitialState });
|
export const copyCourseModal = writable({ ...copyCourseModalInitialState });
|
||||||
|
|
||||||
|
export const deleteCourseModalInitialState = {
|
||||||
|
open: false,
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
isDeleting: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteCourseModal = writable({ ...deleteCourseModalInitialState });
|
||||||
|
|||||||
@@ -6,15 +6,11 @@
|
|||||||
|
|
||||||
export let open = false;
|
export let open = false;
|
||||||
export let onDelete = () => {};
|
export let onDelete = () => {};
|
||||||
|
export let isLoading = false;
|
||||||
function handleDelete() {
|
|
||||||
onDelete();
|
|
||||||
open = false;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
onClose={() => (open = false)}
|
onClose={() => !isLoading && (open = false)}
|
||||||
bind:open
|
bind:open
|
||||||
width="w-96"
|
width="w-96"
|
||||||
containerClass="p-6"
|
containerClass="p-6"
|
||||||
@@ -32,12 +28,14 @@
|
|||||||
variant={VARIANTS.OUTLINED}
|
variant={VARIANTS.OUTLINED}
|
||||||
label={$t('delete_modal.no')}
|
label={$t('delete_modal.no')}
|
||||||
onClick={() => (open = false)}
|
onClick={() => (open = false)}
|
||||||
|
{isLoading}
|
||||||
/>
|
/>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="px-6 py-3"
|
className="px-6 py-3"
|
||||||
variant={VARIANTS.CONTAINED_DANGER}
|
variant={VARIANTS.CONTAINED_DANGER}
|
||||||
label={$t('delete_modal.yes')}
|
label={$t('delete_modal.yes')}
|
||||||
onClick={handleDelete}
|
onClick={onDelete}
|
||||||
|
{isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
import type { Course, Lesson, Profile } from '$lib/utils/types';
|
|
||||||
|
|
||||||
import type { PostgrestError } from '@supabase/supabase-js';
|
|
||||||
// @ts-ignore
|
|
||||||
import { QUESTION_TYPE } from '$lib/components/Question/constants';
|
|
||||||
// @ts-ignore
|
|
||||||
import { ROLE } from '$lib/utils/constants/roles';
|
|
||||||
// @ts-ignore
|
|
||||||
import { addGroupMember } from './index';
|
|
||||||
import { get } from 'svelte/store';
|
|
||||||
// @ts-ignore
|
|
||||||
import { profile } from '$lib/utils/store/user';
|
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
|
||||||
|
|
||||||
type GetCourseResponse = {
|
|
||||||
data: Course;
|
|
||||||
error: PostgrestError | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function fetchCourseData(courseId: string): Promise<GetCourseResponse> {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('course')
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
*,
|
|
||||||
lessons:lesson(
|
|
||||||
*,
|
|
||||||
exercise:id(
|
|
||||||
id, title, description, due_by,
|
|
||||||
questions:question(
|
|
||||||
*,
|
|
||||||
options:option(*),
|
|
||||||
question_type:question_type_id(id, label)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.match({ id: courseId })
|
|
||||||
.single();
|
|
||||||
|
|
||||||
return { data, error };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cloneGroupAndBasicCourse(
|
|
||||||
course: Course,
|
|
||||||
newTitle: string
|
|
||||||
): Promise<{ newCourse: Course }> {
|
|
||||||
const { description } = course;
|
|
||||||
|
|
||||||
const { data: newGroup } = await supabase
|
|
||||||
.from('group')
|
|
||||||
.insert({ name: newTitle, description })
|
|
||||||
.select();
|
|
||||||
|
|
||||||
console.log(`newGroup`, newGroup);
|
|
||||||
if (!Array.isArray(newGroup)) {
|
|
||||||
throw new Error('Group wasnt created successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id: group_id } = newGroup[0];
|
|
||||||
|
|
||||||
// 2. Create course with group_id
|
|
||||||
const { data: newCourse } = await supabase
|
|
||||||
.from('course')
|
|
||||||
.insert({
|
|
||||||
...course,
|
|
||||||
title: newTitle,
|
|
||||||
description,
|
|
||||||
group_id,
|
|
||||||
lessons: undefined,
|
|
||||||
id: undefined,
|
|
||||||
updated_at: undefined,
|
|
||||||
created_at: undefined
|
|
||||||
})
|
|
||||||
.select();
|
|
||||||
|
|
||||||
console.log(`newCourse`, newCourse);
|
|
||||||
|
|
||||||
// 3. Add group members
|
|
||||||
|
|
||||||
const profileStoreValue: Profile = get(profile);
|
|
||||||
|
|
||||||
// Get profile of author and add as member
|
|
||||||
await addGroupMember({
|
|
||||||
profile_id: profileStoreValue.id,
|
|
||||||
email: profileStoreValue.email,
|
|
||||||
group_id,
|
|
||||||
role_id: ROLE.TUTOR
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('profileStoreValue', profileStoreValue);
|
|
||||||
|
|
||||||
return {
|
|
||||||
newCourse: newCourse?.[0]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cloneLessons(lessons: Lesson[], courseId: Course['id']) {
|
|
||||||
const clonedLessons = lessons
|
|
||||||
.sort((a, b) => new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime())
|
|
||||||
.map((lesson, index) => ({
|
|
||||||
call_url: lesson.call_url,
|
|
||||||
course_id: courseId,
|
|
||||||
is_unlocked: lesson.is_unlocked,
|
|
||||||
lesson_at: new Date(),
|
|
||||||
note: lesson.note,
|
|
||||||
order: lesson.order || index + 1,
|
|
||||||
public: lesson.public,
|
|
||||||
slide_url: lesson.slide_url,
|
|
||||||
title: lesson.title,
|
|
||||||
videos: lesson.videos
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { data } = await supabase.from('lesson').insert(clonedLessons).select();
|
|
||||||
|
|
||||||
const newLessons = data?.map((dataItem, dataIndex) => {
|
|
||||||
return {
|
|
||||||
...dataItem,
|
|
||||||
// @ts-ignore
|
|
||||||
exercise: lessons[dataIndex].exercise
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('newLessons', newLessons);
|
|
||||||
return { newLessons };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cloneExercises(lessons: Lesson[]) {
|
|
||||||
console.log('lessons', lessons);
|
|
||||||
|
|
||||||
// Lessons
|
|
||||||
for (const lesson of lessons) {
|
|
||||||
// @ts-ignore
|
|
||||||
const { id: lessonId, exercise = [] } = lesson;
|
|
||||||
|
|
||||||
// Exercises
|
|
||||||
for (const exerciseItem of exercise) {
|
|
||||||
const { title, description, questions = [] } = exerciseItem;
|
|
||||||
// Insert
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('exercise')
|
|
||||||
.insert({
|
|
||||||
description,
|
|
||||||
due_by: new Date(),
|
|
||||||
lesson_id: lessonId,
|
|
||||||
title
|
|
||||||
})
|
|
||||||
.select();
|
|
||||||
const newExercise = Array.isArray(data) ? data[0] : data;
|
|
||||||
console.log('newExercise', newExercise);
|
|
||||||
|
|
||||||
// Questions
|
|
||||||
for (const question of questions) {
|
|
||||||
const { options = [] } = question;
|
|
||||||
|
|
||||||
// Insert
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('question')
|
|
||||||
.insert({
|
|
||||||
name: question.name,
|
|
||||||
title: question.title,
|
|
||||||
points: question.points,
|
|
||||||
order: question.order,
|
|
||||||
question_type_id: question.question_type_id,
|
|
||||||
exercise_id: newExercise.id
|
|
||||||
})
|
|
||||||
.select();
|
|
||||||
const newQuestion = Array.isArray(data) ? data[0] : data;
|
|
||||||
console.log('newQuestion', newQuestion);
|
|
||||||
|
|
||||||
// Options: Don't map options for 'Paragraph' questions
|
|
||||||
if (QUESTION_TYPE.TEXTAREA !== newQuestion.question_type_id) {
|
|
||||||
// @ts-ignore
|
|
||||||
const newOptions = options.map((option) => ({
|
|
||||||
value: option.value,
|
|
||||||
label: option.label,
|
|
||||||
is_correct: option.is_correct,
|
|
||||||
question_id: newQuestion.id
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Insert[Batch]
|
|
||||||
await supabase.from('option').insert(newOptions).select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cloneCourse(courseId: string, newTitle: string): Promise<Course> {
|
|
||||||
const { data: course, error } = await fetchCourseData(courseId);
|
|
||||||
console.log(`course`, course);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error when cloning', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1. Copy course table
|
|
||||||
* 2. Copy lessons table (NOTE: Copy sections table here)
|
|
||||||
* 3. Copy exercises table
|
|
||||||
* 4. Copy questions table
|
|
||||||
* 5. Copy options table
|
|
||||||
*/
|
|
||||||
|
|
||||||
// tables: group, groupmember, course(group_id)
|
|
||||||
const { newCourse } = await cloneGroupAndBasicCourse(course, newTitle);
|
|
||||||
console.log('returned -> newCourse', newCourse);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const { newLessons } = await cloneLessons(course.lessons, newCourse.id);
|
|
||||||
console.log('returned -> newLessons', newLessons);
|
|
||||||
|
|
||||||
const cloneExercisesRes = await cloneExercises(newLessons || []);
|
|
||||||
console.log('returned -> cloneExercisesRes', cloneExercisesRes);
|
|
||||||
|
|
||||||
return newCourse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
1. Click on 3 dots to clone course
|
|
||||||
|
|
||||||
2. Show a modal with a new name "Copy of previous course name"
|
|
||||||
|
|
||||||
-> Note the following information will be copied
|
|
||||||
|
|
||||||
3. Things to copy
|
|
||||||
Lessons
|
|
||||||
Exercises - questions, options, everything basically.
|
|
||||||
|
|
||||||
Technical approach
|
|
||||||
- Fetch all the data to the client
|
|
||||||
- Manually call functions to create
|
|
||||||
|
|
||||||
- call a psql function with newname, course_id
|
|
||||||
|
|
||||||
- copy all in course table
|
|
||||||
- copy all in group table by course_id
|
|
||||||
- add only one group member as admin
|
|
||||||
|
|
||||||
- get all lessons in course
|
|
||||||
- copy each one with unique ids
|
|
||||||
|
|
||||||
- get all exercises by course id
|
|
||||||
- copy exercise
|
|
||||||
|
|
||||||
- get questions
|
|
||||||
- copy questions
|
|
||||||
|
|
||||||
- get options
|
|
||||||
- copy options
|
|
||||||
*/
|
|
||||||
@@ -1090,6 +1090,17 @@
|
|||||||
"date_created": "Dato oprettet",
|
"date_created": "Dato oprettet",
|
||||||
"published": "Offentliggjort",
|
"published": "Offentliggjort",
|
||||||
"lessons": "Lektioner"
|
"lessons": "Lektioner"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Opret et kursus",
|
||||||
|
"course_name_label": "Kursusnavn",
|
||||||
|
"course_name_placeholder": "Dit kursusnavn",
|
||||||
|
"course_description_label": "Kursusbeskrivelse",
|
||||||
|
"course_description_placeholder": "Indtast en beskrivelse til dit kursus",
|
||||||
|
"create_button": "Skabe",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "Kunne ikke klone kursus"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -937,6 +937,17 @@
|
|||||||
"date_created": "Datum erstellt",
|
"date_created": "Datum erstellt",
|
||||||
"published": "Veröffentlicht",
|
"published": "Veröffentlicht",
|
||||||
"lessons": "Unterricht"
|
"lessons": "Unterricht"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Erstellen Sie einen Kurs",
|
||||||
|
"course_name_label": "Kursname",
|
||||||
|
"course_name_placeholder": "Ihr Kursname",
|
||||||
|
"course_description_label": "Kursbeschreibung",
|
||||||
|
"course_description_placeholder": "Geben Sie eine Beschreibung für Ihren Kurs ein",
|
||||||
|
"create_button": "Erstellen",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "Der Kurs konnte nicht geklont werden"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -540,6 +540,17 @@
|
|||||||
"button": "Finish",
|
"button": "Finish",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"next": "Next"
|
"next": "Next"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Create a Course",
|
||||||
|
"course_name_label": "Course name",
|
||||||
|
"course_name_placeholder": "Your course name",
|
||||||
|
"course_description_label": "Course description",
|
||||||
|
"course_description_placeholder": "Enter a description for your course",
|
||||||
|
"create_button": "Create",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "Failed to clone course"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -937,6 +937,17 @@
|
|||||||
"date_created": "fecha de creacion",
|
"date_created": "fecha de creacion",
|
||||||
"published": "Publicado",
|
"published": "Publicado",
|
||||||
"lessons": "Lecciones"
|
"lessons": "Lecciones"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Crear un curso",
|
||||||
|
"course_name_label": "Nombre del curso",
|
||||||
|
"course_name_placeholder": "Nombre de tu curso",
|
||||||
|
"course_description_label": "Descripción del curso",
|
||||||
|
"course_description_placeholder": "Introduce una descripción para tu curso.",
|
||||||
|
"create_button": "Crear",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "No se pudo clonar el curso"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -937,6 +937,17 @@
|
|||||||
"date_created": "date créée",
|
"date_created": "date créée",
|
||||||
"published": "Publié",
|
"published": "Publié",
|
||||||
"lessons": "Cours"
|
"lessons": "Cours"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Créer un cours",
|
||||||
|
"course_name_label": "Nom du cours",
|
||||||
|
"course_name_placeholder": "Le nom de votre cours",
|
||||||
|
"course_description_label": "Descriptif du cours",
|
||||||
|
"course_description_placeholder": "Entrez une description pour votre cours",
|
||||||
|
"create_button": "Créer",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "Échec du clonage du cours"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -937,6 +937,17 @@
|
|||||||
"date_created": "बनाया गया दिनांक",
|
"date_created": "बनाया गया दिनांक",
|
||||||
"published": "प्रकाशित",
|
"published": "प्रकाशित",
|
||||||
"lessons": "पाठ"
|
"lessons": "पाठ"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "एक कोर्स बनाएं",
|
||||||
|
"course_name_label": "कोर्स का नाम",
|
||||||
|
"course_name_placeholder": "आपके पाठ्यक्रम का नाम",
|
||||||
|
"course_description_label": "पाठ्यक्रम विवरण",
|
||||||
|
"course_description_placeholder": "अपने पाठ्यक्रम के लिए विवरण दर्ज करें",
|
||||||
|
"create_button": "बनाएं",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "पाठ्यक्रम क्लोन करने में विफल"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -1086,6 +1086,17 @@
|
|||||||
"date_created": "Utworzona data",
|
"date_created": "Utworzona data",
|
||||||
"published": "Opublikowany",
|
"published": "Opublikowany",
|
||||||
"lessons": "Lekcje"
|
"lessons": "Lekcje"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Utwórz kurs",
|
||||||
|
"course_name_label": "Nazwa kursu",
|
||||||
|
"course_name_placeholder": "Nazwa Twojego kursu",
|
||||||
|
"course_description_label": "Opis kursu",
|
||||||
|
"course_description_placeholder": "Wprowadź opis swojego kursu",
|
||||||
|
"create_button": "Tworzyć",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "Nie udało się sklonować kursu"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -937,6 +937,17 @@
|
|||||||
"date_created": "Data Criada",
|
"date_created": "Data Criada",
|
||||||
"published": "Publicados",
|
"published": "Publicados",
|
||||||
"lessons": "Lições"
|
"lessons": "Lições"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Crie um curso",
|
||||||
|
"course_name_label": "Nome do curso",
|
||||||
|
"course_name_placeholder": "Nome do seu curso",
|
||||||
|
"course_description_label": "Descrição do curso",
|
||||||
|
"course_description_placeholder": "Insira uma descrição para o seu curso",
|
||||||
|
"create_button": "Criar",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "Falha ao clonar curso"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -937,6 +937,17 @@
|
|||||||
"date_created": "Дата создания",
|
"date_created": "Дата создания",
|
||||||
"published": "Опубликовано",
|
"published": "Опубликовано",
|
||||||
"lessons": "Уроки"
|
"lessons": "Уроки"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Создать курс",
|
||||||
|
"course_name_label": "Название курса",
|
||||||
|
"course_name_placeholder": "Название вашего курса",
|
||||||
|
"course_description_label": "Описание курса",
|
||||||
|
"course_description_placeholder": "Введите описание вашего курса",
|
||||||
|
"create_button": "Создавать",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "Не удалось клонировать курс."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
@@ -937,6 +937,17 @@
|
|||||||
"date_created": "Ngày tạo",
|
"date_created": "Ngày tạo",
|
||||||
"published": "Được phát hành",
|
"published": "Được phát hành",
|
||||||
"lessons": "Những bài học"
|
"lessons": "Những bài học"
|
||||||
|
},
|
||||||
|
"copy_course": {
|
||||||
|
"title": "Tạo một khóa học",
|
||||||
|
"course_name_label": "Tên khóa học",
|
||||||
|
"course_name_placeholder": "Tên khóa học của bạn",
|
||||||
|
"course_description_label": "Mô tả khóa học",
|
||||||
|
"course_description_placeholder": "Nhập mô tả cho khóa học của bạn",
|
||||||
|
"create_button": "Tạo nên",
|
||||||
|
"error": {
|
||||||
|
"failed_to_clone": "Không thể sao chép khóa học"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
|
|||||||
Generated
+86
-87
@@ -317,19 +317,19 @@ importers:
|
|||||||
version: 2.2.0
|
version: 2.2.0
|
||||||
'@polar-sh/sveltekit':
|
'@polar-sh/sveltekit':
|
||||||
specifier: ^0.2.1
|
specifier: ^0.2.1
|
||||||
version: 0.2.1(@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))([email protected])
|
version: 0.2.1(@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))([email protected])
|
||||||
'@supabase/supabase-js':
|
'@supabase/supabase-js':
|
||||||
specifier: ^2.31.0
|
specifier: ^2.31.0
|
||||||
version: 2.50.0([email protected])([email protected])
|
version: 2.50.0([email protected])([email protected])
|
||||||
'@sveltejs/adapter-auto':
|
'@sveltejs/adapter-auto':
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.1(@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))
|
version: 2.1.1(@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))
|
||||||
'@sveltejs/adapter-node':
|
'@sveltejs/adapter-node':
|
||||||
specifier: ^1.3.1
|
specifier: ^1.3.1
|
||||||
version: 1.3.1(@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))
|
version: 1.3.1(@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))
|
||||||
'@sveltejs/adapter-vercel':
|
'@sveltejs/adapter-vercel':
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))([email protected])
|
version: 3.1.0(@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))([email protected])
|
||||||
'@sveltekit-i18n/base':
|
'@sveltekit-i18n/base':
|
||||||
specifier: ^1.3.7
|
specifier: ^1.3.7
|
||||||
version: 1.3.7([email protected])
|
version: 1.3.7([email protected])
|
||||||
@@ -338,7 +338,7 @@ importers:
|
|||||||
version: 1.0.8
|
version: 1.0.8
|
||||||
'@tailwindcss/forms':
|
'@tailwindcss/forms':
|
||||||
specifier: ^0.5.4
|
specifier: ^0.5.4
|
||||||
version: 0.5.10([email protected]([email protected](@types/node@24.0.1)([email protected])))
|
version: 0.5.10([email protected]([email protected](@types/node@22.15.31)([email protected])))
|
||||||
'@types/pluralize':
|
'@types/pluralize':
|
||||||
specifier: 0.0.30
|
specifier: 0.0.30
|
||||||
version: 0.0.30
|
version: 0.0.30
|
||||||
@@ -471,10 +471,10 @@ importers:
|
|||||||
version: 7.27.6
|
version: 7.27.6
|
||||||
'@sveltejs/kit':
|
'@sveltejs/kit':
|
||||||
specifier: ^1.29.0
|
specifier: ^1.29.0
|
||||||
version: 1.30.4([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
version: 1.30.4([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.9
|
specifier: ^0.5.9
|
||||||
version: 0.5.16([email protected]([email protected](@types/node@24.0.1)([email protected])))
|
version: 0.5.16([email protected]([email protected](@types/node@22.15.31)([email protected])))
|
||||||
'@testing-library/jest-dom':
|
'@testing-library/jest-dom':
|
||||||
specifier: ^6.1.5
|
specifier: ^6.1.5
|
||||||
version: 6.6.3
|
version: 6.6.3
|
||||||
@@ -513,13 +513,13 @@ importers:
|
|||||||
version: 3.1.1
|
version: 3.1.1
|
||||||
jest:
|
jest:
|
||||||
specifier: ^29.7.0
|
specifier: ^29.7.0
|
||||||
version: 29.7.0(@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected]))
|
version: 29.7.0(@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected]))
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.24
|
specifier: ^8.4.24
|
||||||
version: 8.5.5
|
version: 8.5.5
|
||||||
postcss-load-config:
|
postcss-load-config:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.2([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
version: 4.0.2([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
qrcode:
|
qrcode:
|
||||||
specifier: ^1.5.3
|
specifier: ^1.5.3
|
||||||
version: 1.5.4
|
version: 1.5.4
|
||||||
@@ -534,22 +534,22 @@ importers:
|
|||||||
version: 0.9.61([email protected])
|
version: 0.9.61([email protected])
|
||||||
svelte-jester:
|
svelte-jester:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0([email protected](@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected])))([email protected])
|
version: 3.0.0([email protected](@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected])))([email protected])
|
||||||
svelte-loading-spinners:
|
svelte-loading-spinners:
|
||||||
specifier: ^0.3.4
|
specifier: ^0.3.4
|
||||||
version: 0.3.6
|
version: 0.3.6
|
||||||
svelte-preprocess:
|
svelte-preprocess:
|
||||||
specifier: ^5.0.4
|
specifier: ^5.0.4
|
||||||
version: 5.1.4(@babel/[email protected])([email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))([email protected])([email protected])([email protected])([email protected])
|
version: 5.1.4(@babel/[email protected])([email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))([email protected])([email protected])([email protected])([email protected])
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.4.17([email protected](@types/node@24.0.1)([email protected]))
|
version: 3.4.17([email protected](@types/node@22.15.31)([email protected]))
|
||||||
ts-jest:
|
ts-jest:
|
||||||
specifier: ^29.1.1
|
specifier: ^29.1.1
|
||||||
version: 29.4.0(@babel/[email protected])(@jest/[email protected])(@jest/[email protected])([email protected](@babel/[email protected]))([email protected])([email protected](@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected])))([email protected])
|
version: 29.4.0(@babel/[email protected])(@jest/[email protected])(@jest/[email protected])([email protected](@babel/[email protected]))([email protected])([email protected](@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected])))([email protected])
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@24.0.1)([email protected])
|
version: 10.9.2(@types/node@22.15.31)([email protected])
|
||||||
tsconfig:
|
tsconfig:
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/tsconfig
|
version: link:../../packages/tsconfig
|
||||||
@@ -561,7 +561,7 @@ importers:
|
|||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^4.4.8
|
specifier: ^4.4.8
|
||||||
version: 4.5.14(@types/node@24.0.1)([email protected])
|
version: 4.5.14(@types/node@22.15.31)([email protected])
|
||||||
|
|
||||||
apps/docs:
|
apps/docs:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11308,7 +11308,7 @@ snapshots:
|
|||||||
jest-util: 29.7.0
|
jest-util: 29.7.0
|
||||||
slash: 3.0.0
|
slash: 3.0.0
|
||||||
|
|
||||||
'@jest/[email protected]([email protected](@types/node@24.0.1)([email protected]))':
|
'@jest/[email protected]([email protected](@types/node@22.15.31)([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/console': 29.7.0
|
'@jest/console': 29.7.0
|
||||||
'@jest/reporters': 29.7.0
|
'@jest/reporters': 29.7.0
|
||||||
@@ -11322,7 +11322,7 @@ snapshots:
|
|||||||
exit: 0.1.2
|
exit: 0.1.2
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
jest-changed-files: 29.7.0
|
jest-changed-files: 29.7.0
|
||||||
jest-config: 29.7.0(@types/[email protected])([email protected](@types/node@24.0.1)([email protected]))
|
jest-config: 29.7.0(@types/[email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
jest-haste-map: 29.7.0
|
jest-haste-map: 29.7.0
|
||||||
jest-message-util: 29.7.0
|
jest-message-util: 29.7.0
|
||||||
jest-regex-util: 29.6.3
|
jest-regex-util: 29.6.3
|
||||||
@@ -11711,10 +11711,10 @@ snapshots:
|
|||||||
standardwebhooks: 1.0.0
|
standardwebhooks: 1.0.0
|
||||||
zod: 3.25.64
|
zod: 3.25.64
|
||||||
|
|
||||||
'@polar-sh/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))([email protected])':
|
'@polar-sh/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@polar-sh/sdk': 0.20.2([email protected])
|
'@polar-sh/sdk': 0.20.2([email protected])
|
||||||
'@sveltejs/kit': 1.30.4([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
'@sveltejs/kit': 1.30.4([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
@@ -12341,9 +12341,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
|
|
||||||
'@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))':
|
'@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/kit': 1.30.4([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
'@sveltejs/kit': 1.30.4([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
import-meta-resolve: 4.1.0
|
import-meta-resolve: 4.1.0
|
||||||
|
|
||||||
'@sveltejs/[email protected](@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/[email protected])([email protected])))([email protected])([email protected](@types/[email protected])([email protected])))':
|
'@sveltejs/[email protected](@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/[email protected])([email protected])))([email protected])([email protected](@types/[email protected])([email protected])))':
|
||||||
@@ -12361,26 +12361,26 @@ snapshots:
|
|||||||
'@sveltejs/kit': 2.21.5(@sveltejs/[email protected]([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
|
'@sveltejs/kit': 2.21.5(@sveltejs/[email protected]([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])))([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||||
import-meta-resolve: 4.1.0
|
import-meta-resolve: 4.1.0
|
||||||
|
|
||||||
'@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))':
|
'@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/plugin-commonjs': 25.0.8([email protected])
|
'@rollup/plugin-commonjs': 25.0.8([email protected])
|
||||||
'@rollup/plugin-json': 6.1.0([email protected])
|
'@rollup/plugin-json': 6.1.0([email protected])
|
||||||
'@rollup/plugin-node-resolve': 15.3.1([email protected])
|
'@rollup/plugin-node-resolve': 15.3.1([email protected])
|
||||||
'@sveltejs/kit': 1.30.4([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
'@sveltejs/kit': 1.30.4([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
rollup: 3.29.5
|
rollup: 3.29.5
|
||||||
|
|
||||||
'@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))([email protected])':
|
'@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/kit': 1.30.4([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
'@sveltejs/kit': 1.30.4([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
'@vercel/nft': 0.24.4([email protected])
|
'@vercel/nft': 0.24.4([email protected])
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected]))':
|
'@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/vite-plugin-svelte': 2.5.3([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
'@sveltejs/vite-plugin-svelte': 2.5.3([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
'@types/cookie': 0.5.4
|
'@types/cookie': 0.5.4
|
||||||
cookie: 0.5.0
|
cookie: 0.5.0
|
||||||
devalue: 4.3.3
|
devalue: 4.3.3
|
||||||
@@ -12394,7 +12394,7 @@ snapshots:
|
|||||||
svelte: 4.2.20
|
svelte: 4.2.20
|
||||||
tiny-glob: 0.2.9
|
tiny-glob: 0.2.9
|
||||||
undici: 5.29.0
|
undici: 5.29.0
|
||||||
vite: 4.5.14(@types/node@24.0.1)([email protected])
|
vite: 4.5.14(@types/node@22.15.31)([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -12455,12 +12455,12 @@ snapshots:
|
|||||||
vite: 6.3.5(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
|
vite: 6.3.5(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
|
||||||
vitefu: 1.0.6([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
|
vitefu: 1.0.6([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||||
|
|
||||||
'@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))([email protected])([email protected](@types/node@24.0.1)([email protected]))':
|
'@sveltejs/[email protected](@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))([email protected])([email protected](@types/node@22.15.31)([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/vite-plugin-svelte': 2.5.3([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
'@sveltejs/vite-plugin-svelte': 2.5.3([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
svelte: 4.2.20
|
svelte: 4.2.20
|
||||||
vite: 4.5.14(@types/node@24.0.1)([email protected])
|
vite: 4.5.14(@types/node@22.15.31)([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -12491,17 +12491,17 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected]))':
|
'@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/[email protected]([email protected])([email protected](@types/node@24.0.1)([email protected])))([email protected])([email protected](@types/node@24.0.1)([email protected]))
|
'@sveltejs/vite-plugin-svelte-inspector': 1.0.4(@sveltejs/[email protected]([email protected])([email protected](@types/node@22.15.31)([email protected])))([email protected])([email protected](@types/node@22.15.31)([email protected]))
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
deepmerge: 4.3.1
|
deepmerge: 4.3.1
|
||||||
kleur: 4.1.5
|
kleur: 4.1.5
|
||||||
magic-string: 0.30.17
|
magic-string: 0.30.17
|
||||||
svelte: 4.2.20
|
svelte: 4.2.20
|
||||||
svelte-hmr: 0.15.3([email protected])
|
svelte-hmr: 0.15.3([email protected])
|
||||||
vite: 4.5.14(@types/node@24.0.1)([email protected])
|
vite: 4.5.14(@types/node@22.15.31)([email protected])
|
||||||
vitefu: 0.2.5([email protected](@types/node@24.0.1)([email protected]))
|
vitefu: 0.2.5([email protected](@types/node@22.15.31)([email protected]))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -12567,11 +12567,24 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 3.4.17([email protected](@types/[email protected])([email protected]))
|
tailwindcss: 3.4.17([email protected](@types/[email protected])([email protected]))
|
||||||
|
|
||||||
|
'@tailwindcss/[email protected]([email protected]([email protected](@types/[email protected])([email protected])))':
|
||||||
|
dependencies:
|
||||||
|
mini-svg-data-uri: 1.4.4
|
||||||
|
tailwindcss: 3.4.17([email protected](@types/[email protected])([email protected]))
|
||||||
|
|
||||||
'@tailwindcss/[email protected]([email protected]([email protected](@types/[email protected])([email protected])))':
|
'@tailwindcss/[email protected]([email protected]([email protected](@types/[email protected])([email protected])))':
|
||||||
dependencies:
|
dependencies:
|
||||||
mini-svg-data-uri: 1.4.4
|
mini-svg-data-uri: 1.4.4
|
||||||
tailwindcss: 3.4.17([email protected](@types/[email protected])([email protected]))
|
tailwindcss: 3.4.17([email protected](@types/[email protected])([email protected]))
|
||||||
|
|
||||||
|
'@tailwindcss/[email protected]([email protected]([email protected](@types/[email protected])([email protected])))':
|
||||||
|
dependencies:
|
||||||
|
lodash.castarray: 4.4.0
|
||||||
|
lodash.isplainobject: 4.0.6
|
||||||
|
lodash.merge: 4.6.2
|
||||||
|
postcss-selector-parser: 6.0.10
|
||||||
|
tailwindcss: 3.4.17([email protected](@types/[email protected])([email protected]))
|
||||||
|
|
||||||
'@tailwindcss/[email protected]([email protected]([email protected](@types/[email protected])([email protected])))':
|
'@tailwindcss/[email protected]([email protected]([email protected](@types/[email protected])([email protected])))':
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash.castarray: 4.4.0
|
lodash.castarray: 4.4.0
|
||||||
@@ -12895,6 +12908,7 @@ snapshots:
|
|||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.8.0
|
undici-types: 7.8.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -13978,13 +13992,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
layout-base: 1.0.2
|
layout-base: 1.0.2
|
||||||
|
|
||||||
[email protected](@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected])):
|
[email protected](@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
exit: 0.1.2
|
exit: 0.1.2
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
jest-config: 29.7.0(@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected]))
|
jest-config: 29.7.0(@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected]))
|
||||||
jest-util: 29.7.0
|
jest-util: 29.7.0
|
||||||
prompts: 2.4.2
|
prompts: 2.4.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -15698,16 +15712,16 @@ snapshots:
|
|||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected](@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected])):
|
[email protected](@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/core': 29.7.0([email protected](@types/node@24.0.1)([email protected]))
|
'@jest/core': 29.7.0([email protected](@types/node@22.15.31)([email protected]))
|
||||||
'@jest/test-result': 29.7.0
|
'@jest/test-result': 29.7.0
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
create-jest: 29.7.0(@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected]))
|
create-jest: 29.7.0(@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected]))
|
||||||
exit: 0.1.2
|
exit: 0.1.2
|
||||||
import-local: 3.2.0
|
import-local: 3.2.0
|
||||||
jest-config: 29.7.0(@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected]))
|
jest-config: 29.7.0(@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected]))
|
||||||
jest-util: 29.7.0
|
jest-util: 29.7.0
|
||||||
jest-validate: 29.7.0
|
jest-validate: 29.7.0
|
||||||
yargs: 17.7.2
|
yargs: 17.7.2
|
||||||
@@ -15717,7 +15731,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- ts-node
|
- ts-node
|
||||||
|
|
||||||
[email protected](@types/[email protected])([email protected](@types/node@24.0.1)([email protected])):
|
[email protected](@types/[email protected])([email protected](@types/node@22.15.31)([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.27.4
|
'@babel/core': 7.27.4
|
||||||
'@jest/test-sequencer': 29.7.0
|
'@jest/test-sequencer': 29.7.0
|
||||||
@@ -15743,38 +15757,7 @@ snapshots:
|
|||||||
strip-json-comments: 3.1.1
|
strip-json-comments: 3.1.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.15.31
|
'@types/node': 22.15.31
|
||||||
ts-node: 10.9.2(@types/node@24.0.1)([email protected])
|
ts-node: 10.9.2(@types/node@22.15.31)([email protected])
|
||||||
transitivePeerDependencies:
|
|
||||||
- babel-plugin-macros
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
[email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])):
|
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.27.4
|
|
||||||
'@jest/test-sequencer': 29.7.0
|
|
||||||
'@jest/types': 29.6.3
|
|
||||||
babel-jest: 29.7.0(@babel/[email protected])
|
|
||||||
chalk: 4.1.2
|
|
||||||
ci-info: 3.9.0
|
|
||||||
deepmerge: 4.3.1
|
|
||||||
glob: 7.2.3
|
|
||||||
graceful-fs: 4.2.11
|
|
||||||
jest-circus: 29.7.0
|
|
||||||
jest-environment-node: 29.7.0
|
|
||||||
jest-get-type: 29.6.3
|
|
||||||
jest-regex-util: 29.6.3
|
|
||||||
jest-resolve: 29.7.0
|
|
||||||
jest-runner: 29.7.0
|
|
||||||
jest-util: 29.7.0
|
|
||||||
jest-validate: 29.7.0
|
|
||||||
micromatch: 4.0.8
|
|
||||||
parse-json: 5.2.0
|
|
||||||
pretty-format: 29.7.0
|
|
||||||
slash: 3.0.0
|
|
||||||
strip-json-comments: 3.1.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/node': 24.0.1
|
|
||||||
ts-node: 10.9.2(@types/[email protected])([email protected])
|
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -15994,12 +15977,12 @@ snapshots:
|
|||||||
merge-stream: 2.0.0
|
merge-stream: 2.0.0
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
|
|
||||||
[email protected](@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected])):
|
[email protected](@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/core': 29.7.0([email protected](@types/node@24.0.1)([email protected]))
|
'@jest/core': 29.7.0([email protected](@types/node@22.15.31)([email protected]))
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
import-local: 3.2.0
|
import-local: 3.2.0
|
||||||
jest-cli: 29.7.0(@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected]))
|
jest-cli: 29.7.0(@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected]))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
@@ -18351,9 +18334,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
svelte: 5.34.1
|
svelte: 5.34.1
|
||||||
|
|
||||||
[email protected]([email protected](@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected])))([email protected]):
|
[email protected]([email protected](@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected])))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
jest: 29.7.0(@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected]))
|
jest: 29.7.0(@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected]))
|
||||||
svelte: 4.2.20
|
svelte: 4.2.20
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -18377,6 +18360,21 @@ snapshots:
|
|||||||
svelte: 5.34.1
|
svelte: 5.34.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
[email protected](@babel/[email protected])([email protected]([email protected])([email protected](@types/[email protected])([email protected])))([email protected])([email protected])([email protected])([email protected]):
|
||||||
|
dependencies:
|
||||||
|
'@types/pug': 2.0.10
|
||||||
|
detect-indent: 6.1.0
|
||||||
|
magic-string: 0.30.17
|
||||||
|
sorcery: 0.11.1
|
||||||
|
strip-indent: 3.0.0
|
||||||
|
svelte: 4.2.20
|
||||||
|
optionalDependencies:
|
||||||
|
'@babel/core': 7.27.4
|
||||||
|
postcss: 8.5.5
|
||||||
|
postcss-load-config: 4.0.2([email protected])([email protected](@types/[email protected])([email protected]))
|
||||||
|
sass: 1.89.2
|
||||||
|
typescript: 5.8.3
|
||||||
|
|
||||||
[email protected](@babel/[email protected])([email protected]([email protected])([email protected](@types/[email protected])([email protected])))([email protected])([email protected])([email protected])([email protected]):
|
[email protected](@babel/[email protected])([email protected]([email protected])([email protected](@types/[email protected])([email protected])))([email protected])([email protected])([email protected])([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/pug': 2.0.10
|
'@types/pug': 2.0.10
|
||||||
@@ -18696,12 +18694,12 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected](@babel/[email protected])(@jest/[email protected])(@jest/[email protected])([email protected](@babel/[email protected]))([email protected])([email protected](@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected])))([email protected]):
|
[email protected](@babel/[email protected])(@jest/[email protected])(@jest/[email protected])([email protected](@babel/[email protected]))([email protected])([email protected](@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected])))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
bs-logger: 0.2.6
|
bs-logger: 0.2.6
|
||||||
ejs: 3.1.10
|
ejs: 3.1.10
|
||||||
fast-json-stable-stringify: 2.1.0
|
fast-json-stable-stringify: 2.1.0
|
||||||
jest: 29.7.0(@types/node@24.0.1)([email protected](@types/node@24.0.1)([email protected]))
|
jest: 29.7.0(@types/node@22.15.31)([email protected](@types/node@22.15.31)([email protected]))
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
lodash.memoize: 4.1.2
|
lodash.memoize: 4.1.2
|
||||||
make-error: 1.3.6
|
make-error: 1.3.6
|
||||||
@@ -18770,7 +18768,6 @@ snapshots:
|
|||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
v8-compile-cache-lib: 3.0.1
|
v8-compile-cache-lib: 3.0.1
|
||||||
yn: 3.1.1
|
yn: 3.1.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
[email protected](@types/[email protected])([email protected]):
|
[email protected](@types/[email protected])([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18789,6 +18786,7 @@ snapshots:
|
|||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
v8-compile-cache-lib: 3.0.1
|
v8-compile-cache-lib: 3.0.1
|
||||||
yn: 3.1.1
|
yn: 3.1.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18880,7 +18878,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]:
|
||||||
|
optional: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -19160,13 +19159,13 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
[email protected](@types/node@24.0.1)([email protected]):
|
[email protected](@types/node@22.15.31)([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
postcss: 8.5.5
|
postcss: 8.5.5
|
||||||
rollup: 3.29.5
|
rollup: 3.29.5
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.0.1
|
'@types/node': 22.15.31
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
sass: 1.89.2
|
sass: 1.89.2
|
||||||
|
|
||||||
@@ -19216,9 +19215,9 @@ snapshots:
|
|||||||
tsx: 4.20.3
|
tsx: 4.20.3
|
||||||
yaml: 2.8.0
|
yaml: 2.8.0
|
||||||
|
|
||||||
[email protected]([email protected](@types/node@24.0.1)([email protected])):
|
[email protected]([email protected](@types/node@22.15.31)([email protected])):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 4.5.14(@types/node@24.0.1)([email protected])
|
vite: 4.5.14(@types/node@22.15.31)([email protected])
|
||||||
|
|
||||||
[email protected]([email protected](@types/[email protected])([email protected])):
|
[email protected]([email protected](@types/[email protected])([email protected])):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user