multi line paste for add course and edit course

This commit is contained in:
rgrgogu
2026-07-30 20:11:32 +08:00
parent 04c4a3e1f6
commit f9616b7219
9 changed files with 35 additions and 10 deletions
@@ -131,7 +131,7 @@ export function Hero({ ads, onCtaClick }) {
{/* Navigation and Progress — only meaningful with more than one slide */} {/* Navigation and Progress — only meaningful with more than one slide */}
{ads.length > 1 && ( {ads.length > 1 && (
<div className="flex items-center justify-between mt-4 px-2"> <div className="flex items-center justify-between mt-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CarouselPrevious className="static translate-y-0 size-8 rounded-lg" /> <CarouselPrevious className="static translate-y-0 size-8 rounded-lg" />
<CarouselNext className="static translate-y-0 size-8 rounded-lg" /> <CarouselNext className="static translate-y-0 size-8 rounded-lg" />
@@ -516,6 +516,7 @@ export function VideoBlock({ content, onWatchProgress, resumePercent, antiSkipEn
playsInline playsInline
controlsList="nodownload nofullscreen noremoteplayback" controlsList="nodownload nofullscreen noremoteplayback"
disablePictureInPicture disablePictureInPicture
disableRemotePlayback
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
className={`w-full h-full ${isFullscreen ? "object-contain" : "object-cover"}`} className={`w-full h-full ${isFullscreen ? "object-contain" : "object-cover"}`}
/> />
@@ -610,7 +611,7 @@ export function VideoBlock({ content, onWatchProgress, resumePercent, antiSkipEn
style={{ left: `${hoverProgress.x}px`, transform: "translateX(-50%)" }} style={{ left: `${hoverProgress.x}px`, transform: "translateX(-50%)" }}
> >
<div className="rounded-md overflow-hidden border border-white/20 shadow-xl" style={{ width: 160, height: 90 }}> <div className="rounded-md overflow-hidden border border-white/20 shadow-xl" style={{ width: 160, height: 90 }}>
<video ref={previewVidRef} src={blobUrl} preload="auto" muted playsInline disablePictureInPicture onContextMenu={(e) => e.preventDefault()} className="w-full h-full object-cover" /> <video ref={previewVidRef} src={blobUrl} preload="auto" muted playsInline disablePictureInPicture disableRemotePlayback onContextMenu={(e) => e.preventDefault()} className="w-full h-full object-cover" />
</div> </div>
<span className="text-white text-xs mt-1 font-medium tabular-nums drop-shadow">{fmtTime(hoverProgress.time)}</span> <span className="text-white text-xs mt-1 font-medium tabular-nums drop-shadow">{fmtTime(hoverProgress.time)}</span>
<div className="w-2 h-2 bg-black/60 rotate-45 -mt-1 border-r border-b border-white/20" /> <div className="w-2 h-2 bg-black/60 rotate-45 -mt-1 border-r border-b border-white/20" />
+13 -1
View File
@@ -181,8 +181,19 @@ export default function AddCourse() {
}, },
}); });
const { fields: objectiveFields, append: appendObjective, remove: removeObjective } = const { fields: objectiveFields, append: appendObjective, remove: removeObjective, insert: insertObjective } =
useFieldArray({ control, name: "objectives" }); useFieldArray({ control, name: "objectives" });
const handleObjectivePaste = (e, index) => {
const text = e.clipboardData.getData("text");
const lines = text.split(/\r\n|\r|\n/).map((l) => l.trim()).filter(Boolean);
if (lines.length <= 1) return;
e.preventDefault();
setValue(`objectives.${index}.text`, lines[0], { shouldDirty: true, shouldValidate: true });
lines.slice(1).forEach((line, i) => {
insertObjective(index + 1 + i, { text: line });
});
};
const { fields: roleFields, append: appendRole, remove: removeRole } = const { fields: roleFields, append: appendRole, remove: removeRole } =
useFieldArray({ control, name: "roles" }); useFieldArray({ control, name: "roles" });
@@ -415,6 +426,7 @@ export default function AddCourse() {
<Input <Input
placeholder={`Objective ${index + 1}`} placeholder={`Objective ${index + 1}`}
{...register(`objectives.${index}.text`)} {...register(`objectives.${index}.text`)}
onPaste={(e) => handleObjectivePaste(e, index)}
/> />
<FieldError message={errors.objectives?.[index]?.text?.message} /> <FieldError message={errors.objectives?.[index]?.text?.message} />
</div> </div>
+13 -1
View File
@@ -239,8 +239,19 @@ export default function EditCourse() {
}, },
}); });
const { fields: objectiveFields, append: appendObjective, remove: removeObjective } = const { fields: objectiveFields, append: appendObjective, remove: removeObjective, insert: insertObjective } =
useFieldArray({ control, name: "objectives" }); useFieldArray({ control, name: "objectives" });
const handleObjectivePaste = (e, index) => {
const text = e.clipboardData.getData("text");
const lines = text.split(/\r\n|\r|\n/).map((l) => l.trim()).filter(Boolean);
if (lines.length <= 1) return;
e.preventDefault();
setValue(`objectives.${index}.text`, lines[0], { shouldDirty: true, shouldValidate: true });
lines.slice(1).forEach((line, i) => {
insertObjective(index + 1 + i, { text: line });
});
};
const { fields: roleFields, append: appendRole, remove: removeRole } = const { fields: roleFields, append: appendRole, remove: removeRole } =
useFieldArray({ control, name: "roles" }); useFieldArray({ control, name: "roles" });
@@ -675,6 +686,7 @@ export default function EditCourse() {
<Input <Input
placeholder={`Objective ${index + 1}`} placeholder={`Objective ${index + 1}`}
{...register(`objectives.${index}.text`)} {...register(`objectives.${index}.text`)}
onPaste={(e) => handleObjectivePaste(e, index)}
/> />
<FieldError message={errors.objectives?.[index]?.text?.message} /> <FieldError message={errors.objectives?.[index]?.text?.message} />
</div> </div>
+1 -1
View File
@@ -704,7 +704,7 @@ const CourseDetails = () => {
<span>{progressSummary.lessons_completed} of {progressSummary.lessons_total} lessons complete</span> <span>{progressSummary.lessons_completed} of {progressSummary.lessons_total} lessons complete</span>
<span>{progressSummary.percent}%</span> <span>{progressSummary.percent}%</span>
</div> </div>
<Progress value={progressSummary.percent} /> <Progress value={progressSummary.percent} className="[&>div]:bg-blue-500" />
</div> </div>
)} )}
<div ref={CourseBreadcrumb} className="w-fit"> <div ref={CourseBreadcrumb} className="w-fit">
+1 -1
View File
@@ -181,7 +181,7 @@ const LessonDetails = () => {
className="flex-1 flex flex-col" className="flex-1 flex flex-col"
style={{ paddingTop: "var(--navbar-h)" }} style={{ paddingTop: "var(--navbar-h)" }}
> >
<div className="flex flex-col gap-6 flex-1 min-w-0 xs:px-4 xs:py-8 lg:px-16 lg:py-10"> <div className="flex flex-col gap-6 lg:container lg:max-w-4xl mx-auto xs:px-4 xs:py-8 lg:px-16 lg:py-10">
<AppBreadcrumb items={items} /> <AppBreadcrumb items={items} />
{taskCtx?.has_task && ( {taskCtx?.has_task && (
+1 -1
View File
@@ -305,7 +305,7 @@ const ProfilePage = () => {
}; };
return ( return (
<div className="mt-17 bg-muted h-full"> <div className="mt-17 bg-muted min-h-screen">
<div className="p-6 lg:container lg:max-w-5xl lg:mx-auto space-y-4"> <div className="p-6 lg:container lg:max-w-5xl lg:mx-auto space-y-4">
{/* ── Header card ──────────────────────────────────────────────────── */} {/* ── Header card ──────────────────────────────────────────────────── */}
+2 -2
View File
@@ -33,7 +33,7 @@ const LessonsRoadmap = ({ unitDetail, currentLessonId, onLessonClick, onContinue
const quiz = unitDetail.quiz ?? null; const quiz = unitDetail.quiz ?? null;
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 ">
{lessons.map((lesson, index) => { {lessons.map((lesson, index) => {
const completed = lesson.status === "completed"; const completed = lesson.status === "completed";
const isCurrent = lesson.lesson_id === currentLessonId; const isCurrent = lesson.lesson_id === currentLessonId;
@@ -175,7 +175,7 @@ const UnitDetails = () => {
return ( return (
<div> <div>
<PageMeta title={unitDetail ? `${unitDetail.title} - STARR` : undefined} description={unitDetail?.description} /> <PageMeta title={unitDetail ? `${unitDetail.title} - STARR` : undefined} description={unitDetail?.description} />
<div className="xs:my-20 lg:my-28 lg:container lg:mx-auto xs:px-4 lg:px-4 flex flex-col gap-6 max-w-3xl"> <div className="xs:py-24 xs:px-8 lg:px-0 lg:py-28 flex flex-col gap-6 lg:container lg:mx-auto lg:max-w-2xl">
<AppBreadcrumb items={items} /> <AppBreadcrumb items={items} />
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
+1 -1
View File
@@ -194,7 +194,7 @@ function LandingPage() {
{ContactData.map(({ id, icon: Icon, label, value }) => ( {ContactData.map(({ id, icon: Icon, label, value }) => (
<div <div
key={id} key={id}
className="flex items-center justify-between bg-primary dark:bg-blue-600 rounded-md px-4 py-2 text-sm text-primary dark:text-white" className="flex items-center justify-between bg-primary dark:bg-blue-600 rounded-md px-4 py-2 text-sm text-primary-foreground dark:text-white"
> >
<div className="flex items-center flex-wrap gap-2"> <div className="flex items-center flex-wrap gap-2">
<Icon className="size-4" /> <Icon className="size-4" />