course creation integartion fix
This commit is contained in:
parent
7ffbc666b0
commit
4d5d4f0d15
|
|
@ -7,10 +7,12 @@ export interface FileUploadProps extends Omit<React.InputHTMLAttributes<HTMLInpu
|
||||||
accept?: string
|
accept?: string
|
||||||
label?: string
|
label?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
/** Shorter, row-oriented layout for wide forms / modals */
|
||||||
|
variant?: "default" | "compact"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUpload = React.forwardRef<HTMLInputElement, FileUploadProps>(
|
export const FileUpload = React.forwardRef<HTMLInputElement, FileUploadProps>(
|
||||||
({ className, onFileSelect, accept, label, description, ...props }, ref) => {
|
({ className, onFileSelect, accept, label, description, variant = "default", ...props }, ref) => {
|
||||||
const [file, setFile] = React.useState<File | null>(null)
|
const [file, setFile] = React.useState<File | null>(null)
|
||||||
const [dragActive, setDragActive] = React.useState(false)
|
const [dragActive, setDragActive] = React.useState(false)
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
@ -48,10 +50,15 @@ export const FileUpload = React.forwardRef<HTMLInputElement, FileUploadProps>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCompact = variant === "compact"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors",
|
"relative flex rounded-lg border-2 border-dashed transition-colors",
|
||||||
|
isCompact
|
||||||
|
? "min-h-0 flex-col items-stretch justify-center sm:flex-row sm:items-center"
|
||||||
|
: "flex-col items-center justify-center",
|
||||||
dragActive ? "border-brand-500 bg-brand-50" : "border-grayScale-200 bg-grayScale-50",
|
dragActive ? "border-brand-500 bg-brand-50" : "border-grayScale-200 bg-grayScale-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
@ -68,27 +75,44 @@ export const FileUpload = React.forwardRef<HTMLInputElement, FileUploadProps>(
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center justify-center p-8 text-center">
|
<div
|
||||||
<div className="mb-4 grid h-16 w-16 place-items-center rounded-full bg-brand-100 text-brand-600">
|
className={cn(
|
||||||
<Upload className="h-8 w-8" />
|
"flex w-full",
|
||||||
|
isCompact
|
||||||
|
? "flex-col gap-3 p-4 text-center sm:flex-row sm:items-center sm:gap-4 sm:p-4 sm:text-left"
|
||||||
|
: "flex-col items-center justify-center p-8 text-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid shrink-0 place-items-center rounded-full bg-brand-100 text-brand-600",
|
||||||
|
isCompact ? "mx-auto h-11 w-11 sm:mx-0" : "mb-4 h-16 w-16",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Upload className={isCompact ? "h-5 w-5" : "h-8 w-8"} />
|
||||||
</div>
|
</div>
|
||||||
{file ? (
|
{file ? (
|
||||||
<>
|
<div className={cn("min-w-0 flex-1", !isCompact && "text-center")}>
|
||||||
<p className="mb-1 text-sm font-medium text-grayScale-900">{file.name}</p>
|
<p className="mb-1 text-sm font-medium text-grayScale-900 break-all">{file.name}</p>
|
||||||
<p className="text-xs text-grayScale-500">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
|
<p className="text-xs text-grayScale-500">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="mb-1 text-sm font-medium text-grayScale-900">
|
<div className={cn("min-w-0 flex-1 space-y-1", isCompact && "sm:pr-2")}>
|
||||||
|
<p className="text-sm font-medium text-grayScale-900">
|
||||||
{label || "Drag & Drop Video Here"}
|
{label || "Drag & Drop Video Here"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-4 text-xs text-grayScale-500">
|
<p className="text-xs text-grayScale-500">
|
||||||
{description || "or click to browse files"}
|
{description || "or click to browse files"}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
className="rounded-lg bg-brand-500 px-4 py-2 text-sm font-medium text-white hover:bg-brand-600"
|
className={cn(
|
||||||
|
"shrink-0 rounded-lg bg-brand-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-brand-600",
|
||||||
|
isCompact ? "mx-auto w-full max-w-[200px] sm:mx-0 sm:w-auto" : "mt-1",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Browse Files
|
Browse Files
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export function AppLayout() {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Topbar onSidebarToggle={handleSidebarToggle} />
|
<Topbar onSidebarToggle={handleSidebarToggle} />
|
||||||
<main className="min-w-0 flex-1 overflow-y-auto px-4 pb-8 pt-4 lg:px-6">
|
<main className="min-w-0 flex-1 overflow-x-hidden overflow-y-auto px-3 pb-8 pt-4 sm:px-4 lg:px-6">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<footer className="border-t bg-grayScale-50 px-4 py-3 lg:px-6">
|
<footer className="border-t bg-grayScale-50 px-4 py-3 lg:px-6">
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ import {
|
||||||
addSubCoursePrerequisite,
|
addSubCoursePrerequisite,
|
||||||
removeSubCoursePrerequisite,
|
removeSubCoursePrerequisite,
|
||||||
} from "../../api/courses.api";
|
} from "../../api/courses.api";
|
||||||
|
import { uploadImageFile } from "../../api/files.api";
|
||||||
import { Input } from "../../components/ui/input";
|
import { Input } from "../../components/ui/input";
|
||||||
|
import { FileUpload } from "../../components/ui/file-upload";
|
||||||
import type {
|
import type {
|
||||||
SubCourse,
|
SubCourse,
|
||||||
Course,
|
Course,
|
||||||
|
|
@ -43,6 +45,7 @@ import type {
|
||||||
SubCoursePrerequisite,
|
SubCoursePrerequisite,
|
||||||
} from "../../types/course.types";
|
} from "../../types/course.types";
|
||||||
import { SpinnerIcon } from "../../components/ui/spinner-icon";
|
import { SpinnerIcon } from "../../components/ui/spinner-icon";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function SubCoursesPage() {
|
export function SubCoursesPage() {
|
||||||
const { categoryId, courseId } = useParams<{
|
const { categoryId, courseId } = useParams<{
|
||||||
|
|
@ -72,7 +75,11 @@ export function SubCoursesPage() {
|
||||||
);
|
);
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [level, setLevel] = useState("");
|
const [level, setLevel] = useState("BEGINNER");
|
||||||
|
const [subLevel, setSubLevel] = useState("");
|
||||||
|
const [thumbnailUrl, setThumbnailUrl] = useState("");
|
||||||
|
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
|
||||||
|
const [displayOrder, setDisplayOrder] = useState("1");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState<string | null>(null);
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -222,10 +229,19 @@ export function SubCoursesPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nextSubCourseDisplayOrder = () =>
|
||||||
|
subCourses.length === 0
|
||||||
|
? 1
|
||||||
|
: Math.max(0, ...subCourses.map((s) => s.display_order ?? 0)) + 1;
|
||||||
|
|
||||||
const handleAddSubCourse = () => {
|
const handleAddSubCourse = () => {
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setLevel("");
|
setLevel("BEGINNER");
|
||||||
|
setSubLevel("");
|
||||||
|
setThumbnailUrl("");
|
||||||
|
setThumbnailFile(null);
|
||||||
|
setDisplayOrder(String(nextSubCourseDisplayOrder()));
|
||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
setShowAddModal(true);
|
setShowAddModal(true);
|
||||||
};
|
};
|
||||||
|
|
@ -235,20 +251,45 @@ export function SubCoursesPage() {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setSaveError(null);
|
setSaveError(null);
|
||||||
try {
|
try {
|
||||||
|
let thumbnail = thumbnailUrl.trim();
|
||||||
|
if (thumbnailFile) {
|
||||||
|
const uploadRes = await uploadImageFile(thumbnailFile);
|
||||||
|
const uploadedUrl = uploadRes.data?.data?.url?.trim();
|
||||||
|
if (!uploadedUrl) throw new Error("Missing uploaded image url");
|
||||||
|
thumbnail = uploadedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedOrder = parseInt(displayOrder, 10);
|
||||||
|
const display_order = Number.isFinite(parsedOrder) && parsedOrder >= 0
|
||||||
|
? parsedOrder
|
||||||
|
: nextSubCourseDisplayOrder();
|
||||||
|
|
||||||
await createSubCourse({
|
await createSubCourse({
|
||||||
course_id: Number(courseId),
|
course_id: Number(courseId),
|
||||||
title,
|
title: title.trim(),
|
||||||
description,
|
description: description.trim(),
|
||||||
level,
|
thumbnail,
|
||||||
|
display_order,
|
||||||
|
level: level.trim() || "BEGINNER",
|
||||||
|
sub_level: subLevel.trim(),
|
||||||
});
|
});
|
||||||
setShowAddModal(false);
|
setShowAddModal(false);
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setLevel("");
|
setLevel("BEGINNER");
|
||||||
|
setSubLevel("");
|
||||||
|
setThumbnailUrl("");
|
||||||
|
setThumbnailFile(null);
|
||||||
|
setDisplayOrder("1");
|
||||||
await fetchSubCourses();
|
await fetchSubCourses();
|
||||||
} catch (err) {
|
toast.success("Course created successfully");
|
||||||
|
} catch (err: unknown) {
|
||||||
console.error("Failed to create sub-course:", err);
|
console.error("Failed to create sub-course:", err);
|
||||||
setSaveError("Failed to create course");
|
const msg =
|
||||||
|
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
|
||||||
|
"Failed to create course";
|
||||||
|
setSaveError(msg);
|
||||||
|
toast.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
@ -649,8 +690,8 @@ export function SubCoursesPage() {
|
||||||
|
|
||||||
{/* Delete Modal */}
|
{/* Delete Modal */}
|
||||||
{showDeleteModal && subCourseToDelete && (
|
{showDeleteModal && subCourseToDelete && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden bg-black/40 p-3 backdrop-blur-sm sm:p-6">
|
||||||
<div className="mx-4 w-full max-w-sm rounded-2xl bg-white shadow-2xl">
|
<div className="my-auto w-full max-w-sm rounded-2xl bg-white shadow-2xl">
|
||||||
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4">
|
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4">
|
||||||
<h2 className="text-lg font-semibold text-grayScale-700">
|
<h2 className="text-lg font-semibold text-grayScale-700">
|
||||||
Delete Course
|
Delete Course
|
||||||
|
|
@ -697,24 +738,30 @@ export function SubCoursesPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add Sub-course Modal */}
|
{/* Add Sub-course Modal — POST /course-management/sub-courses */}
|
||||||
{showAddModal && (
|
{showAddModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden bg-black/40 p-3 backdrop-blur-sm sm:p-6">
|
||||||
<div className="mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
<div
|
||||||
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4">
|
role="dialog"
|
||||||
<h2 className="text-lg font-semibold text-grayScale-700">
|
aria-modal="true"
|
||||||
|
className="my-auto w-full max-w-4xl flex-shrink-0 rounded-2xl bg-white shadow-2xl"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-grayScale-100 px-4 py-3 sm:px-6 sm:py-4">
|
||||||
|
<h2 className="text-base font-semibold text-grayScale-700 sm:text-lg">
|
||||||
Add New Course
|
Add New Course
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setShowAddModal(false)}
|
onClick={() => setShowAddModal(false)}
|
||||||
className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"
|
className="grid h-10 w-10 min-h-[44px] min-w-[44px] place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600 sm:h-8 sm:w-8 sm:min-h-0 sm:min-w-0"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-5 px-6 py-6">
|
<div className="max-h-[min(78vh,840px)] overflow-y-auto px-4 py-5 sm:px-6 sm:py-6">
|
||||||
<div className="space-y-1.5">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-x-6 md:gap-y-4">
|
||||||
|
<div className="space-y-1.5 md:col-span-2">
|
||||||
<label className="text-sm font-semibold text-grayScale-600">
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
Title
|
Title
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -722,9 +769,10 @@ export function SubCoursesPage() {
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="Enter course title"
|
placeholder="Enter course title"
|
||||||
|
className="min-h-[44px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5 md:col-span-2">
|
||||||
<label className="text-sm font-semibold text-grayScale-600">
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
Description
|
Description
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -740,21 +788,77 @@ export function SubCoursesPage() {
|
||||||
<label className="text-sm font-semibold text-grayScale-600">
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
Level
|
Level
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<select
|
||||||
value={level}
|
value={level}
|
||||||
onChange={(e) => setLevel(e.target.value)}
|
onChange={(e) => setLevel(e.target.value)}
|
||||||
placeholder="e.g., Beginner, Intermediate, Advanced"
|
className="min-h-[44px] w-full rounded-lg border border-grayScale-200 bg-white px-3 py-2.5 text-sm transition-colors focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
|
||||||
|
>
|
||||||
|
<option value="BEGINNER">BEGINNER</option>
|
||||||
|
<option value="INTERMEDIATE">INTERMEDIATE</option>
|
||||||
|
<option value="ADVANCED">ADVANCED</option>
|
||||||
|
<option value="EXPERT">EXPERT</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
|
Sub-level
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={subLevel}
|
||||||
|
onChange={(e) => setSubLevel(e.target.value)}
|
||||||
|
placeholder='e.g. A1, B2 (CEFR or your scale)'
|
||||||
|
className="min-h-[44px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1.5 md:col-span-2 md:max-w-xs">
|
||||||
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
|
Display order
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={displayOrder}
|
||||||
|
onChange={(e) => setDisplayOrder(e.target.value)}
|
||||||
|
placeholder="1"
|
||||||
|
className="min-h-[44px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3 md:col-span-2">
|
||||||
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
|
Thumbnail
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:items-start">
|
||||||
|
<FileUpload
|
||||||
|
variant="compact"
|
||||||
|
accept="image/*"
|
||||||
|
onFileSelect={setThumbnailFile}
|
||||||
|
label="Upload thumbnail image"
|
||||||
|
description="JPEG, PNG, WEBP — or paste a URL"
|
||||||
|
className="border-grayScale-200 transition-colors hover:border-brand-400 hover:bg-brand-50/30"
|
||||||
|
/>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<span className="text-xs font-medium text-grayScale-500">
|
||||||
|
Thumbnail URL (optional)
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
value={thumbnailUrl}
|
||||||
|
onChange={(e) => setThumbnailUrl(e.target.value)}
|
||||||
|
placeholder="https://..."
|
||||||
|
className="min-h-[44px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{saveError && (
|
{saveError && (
|
||||||
<div className="flex items-center gap-2 rounded-lg border border-red-100 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600">
|
<div className="flex items-center gap-2 rounded-lg border border-red-100 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600 md:col-span-2">
|
||||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||||
{saveError}
|
{saveError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col-reverse gap-3 border-t border-grayScale-100 px-6 py-4 sm:flex-row sm:justify-end">
|
<div className="flex flex-col-reverse gap-3 border-t border-grayScale-100 px-4 py-4 sm:flex-row sm:justify-end sm:px-6">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowAddModal(false)}
|
onClick={() => setShowAddModal(false)}
|
||||||
|
|
@ -912,22 +1016,28 @@ export function SubCoursesPage() {
|
||||||
|
|
||||||
{/* Edit Sub-course Modal */}
|
{/* Edit Sub-course Modal */}
|
||||||
{showEditModal && subCourseToEdit && (
|
{showEditModal && subCourseToEdit && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden bg-black/40 p-3 backdrop-blur-sm sm:p-6">
|
||||||
<div className="mx-4 w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
<div
|
||||||
<div className="flex items-center justify-between border-b border-grayScale-100 px-6 py-4">
|
role="dialog"
|
||||||
<h2 className="text-lg font-semibold text-grayScale-700">
|
aria-modal="true"
|
||||||
|
className="my-auto w-full max-w-3xl flex-shrink-0 rounded-2xl bg-white shadow-2xl"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-grayScale-100 px-4 py-3 sm:px-6 sm:py-4">
|
||||||
|
<h2 className="text-base font-semibold text-grayScale-700 sm:text-lg">
|
||||||
Edit Course
|
Edit Course
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => setShowEditModal(false)}
|
onClick={() => setShowEditModal(false)}
|
||||||
className="grid h-8 w-8 place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600"
|
className="grid h-10 w-10 min-h-[44px] min-w-[44px] place-items-center rounded-lg text-grayScale-400 transition-colors hover:bg-grayScale-100 hover:text-grayScale-600 sm:h-8 sm:w-8 sm:min-h-0 sm:min-w-0"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-5 px-6 py-6">
|
<div className="max-h-[min(78vh,640px)] overflow-y-auto px-4 py-5 sm:px-6 sm:py-6">
|
||||||
<div className="space-y-1.5">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-x-6 md:gap-y-4">
|
||||||
|
<div className="space-y-1.5 md:col-span-2">
|
||||||
<label className="text-sm font-semibold text-grayScale-600">
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
Title
|
Title
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -935,9 +1045,10 @@ export function SubCoursesPage() {
|
||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
placeholder="Enter course title"
|
placeholder="Enter course title"
|
||||||
|
className="min-h-[44px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5 md:col-span-2">
|
||||||
<label className="text-sm font-semibold text-grayScale-600">
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
Description
|
Description
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -949,25 +1060,27 @@ export function SubCoursesPage() {
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5 md:col-span-2 md:max-w-md">
|
||||||
<label className="text-sm font-semibold text-grayScale-600">
|
<label className="text-sm font-semibold text-grayScale-600">
|
||||||
Level
|
Level
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
value={level}
|
value={level}
|
||||||
onChange={(e) => setLevel(e.target.value)}
|
onChange={(e) => setLevel(e.target.value)}
|
||||||
placeholder="e.g., Beginner, Intermediate, Advanced"
|
placeholder="e.g., BEGINNER, Intermediate, Advanced"
|
||||||
|
className="min-h-[44px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{saveError && (
|
{saveError && (
|
||||||
<div className="flex items-center gap-2 rounded-lg border border-red-100 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600">
|
<div className="flex items-center gap-2 rounded-lg border border-red-100 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600 md:col-span-2">
|
||||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||||
{saveError}
|
{saveError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col-reverse gap-3 border-t border-grayScale-100 px-6 py-4 sm:flex-row sm:justify-end">
|
<div className="flex flex-col-reverse gap-3 border-t border-grayScale-100 px-4 py-4 sm:flex-row sm:justify-end sm:px-6">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowEditModal(false)}
|
onClick={() => setShowEditModal(false)}
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ export interface SubCourse {
|
||||||
level: string
|
level: string
|
||||||
thumbnail: string
|
thumbnail: string
|
||||||
display_order: number
|
display_order: number
|
||||||
|
sub_level?: string
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,11 +211,15 @@ export interface GetSubCoursesResponse {
|
||||||
metadata: unknown
|
metadata: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** POST /course-management/sub-courses */
|
||||||
export interface CreateSubCourseRequest {
|
export interface CreateSubCourseRequest {
|
||||||
course_id: number
|
course_id: number
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
|
thumbnail: string
|
||||||
|
display_order: number
|
||||||
level: string
|
level: string
|
||||||
|
sub_level: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateSubCourseRequest {
|
export interface UpdateSubCourseRequest {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user