diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index 07c456b..cc6c51f 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -78,6 +78,26 @@ import type { CreateTopLevelCourseModuleResponse, CreateProgramCourseRequest, CreateProgramCourseResponse, + CreateExamPrepCatalogCourseRequest, + CreateExamPrepCatalogCourseResponse, + GetExamPrepCatalogCoursesResponse, + UpdateExamPrepCatalogCourseRequest, + UpdateExamPrepCatalogCourseResponse, + CreateExamPrepCatalogUnitRequest, + CreateExamPrepCatalogUnitResponse, + UpdateExamPrepCatalogUnitRequest, + UpdateExamPrepCatalogUnitResponse, + GetExamPrepCatalogUnitsResponse, + CreateExamPrepUnitModuleRequest, + CreateExamPrepUnitModuleResponse, + UpdateExamPrepUnitModuleRequest, + UpdateExamPrepUnitModuleResponse, + GetExamPrepUnitModulesResponse, + CreateExamPrepModuleLessonRequest, + CreateExamPrepModuleLessonResponse, + UpdateExamPrepModuleLessonRequest, + UpdateExamPrepModuleLessonResponse, + GetExamPrepModuleLessonsResponse, GetTopLevelModuleLessonsResponse, GetPracticesByParentContextResponse, CreateParentLinkedPracticeRequest, @@ -447,6 +467,128 @@ export const createProgramCourse = ( data: CreateProgramCourseRequest, ) => http.post(`/programs/${programId}/courses`, data) +/** English proficiency catalog course — POST /exam-prep/catalog-courses */ +export const createExamPrepCatalogCourse = ( + data: CreateExamPrepCatalogCourseRequest, +) => http.post("/exam-prep/catalog-courses", data) + +/** English proficiency catalog courses — GET /exam-prep/catalog-courses */ +export const getExamPrepCatalogCourses = (params?: { limit?: number; offset?: number }) => + http.get("/exam-prep/catalog-courses", { params }) + +/** English proficiency catalog course — PUT /exam-prep/catalog-courses/:catalogCourseId */ +export const updateExamPrepCatalogCourse = ( + catalogCourseId: number, + data: UpdateExamPrepCatalogCourseRequest, +) => + http.put( + `/exam-prep/catalog-courses/${catalogCourseId}`, + data, + ) + +/** English proficiency catalog course — DELETE /exam-prep/catalog-courses/:catalogCourseId */ +export const deleteExamPrepCatalogCourse = (catalogCourseId: number) => + http.delete(`/exam-prep/catalog-courses/${catalogCourseId}`) + +/** English proficiency catalog unit — POST /exam-prep/catalog-courses/:catalogCourseId/units */ +export const createExamPrepCatalogUnit = ( + catalogCourseId: number, + data: CreateExamPrepCatalogUnitRequest, +) => + http.post( + `/exam-prep/catalog-courses/${catalogCourseId}/units`, + data, + ) + +/** English proficiency catalog units — GET /exam-prep/catalog-courses/:catalogCourseId/units */ +export const getExamPrepCatalogUnits = ( + catalogCourseId: number, + params?: { limit?: number; offset?: number }, +) => + http.get( + `/exam-prep/catalog-courses/${catalogCourseId}/units`, + { params }, + ) + +/** English proficiency unit — PUT /exam-prep/units/:unitId */ +export const updateExamPrepCatalogUnit = ( + unitId: number, + data: UpdateExamPrepCatalogUnitRequest, +) => http.put(`/exam-prep/units/${unitId}`, data) + +/** English proficiency unit — DELETE /exam-prep/units/:unitId */ +export const deleteExamPrepCatalogUnit = (unitId: number) => + http.delete(`/exam-prep/units/${unitId}`) + +/** English proficiency unit modules — POST /exam-prep/units/:unitId/modules */ +export const createExamPrepUnitModule = ( + unitId: number, + data: CreateExamPrepUnitModuleRequest, +) => + http.post( + `/exam-prep/units/${unitId}/modules`, + data, + ) + +/** English proficiency unit modules — GET /exam-prep/units/:unitId/modules */ +export const getExamPrepUnitModules = ( + unitId: number, + params?: { limit?: number; offset?: number }, +) => + http.get(`/exam-prep/units/${unitId}/modules`, { + params, + }) + +/** English proficiency module — PUT /exam-prep/modules/:moduleId */ +export const updateExamPrepUnitModule = ( + moduleId: number, + data: UpdateExamPrepUnitModuleRequest, +) => + http.put( + `/exam-prep/modules/${moduleId}`, + data, + ) + +/** English proficiency module — DELETE /exam-prep/modules/:moduleId */ +export const deleteExamPrepUnitModule = (moduleId: number) => + http.delete(`/exam-prep/modules/${moduleId}`) + +/** English proficiency module lessons — GET /exam-prep/modules/:moduleId/lessons */ +export const getExamPrepModuleLessons = ( + moduleId: number, + params?: { limit?: number; offset?: number }, +) => + http.get( + `/exam-prep/modules/${moduleId}/lessons`, + { + params, + }, + ) + +/** English proficiency module lesson — POST /exam-prep/modules/:moduleId/lessons */ +export const createExamPrepModuleLesson = ( + moduleId: number, + data: CreateExamPrepModuleLessonRequest, +) => + http.post( + `/exam-prep/modules/${moduleId}/lessons`, + data, + ) + +/** English proficiency lesson — PUT /exam-prep/lessons/:lessonId */ +export const updateExamPrepModuleLesson = ( + lessonId: number, + data: UpdateExamPrepModuleLessonRequest, +) => + http.put( + `/exam-prep/lessons/${lessonId}`, + data, + ) + +/** English proficiency lesson — DELETE /exam-prep/lessons/:lessonId */ +export const deleteExamPrepModuleLesson = (lessonId: number) => + http.delete(`/exam-prep/lessons/${lessonId}`) + /** Top-level course resource (Learn English track) — PUT /courses/:id */ export const updateTopLevelCourse = (courseId: number, data: UpdateTopLevelCourseRequest) => http.put(`/courses/${courseId}`, data) diff --git a/src/api/files.api.ts b/src/api/files.api.ts index ec6a59d..101195c 100644 --- a/src/api/files.api.ts +++ b/src/api/files.api.ts @@ -44,6 +44,36 @@ export interface UploadMediaFromUrlPayload extends UploadMediaOptions { sourceUrl: string } +const GOOGLE_DRIVE_HOSTS = new Set([ + "drive.google.com", + "www.drive.google.com", +]) + +const getGoogleDriveFileId = (rawUrl: string): string | null => { + try { + const url = new URL(rawUrl.trim()) + if (!GOOGLE_DRIVE_HOSTS.has(url.hostname.toLowerCase())) return null + const fromQuery = url.searchParams.get("id")?.trim() + if (fromQuery) return fromQuery + const fileMatch = url.pathname.match(/\/file\/d\/([^/]+)/i) + return fileMatch?.[1]?.trim() || null + } catch { + return null + } +} + +const normalizeSourceUrlForUpload = ( + mediaType: UploadMediaType, + sourceUrl: string, +): string => { + const trimmed = sourceUrl.trim() + if (mediaType !== "image") return trimmed + const fileId = getGoogleDriveFileId(trimmed) + if (!fileId) return trimmed + // Use Drive thumbnail endpoint so backend receives actual image bytes, not HTML viewer. + return `https://drive.google.com/thumbnail?id=${encodeURIComponent(fileId)}&sz=w2048` +} + export const uploadMediaFile = ( mediaType: UploadMediaType, file: File, @@ -67,7 +97,7 @@ export const uploadMediaFromUrl = ( ) => http.post("/files/upload", { media_type: mediaType, - source_url: payload.sourceUrl, + source_url: normalizeSourceUrlForUpload(mediaType, payload.sourceUrl), ...(mediaType === "video" && payload.title ? { title: payload.title } : {}), ...(mediaType === "video" && payload.description ? { description: payload.description } : {}), }) diff --git a/src/components/content-management/PracticeQuestionEditorFields.tsx b/src/components/content-management/PracticeQuestionEditorFields.tsx index 4a63c3d..3635961 100644 --- a/src/components/content-management/PracticeQuestionEditorFields.tsx +++ b/src/components/content-management/PracticeQuestionEditorFields.tsx @@ -14,6 +14,8 @@ import { Select } from "../ui/select" import { Button } from "../ui/button" import { SpinnerIcon } from "../ui/spinner-icon" import { cn } from "../../lib/utils" +import { ResolvedAudio } from "../media/ResolvedAudio" +import { ResolvedImage } from "../media/ResolvedImage" export type PracticeQuestionEditorType = "MCQ" | "TRUE_FALSE" | "SHORT" | "AUDIO" export type PracticeQuestionEditorDifficulty = "EASY" | "MEDIUM" | "HARD" @@ -815,7 +817,7 @@ export function PracticeQuestionEditorFields({ disabled={controlsDisabled} /> {voicePreviewUrl ? ( -