This commit is contained in:
elnatansamuel25 2026-04-24 15:20:51 +03:00
parent 1480eefbe6
commit d4d61bfed2
34 changed files with 3979 additions and 1247 deletions

View File

@ -22,6 +22,13 @@ import { CourseDetailPage } from "../pages/content-management/CourseDetailPage";
import { ModuleDetailPage } from "../pages/content-management/ModuleDetailPage"; import { ModuleDetailPage } from "../pages/content-management/ModuleDetailPage";
import { AddVideoFlow } from "../pages/content-management/AddVideoFlow"; import { AddVideoFlow } from "../pages/content-management/AddVideoFlow";
import { AddPracticeFlow } from "../pages/content-management/AddPracticeFlow"; import { AddPracticeFlow } from "../pages/content-management/AddPracticeFlow";
import { CourseModuleDetailPage } from "../pages/content-management/CourseModuleDetailPage";
import { AttachPracticeFlow } from "../pages/content-management/AttachPracticeFlow";
import { AttachProgramPracticeFlow } from "../pages/content-management/AttachProgramPracticeFlow";
import { ProgramTypeSelectionPage } from "../pages/content-management/ProgramTypeSelectionPage";
import { ProgramDetailPage } from "../pages/content-management/ProgramDetailPage";
import { CourseManagementPage } from "../pages/content-management/CourseManagementPage";
import { UnitManagementPage } from "../pages/content-management/UnitManagementPage";
import { NotFoundPage } from "../pages/NotFoundPage"; import { NotFoundPage } from "../pages/NotFoundPage";
import { NotificationsPage } from "../pages/notifications/NotificationsPage"; import { NotificationsPage } from "../pages/notifications/NotificationsPage";
import { CreateNotificationPage } from "../pages/notifications/CreateNotificationPage"; import { CreateNotificationPage } from "../pages/notifications/CreateNotificationPage";
@ -154,6 +161,34 @@ export function AppRoutes() {
</Route> </Route>
<Route path="/new-content" element={<NewContentPage />} /> <Route path="/new-content" element={<NewContentPage />} />
<Route
path="/new-content/courses"
element={<ProgramTypeSelectionPage />}
/>
<Route
path="/new-content/courses/:programType"
element={<ProgramDetailPage />}
/>
<Route
path="/new-content/courses/:programType/attach-practice"
element={<AttachProgramPracticeFlow />}
/>
<Route
path="/new-content/courses/:programType/:courseId/unit/:unitId/module/:moduleId/attach-practice"
element={<AttachPracticeFlow />}
/>
<Route
path="/new-content/courses/:programType/:courseId"
element={<CourseManagementPage />}
/>
<Route
path="/new-content/courses/:programType/:courseId/:unitId"
element={<UnitManagementPage />}
/>
<Route
path="/new-content/courses/:programType/:courseId/:unitId/:moduleId"
element={<CourseModuleDetailPage />}
/>
<Route <Route
path="/new-content/learn-english" path="/new-content/learn-english"
element={<LearnEnglishPage />} element={<LearnEnglishPage />}

View File

@ -1,12 +1,12 @@
import * as React from "react" import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react" import { X } from "lucide-react";
import { cn } from "../../lib/utils" import { cn } from "../../lib/utils";
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -20,8 +20,8 @@ const DialogOverlay = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -38,27 +38,42 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute right-6 top-10 rounded-sm opacity-60 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className="h-6 w-6" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)) ));
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} /> className,
) ...props
DialogHeader.displayName = "DialogHeader" }: React.HTMLAttributes<HTMLDivElement>) => (
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props} {...props}
/> />
) );
DialogFooter.displayName = "DialogFooter" DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@ -66,11 +81,14 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)} className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props} {...props}
/> />
)) ));
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@ -81,8 +99,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)) ));
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { export {
Dialog, Dialog,
@ -95,5 +113,4 @@ export {
DialogFooter, DialogFooter,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
} };

View File

@ -9,7 +9,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-10 w-full rounded-lg border bg-white px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-grayScale-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-[6px] border bg-white px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-grayScale-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className, className,
)} )}
ref={ref} ref={ref}

View File

@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react";
import { ChevronDown } from "lucide-react" import { ChevronDown } from "lucide-react";
import { cn } from "../../lib/utils" import { cn } from "../../lib/utils";
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {} export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
@ -18,10 +18,9 @@ export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
> >
{children} {children}
</select> </select>
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-grayScale-400" /> <ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-grayScale-600" />
</div> </div>
) );
}, },
) );
Select.displayName = "Select" Select.displayName = "Select";

View File

@ -18,15 +18,18 @@ export function Stepper({ steps, currentStep, className }: StepperProps) {
key={step} key={step}
className="flex-1 relative flex flex-col items-center group" className="flex-1 relative flex flex-col items-center group"
> >
{/* Connector Line (Behind) */} {/* Connector Line - floats between circles with gap on both sides */}
{index < steps.length - 1 && ( {index < steps.length - 1 && (
<div className="absolute left-1/2 w-[100%] mx-auto top-5 h-[2px] bg-grayScale-200 z-0" /> <div
className="absolute top-4 h-[1.5px] bg-grayScale-200 z-0"
style={{ left: "calc(50% + 24px)", right: "calc(-50% + 24px)" }}
/>
)} )}
{/* Circle */} {/* Circle */}
<div <div
className={cn( className={cn(
"relative z-10 grid h-10 w-10 place-items-center rounded-full border-2 text-sm font-bold transition-all duration-300 mb-3", "relative z-10 grid h-8 w-8 place-items-center rounded-full border-2 text-sm font-bold transition-all duration-300 mb-3",
isCurrent isCurrent
? "border-brand-500 bg-brand-500 text-white shadow-md scale-110" ? "border-brand-500 bg-brand-500 text-white shadow-md scale-110"
: "border-grayScale-100 bg-white text-grayScale-400 font-medium", : "border-grayScale-100 bg-white text-grayScale-400 font-medium",
@ -38,7 +41,7 @@ export function Stepper({ steps, currentStep, className }: StepperProps) {
{/* Label */} {/* Label */}
<span <span
className={cn( className={cn(
"relative z-10 text-[13px] font-bold transition-colors duration-300", "relative z-10 text-[12px] font-bold transition-colors duration-300",
isCurrent ? "text-brand-500" : "text-grayScale-400 font-medium", isCurrent ? "text-brand-500" : "text-grayScale-400 font-medium",
)} )}
> >

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@ export function AddPracticeFlow() {
const moduleId = searchParams.get("moduleId"); const moduleId = searchParams.get("moduleId");
const isModuleContext = backTo === "module"; const isModuleContext = backTo === "module";
const isCourseContext = backTo === "modules";
const backLabel = const backLabel =
backTo === "module" backTo === "module"
@ -73,7 +74,7 @@ export function AddPracticeFlow() {
if (isPublished) { if (isPublished) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen px-4 text-center pb-20 animate-in fade-in zoom-in duration-500 bg-white"> <div className="flex flex-col items-center justify-center min-h-screen px-4 text-center pb-20 animate-in fade-in zoom-in duration-500">
<div className="mb-10 relative"> <div className="mb-10 relative">
<div className="absolute inset-0 bg-brand-500/10 blur-3xl rounded-full" /> <div className="absolute inset-0 bg-brand-500/10 blur-3xl rounded-full" />
<img <img
@ -82,16 +83,16 @@ export function AddPracticeFlow() {
className="h-[128px] w-[128px] relative" className="h-[128px] w-[128px] relative"
/> />
</div> </div>
<h1 className="text-[32px] font-bold text-grayScale-900 mb-4"> <h1 className="text-[28px] font-bold text-grayScale-900 mb-2">
Practice Published Successfully! Practice Published Successfully!
</h1> </h1>
<p className="text-grayScale-600 text-lg mb-14 max-w-lg font-medium leading-relaxed"> <p className="text-grayScale-600 text-md mb-14 max-w-lg font-medium leading-relaxed">
Your speaking practice is now active and available inside the module. Your speaking practice is now active and available inside the module.
</p> </p>
<div className="flex flex-col gap-4 w-full max-w-[400px]"> <div className="flex flex-col gap-4 w-full max-w-[400px]">
<Button <Button
onClick={() => navigate(backPath)} onClick={() => navigate(backPath)}
className="h-14 rounded-2xl bg-brand-500 font-bold shadow-xl shadow-brand-500/20 text-[17px] text-white hover:bg-brand-600 transition-all active:scale-95" className="h-14 rounded-[6px] bg-[#9E2891] font-bold shadow-xl shadow-brand-500/20 text-[16px] text-white "
> >
Go back to Module Go back to Module
</Button> </Button>
@ -106,7 +107,7 @@ export function AddPracticeFlow() {
}); });
}} }}
variant="outline" variant="outline"
className="h-14 rounded-2xl border-brand-200 text-brand-500 font-bold hover:bg-brand-50 transition-all text-[17px] bg-white" className="h-14 rounded-[6px] border-[#9E2891] text-[#9E2891] font-semibold text-[16px] bg-white "
> >
Add Another Practice Add Another Practice
</Button> </Button>
@ -128,6 +129,7 @@ export function AddPracticeFlow() {
navigate={navigate} navigate={navigate}
level={level!} level={level!}
isModuleContext={isModuleContext} isModuleContext={isModuleContext}
isCourseContext={isCourseContext}
/> />
); );
case 2: case 2:
@ -182,6 +184,7 @@ export function AddPracticeFlow() {
navigate={navigate} navigate={navigate}
level={level!} level={level!}
isModuleContext={isModuleContext} isModuleContext={isModuleContext}
isCourseContext={isCourseContext}
/> />
); );
case 2: case 2:
@ -219,40 +222,46 @@ export function AddPracticeFlow() {
}; };
return ( return (
<div className="space-y-8 pb-32 px-6 pt-6 min-h-screen bg-[#F8FAFC]"> <div className="space-y-8 pb-32 px-6 pt-6 min-h-screen ">
{/* Header */} {/* Header */}
<div className="mx-auto max-w-7xl w-full"> <div className="mx-auto max-w-7xl w-full">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<Link <Link
to={backPath} to={backPath}
className="flex items-center gap-2 text-[15px] font-medium text-grayScale-500 transition-colors hover:text-brand-500 decoration-none" className="flex items-center gap-2 text-[15px] font-medium text-grayScale-600 transition-colors hover:text-brand-500 decoration-none"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
{backLabel} {backLabel}
</Link> </Link>
<Button
variant="outline"
className="rounded-[8px] border-grayScale-200 text-grayScale-600 h-10 px-6 font-bold bg-white hover:bg-grayScale-50"
onClick={() => navigate(backPath)}
>
Cancel
</Button>
</div> </div>
<div className="space-y-4 mb-10"> <div className=" mb-10">
<h1 className="text-4xl font-bold text-[#0F172A]"> <div className="flex items-center justify-between">
Add New Practice <h1 className="text-3xl font-bold text-[#0F172A]">
</h1> Add New Practice
<p className="text-grayScale-400 text-lg"> </h1>
<Button
variant="outline"
className="rounded-[8px] border-grayScale-200 text-grayScale-600 h-10 px-6 font-bold bg-white hover:bg-grayScale-50"
onClick={() => navigate(backPath)}
>
Cancel
</Button>
</div>
<p className="text-grayScale-400 text-base">
Create a new immersive practice session for students. Create a new immersive practice session for students.
</p> </p>
</div> </div>
<div className="mx-auto max-w-4xl mb-12"> <div className="mx-auto w-[70%] mb-12">
<Stepper steps={flowSteps} currentStep={currentStep} /> <Stepper steps={flowSteps} currentStep={currentStep} />
</div> </div>
<div className="mx-auto max-w-4xl">{renderStep()}</div> <div
className={`mx-auto ${(!isModuleContext && currentStep === 3) || (isModuleContext && currentStep === 2) || currentStep === 5 ? "max-w-6xl" : "max-w-4xl"}`}
>
{renderStep()}
</div>
</div> </div>
</div> </div>
); );

View File

@ -6,6 +6,7 @@ import { Stepper } from "../../components/ui/stepper";
import { VideoDetailStep } from "./components/video-steps/VideoDetailStep"; import { VideoDetailStep } from "./components/video-steps/VideoDetailStep";
import { ReviewPublishStep } from "./components/video-steps/ReviewPublishStep"; import { ReviewPublishStep } from "./components/video-steps/ReviewPublishStep";
import successIcon from "../../assets/success.svg";
const STEPS = [ const STEPS = [
{ id: 1, label: "Video Detail" }, { id: 1, label: "Video Detail" },
@ -37,43 +38,31 @@ export function AddVideoFlow() {
if (isPublished) { if (isPublished) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen px-4 text-center pb-20 animate-in fade-in zoom-in duration-500 bg-white"> <div className="flex flex-col items-center justify-center min-h-screen px-4 text-center pb-20 animate-in fade-in zoom-in duration-500 ">
{/* Success Icon Wrapper (Jagged Circle Style) */} {/* Success Icon Wrapper (Jagged Circle Style) */}
<div className="mb-12 relative scale-110"> <div className="mb-12 relative scale-110">
<div className="absolute inset-0 bg-brand-500/5 blur-3xl rounded-full" /> <div className="absolute inset-0 bg-brand-500/5 blur-3xl rounded-full" />
<div className="relative"> <div className="relative">
<div <div className="absolute inset-0 bg-brand-500/10 blur-3xl rounded-full" />
className="h-24 w-24 bg-brand-500 flex items-center justify-center" <img
style={{ src={successIcon}
clipPath: alt="Success"
"polygon(50% 0%, 61% 10%, 75% 10%, 80% 24%, 94% 30%, 90% 44%, 100% 56%, 90% 68%, 94% 82%, 80% 88%, 75% 100%, 61% 100%, 50% 90%, 39% 100%, 25% 100%, 20% 88%, 6% 82%, 10% 68%, 0% 56%, 10% 44%, 6% 30%, 20% 24%, 25% 10%, 39% 10%)", className="h-[128px] w-[128px] relative"
}}
>
<Check className="h-12 w-12 text-white stroke-[4px]" />
</div>
{/* Sub-Jagged layer for depth if needed */}
<div
className="absolute inset-0 bg-brand-500/20 scale-110 -z-10"
style={{
clipPath:
"polygon(50% 0%, 61% 10%, 75% 10%, 80% 24%, 94% 30%, 90% 44%, 100% 56%, 90% 68%, 94% 82%, 80% 88%, 75% 100%, 61% 100%, 50% 90%, 39% 100%, 25% 100%, 20% 88%, 6% 82%, 10% 68%, 0% 56%, 10% 44%, 6% 30%, 20% 24%, 25% 10%, 39% 10%)",
opacity: 0.3,
}}
/> />
</div> </div>
</div> </div>
<h1 className="text-[32px] font-bold text-grayScale-900 mb-4"> <h1 className="text-[26px] font-bold text-grayScale-900 mb-4">
Video Published Successfully! Video Published Successfully!
</h1> </h1>
<p className="text-grayScale-600 text-lg mb-14 max-w-lg font-medium leading-relaxed"> <p className="text-grayScale-600 text-base mb-14 max-w-lg font-medium leading-relaxed">
Your video is now live and available inside the selected module. Your video is now live and available inside the selected module.
</p> </p>
<div className="flex flex-col gap-4 w-full max-w-[400px]"> <div className="flex flex-col gap-4 w-full max-w-[400px]">
<Button <Button
onClick={() => navigate(`/new-content/learn-english/${level}`)} onClick={() => navigate(`/new-content/learn-english/${level}`)}
className="h-14 rounded-2xl bg-brand-500 font-bold shadow-xl shadow-brand-500/20 text-[17px] text-white hover:bg-brand-600 transition-all active:scale-95" className="h-12 rounded-[6px] bg-brand-500 font-bold text-[17px] text-white transition-all active:scale-95"
> >
Go back to Learn English Go back to Learn English
</Button> </Button>
@ -90,7 +79,7 @@ export function AddVideoFlow() {
setCurrentStep(1); setCurrentStep(1);
}} }}
variant="outline" variant="outline"
className="h-14 rounded-2xl border-brand-200 text-brand-500 font-bold hover:bg-brand-50 transition-all text-[17px] active:scale-95 bg-white" className="h-12 rounded-[6px] border-brand-200 text-brand-500 font-bold text-[17px] active:scale-95 bg-white"
> >
Add Another Video Add Another Video
</Button> </Button>
@ -100,7 +89,7 @@ export function AddVideoFlow() {
} }
return ( return (
<div className="space-y-8 pb-32 px-6 pt-6 min-h-screen bg-[#F8FAFC]"> <div className="space-y-8 pb-32 px-6 pt-6 min-h-screen ">
{/* Header */} {/* Header */}
<div className="mx-auto max-w-7xl w-full"> <div className="mx-auto max-w-7xl w-full">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
@ -120,7 +109,7 @@ export function AddVideoFlow() {
</Button> </Button>
</div> </div>
<h1 className="text-4xl font-bold text-[#0F172A] mb-10"> <h1 className="text-2xl font-bold text-[#0F172A] mb-10">
Add New Video Add New Video
</h1> </h1>

View File

@ -0,0 +1,193 @@
import { useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, Clock, FileVideo, Check } from "lucide-react";
import { Button } from "../../components/ui/button";
import { Stepper } from "../../components/ui/stepper";
import successIcon from "../../assets/success.svg";
import { AttachPracticeStep1 } from "./components/practice-steps/AttachPracticeStep1";
import { AttachPracticeReviewStep } from "./components/practice-steps/AttachPracticeReviewStep";
export function AttachPracticeFlow() {
const navigate = useNavigate();
const { programType, courseId, unitId, moduleId } = useParams<{
programType: string;
courseId: string;
unitId: string;
moduleId: string;
}>();
const backPath = `/new-content/courses/${programType}/${courseId}/${unitId}/${moduleId}`;
const [currentStep, setCurrentStep] = useState(1);
const [isPublished, setIsPublished] = useState(false);
const [formData, setFormData] = useState({
program:
programType === "skill"
? "Skill-Based Courses"
: "English Proficiency Exams",
module: "Module 4: Interactive Speaking",
video: "Intro to Interactive Speaking",
questionType: "speaking",
version: "v1",
});
const steps = ["Set Video", "Review & Publish"];
const nextStep = () =>
setCurrentStep((prev) => Math.min(prev + 1, steps.length));
const prevStep = () => setCurrentStep((prev) => Math.max(prev - 1, 1));
if (isPublished) {
return (
<div className="flex flex-col items-center justify-center min-h-screen px-4 text-center pb-20 animate-in fade-in zoom-in duration-700 ">
{/* Scalloped Success Icon */}
<div className="mb-10 relative">
<div className="absolute inset-0 bg-brand-500/10 blur-3xl rounded-full" />
<img
src={successIcon}
alt="Success"
className="h-[128px] w-[128px] relative"
/>
</div>
<h1 className="text-[28px] font-bold text-grayScale-900 mb-2">
Practice Attached Successfully!
</h1>
<p className="text-grayScale-600 text-md mb-14 max-w-2xl font-medium leading-relaxed">
The practice has been successfully linked to a video{" "}
<span className="text-[#9E2891]">{formData.video}</span>
</p>
{/* Video Info Card */}
<div className="w-full max-w-[600px] bg-[#9E289114] border border-[#9E2891] rounded-[12px] p-4 flex items-center justify-between mb-16 shadow-sm">
<div className="flex items-center gap-5">
<div className="h-[60px] w-[120px] rounded-xl overflow-hidden shadow-inner flex-shrink-0">
<img
src="https://images.unsplash.com/photo-1557425955-df376b5903c8?auto=format&fit=crop&q=80&w=400"
alt="Video Thumbnail"
className="w-full h-full object-cover"
/>
</div>
<div className="text-left space-y-1.5">
<h4 className="text-[14px] font-medium text-grayScale-900">
Intro to IELTS Speaking Part 1
</h4>
<div className="flex items-center gap-3 text-grayScale-400 font-medium text-[12px]">
<div className="flex items-center gap-1.5 uppercase tracking-wide">
<Clock className="h-3 w-3" />
10:42 mins
</div>
<span></span>
<div className="flex items-center gap-1.5 uppercase tracking-wide">
MP4
</div>
</div>
</div>
</div>
<div className="pr-4">
<Check className="h-5 w-5 text-[#9E2891] stroke-[3px]" />
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col gap-4 w-full max-w-[440px]">
<Button
onClick={() => navigate(backPath)}
className="h-14 rounded-[6px] bg-[#9E2891] font-bold shadow-xl shadow-brand-500/20 text-[16px] text-white"
>
Go back to Videos
</Button>
<Button
variant="outline"
onClick={() => {
setIsPublished(false);
setCurrentStep(1);
}}
className="h-14 rounded-[6px] border-[#9E2891] text-[#9E2891] font-semibold text-[16px] bg-white"
>
Attach More Practice
</Button>
</div>
</div>
);
}
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<AttachPracticeStep1
formData={formData}
setFormData={setFormData}
nextStep={nextStep}
onCancel={() => navigate(backPath)}
/>
);
case 2:
return (
<AttachPracticeReviewStep
formData={formData}
prevStep={prevStep}
onPublish={() => setIsPublished(true)}
/>
);
default:
return null;
}
};
const title =
currentStep === 1 ? "Attach Practice to a Video" : "Review & Publish";
const description =
currentStep === 1
? "Create a new immersive practice session for a video."
: "Verify practice details before publishing it.";
return (
<div className="space-y-8 pb-32 px-6 pt-10 min-h-screen animate-in fade-in duration-500">
<div className="mx-auto w-full">
{/* Navigation Breadcrumb */}
<div className="flex items-center justify-between mb-12">
<Link
to={backPath}
className="flex items-center gap-2 text-[15px] font-bold text-grayScale-600 transition-colors hover:text-brand-500 group"
>
<ArrowLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
Back to Videos
</Link>
</div>
{/* Stepper Area */}
<div className="mb-20 w-full pointer-events-none">
<Stepper steps={steps} currentStep={currentStep} />
</div>
{/* Page Title & Header Actions */}
<div className="mb-10 flex items-start justify-between">
<div className="">
<h1 className="text-[30px] font-bold text-[#0D1421] ">{title}</h1>
<p className="text-grayScale-400 text-[16px] font-medium ">
{description}
</p>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
className="h-10 px-8 rounded-[6px] border-grayScale-100 text-grayScale-600 font-bold bg-white hover:bg-grayScale-50 shadow-sm"
onClick={() => navigate(backPath)}
>
Cancel
</Button>
<Button className="h-10 px-8 rounded-[6px] bg-[#9E2891] font-bold text-white shadow-md hover:bg-[#8A237E] transition-all">
Save as Draft
</Button>
</div>
</div>
{/* Form Content */}
<div className="w-full">{renderStep()}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,157 @@
import { useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, Check } from "lucide-react";
import { Button } from "../../components/ui/button";
import { Stepper } from "../../components/ui/stepper";
import successIcon from "../../assets/success.svg";
import { ProgramAttachStep1 } from "./components/practice-steps/ProgramAttachStep1";
import { ProgramAttachReviewStep } from "./components/practice-steps/ProgramAttachReviewStep";
export function AttachProgramPracticeFlow() {
const navigate = useNavigate();
const { programType } = useParams<{ programType: string }>();
const backPath = `/new-content/courses/${programType}`;
const [currentStep, setCurrentStep] = useState(1);
const [isPublished, setIsPublished] = useState(false);
const [formData, setFormData] = useState({
program: "English Proficiency Exams",
test: "Mock Exam 1",
questionType: "Speaking Practice",
version: "V 1.0",
});
const steps = ["Set Program", "Review & Publish"];
const nextStep = () =>
setCurrentStep((prev) => Math.min(prev + 1, steps.length));
const prevStep = () => setCurrentStep((prev) => Math.max(prev - 1, 1));
if (isPublished) {
return (
<div className="flex flex-col items-center justify-center min-h-screen px-4 text-center pb-20 animate-in fade-in zoom-in duration-700 bg-white">
{/* Scalloped Success Icon */}
<div className="mb-10 relative">
<div className="absolute inset-0 bg-brand-500/10 blur-3xl rounded-full" />
<img
src={successIcon}
alt="Success"
className="h-[128px] w-[128px] relative"
/>
</div>
<h1 className="text-[28px] font-bold text-grayScale-900 mb-2">
Practice Attached Successfully!
</h1>
<p className="text-grayScale-600 text-md mb-14 max-w-lg font-medium leading-relaxed">
The practice has been successfully linked to the program{" "}
<span className="text-[#9E2891]">{formData.program}</span>
</p>
<div className="flex flex-col gap-4 w-full max-w-[440px]">
<Button
onClick={() => navigate(backPath)}
className="h-14 rounded-[12px] bg-[#9E2891] font-bold shadow-xl shadow-brand-500/20 text-[16px] text-white "
>
Go back to Program
</Button>
<Button
variant="outline"
onClick={() => {
setIsPublished(false);
setCurrentStep(1);
}}
className="h-14 rounded-[12px] border-[#9E2891] text-[#9E2891] font-bold text-[16px] bg-white "
>
Attach More Practice
</Button>
</div>
</div>
);
}
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<ProgramAttachStep1
formData={formData}
setFormData={setFormData}
nextStep={nextStep}
onCancel={() => navigate(backPath)}
/>
);
case 2:
return (
<ProgramAttachReviewStep
formData={formData}
prevStep={prevStep}
onPublish={() => setIsPublished(true)}
onCancel={() => navigate(backPath)}
/>
);
default:
return null;
}
};
const title =
currentStep === 1 ? "Attach Practice to a program" : "Review & Publish";
const description =
currentStep === 1
? "Create a new immersive practice session for a video."
: "Verify practice details before publishing it.";
return (
<div className="space-y-8 px-6 pt-10 min-h-screen animate-in fade-in duration-500">
<div className=" w-full">
{/* Navigation Breadcrumb */}
<div className="flex items-center justify-between mb-12">
<Link
to={backPath}
className="flex items-center gap-2 text-[15px] font-bold text-grayScale-600 transition-colors hover:text-brand-500 group"
>
<ArrowLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
Back to Program
</Link>
</div>
{/* Stepper Area */}
<div className="mb-20 pointer-events-none">
<Stepper steps={steps} currentStep={currentStep} />
</div>
{/* Page Title & Header Actions */}
<div className="mb-10 flex items-start justify-between">
<div className="">
<h1 className="text-[30px] font-bold text-[#0D1421] ">{title}</h1>
<p className="text-grayScale-500 text-[14px]">{description}</p>
</div>
<div className="flex items-center gap-3">
<Button
variant="outline"
className="h-10 px-8 rounded-[6px] border-grayScale-100 text-grayScale-600 font-bold bg-white hover:bg-grayScale-50 shadow-sm"
onClick={() => navigate(backPath)}
>
Cancel
</Button>
<Button className="h-10 px-8 rounded-[6px] bg-[#9E2891] font-bold text-white shadow-md hover:bg-[#8A237E] transition-all">
Save as Draft
</Button>
</div>
</div>
{/* Form Content */}
<div
className={`w-full mx-auto ${
currentStep === 1 ? "max-w-4xl" : "max-w-none"
}`}
>
{renderStep()}
</div>
</div>
</div>
);
}

View File

@ -41,7 +41,7 @@ export function CourseDetailPage() {
const [isAddModuleOpen, setIsAddModuleOpen] = useState(false); const [isAddModuleOpen, setIsAddModuleOpen] = useState(false);
return ( return (
<div className="space-y-10 pb-20"> <div className="space-y-10 pb-20 pt-10">
{/* Header Navigation */} {/* Header Navigation */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link <Link
@ -55,11 +55,11 @@ export function CourseDetailPage() {
{/* Hero Section */} {/* Hero Section */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6"> <div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div className="space-y-2"> <div className="">
<h1 className="text-4xl font-extrabold text-grayScale-900 tracking-tight"> <h1 className="text-2xl font-medium text-grayScale-900 tracking-tight">
{courseId?.toUpperCase() || "A1"} {courseId?.toUpperCase() || "A1"}
</h1> </h1>
<p className="text-grayScale-500 text-lg max-w-2xl font-medium"> <p className="text-grayScale-500 text-sm max-w-2xl font-medium">
Learn basic English words, phrases, and simple sentences for daily Learn basic English words, phrases, and simple sentences for daily
situations. situations.
</p> </p>
@ -67,37 +67,51 @@ export function CourseDetailPage() {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button <Button
variant="outline" variant="outline"
className="h-12 px-6 rounded-[6px] border-brand-500 text-brand-500 font-bold transition-all gap-2" className="rounded-[6px] border-brand-500 text-brand-500 "
onClick={() => onClick={() =>
navigate( navigate(
`/new-content/learn-english/${level}/courses/add-practice?backTo=modules&courseId=${courseId}`, `/new-content/learn-english/${level}/courses/add-practice?backTo=modules&courseId=${courseId}`,
) )
} }
> >
<Calendar className="h-5 w-5" /> <Calendar className="h-4 w-4" />
Add Practice Add Practice
</Button> </Button>
<Button <Button
className="h-12 px-6 rounded-[6px] bg-brand-500 font-bold shadow-lg shadow-brand-500/20 transition-all gap-2" className="rounded-[6px] bg-brand-500 font-semibold hover:bg-brand-600"
onClick={() => setIsAddModuleOpen(true)} onClick={() => setIsAddModuleOpen(true)}
> >
<Plus className="h-5 w-5" /> <Plus className="h-4 w-4" />
Add Module Add Module
</Button> </Button>
</div> </div>
</div> </div>
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
<AddModuleModal <AddModuleModal
isOpen={isAddModuleOpen} isOpen={isAddModuleOpen}
onClose={() => setIsAddModuleOpen(false)} onClose={() => setIsAddModuleOpen(false)}
/> />
{/* Gradient Divider */}
{/* Gradient Grid */} {/* Gradient Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="flex flex-warp gap-10">
{MODULES.map((module) => ( {MODULES.map((module) => (
<Card <Card
key={module.id} key={module.id}
className="group overflow-hidden border border-grayScale-50 shadow-sm hover:shadow-lg transition-all duration-300 rounded-[16px] bg-white flex flex-col h-full" className="group overflow-hidden border w-[330px] border-grayScale-50 shadow-sm hover:shadow-lg transition-all duration-300 rounded-[16px] bg-white flex flex-col h-full"
> >
{/* Gradient Banner */} {/* Gradient Banner */}
<div <div
@ -107,19 +121,23 @@ export function CourseDetailPage() {
)} )}
/> />
<div className="p-2 pb-4 pt-8 flex-1 flex flex-col"> <div className="p-2 pb-4 pt-4 flex-1 flex flex-col">
<div className="flex gap-4 mb-8"> <div className="flex gap-4 mb-8">
{/* Icon Circle */} {/* Icon Circle */}
<div className="h-12 w-12 rounded-full bg-[#f3e8ff] flex items-center justify-center p-3 flex-shrink-0 border border-purple-100/50"> <div
<module.icon className="h-6 w-6 text-brand-500" /> className={`h-12 w-12 rounded-full ${module.id === "m2" ? "bg-[#F8FAFC]" : "bg-[#f3e8ff]"} flex items-center justify-center p-3 flex-shrink-0 border border-purple-100/50`}
>
<module.icon
className={`h-6 w-6 ${module.id === "m2" ? "text-[#64748B]" : "text-brand-500"}`}
/>
</div> </div>
{/* Content */} {/* Content */}
<div className="space-y-1"> <div className="space-y-1">
<h3 className="text-xl font-bold text-[#0F172A] tracking-tight"> <h3 className="text-lg font-bold text-[#0F172A] tracking-tight">
{module.title} {module.title}
</h3> </h3>
<p className="text-grayScale-400 font-medium leading-normal text-[14px]"> <p className="text-grayScale-400 font-medium text-[12px]">
{module.description} {module.description}
</p> </p>
</div> </div>
@ -129,7 +147,7 @@ export function CourseDetailPage() {
<div className="flex items-center gap-3 mt-auto"> <div className="flex items-center gap-3 mt-auto">
<Button <Button
variant="outline" variant="outline"
className="flex-1 h-12 rounded-[6px] border-[#9E2891] text-[#9E2891] font-bold transition-all text-sm" className="flex-1 h-10 rounded-[6px] border-[#9E2891] text-[#9E2891] transition-all text-sm"
onClick={() => onClick={() =>
navigate( navigate(
`/new-content/learn-english/${level}/courses/${courseId}/modules/${module.id}`, `/new-content/learn-english/${level}/courses/${courseId}/modules/${module.id}`,
@ -141,12 +159,12 @@ export function CourseDetailPage() {
{module.status === "Published" ? ( {module.status === "Published" ? (
<Button <Button
disabled disabled
className="flex-1 h-12 rounded-[6px] bg-[#D291BC] text-white font-bold opacity-100 cursor-default border-none shadow-none text-sm" className="flex-1 h-10 rounded-[6px] bg-[#D291BC] text-white opacity-100 cursor-default border-none shadow-none text-sm"
> >
Published Published
</Button> </Button>
) : ( ) : (
<Button className="flex-1 h-12 rounded-[6px] bg-brand-500 text-white font-bold shadow-md shadow-brand-500/10 text-sm"> <Button className="flex-1 h-10 rounded-[6px] bg-brand-500 text-white shadow-md shadow-brand-500/10 text-sm">
Publish Practice Publish Practice
</Button> </Button>
)} )}

View File

@ -0,0 +1,264 @@
import { Link, useParams, useNavigate } from "react-router-dom";
import {
ArrowLeft,
Plus,
FileText,
LayoutGrid,
PlayCircle,
ClipboardCheck,
ChevronRight,
ArrowRight,
X,
} from "lucide-react";
import { Button } from "../../components/ui/button";
import { Card } from "../../components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import uploadIcon from "../../assets/icons/upload.png";
export function CourseManagementPage() {
const navigate = useNavigate();
const { programType, courseId } = useParams<{
programType: string;
courseId: string;
}>();
// Mock data for display titles
const courseTitles: Record<string, string> = {
duolingo: "Duolingo English Test",
ielts: "IELTS Academic",
};
const courseDisplayName =
courseTitles[courseId || ""] || "Duolingo English Test";
const units = [
{
id: "unit1",
name: "Greetings & Introductions",
description:
"Learn basic greetings, self-introductions, and polite expressions in everyday situations.",
modules: 3,
videos: 9,
practices: 9,
gradient:
"linear-gradient(135deg, rgba(158, 40, 145, 0.5) 0%, rgba(158, 40, 145, 0.8) 100%)",
},
{
id: "unit2",
name: "Speaking",
description:
"Core speaking practice and skill building for natural pronunciation and fluency.",
modules: 3,
videos: 9,
practices: 9,
gradient:
"linear-gradient(135deg, rgba(79, 70, 229, 0.5) 0%, rgba(79, 70, 229, 0.8) 100%)",
},
{
id: "unit3",
name: "Reading",
description:
"Reading comprehension and vocabulary improvement through various text types.",
modules: 3,
videos: 9,
practices: 9,
gradient:
"linear-gradient(135deg, rgba(124, 58, 237, 0.5) 0%, rgba(124, 58, 237, 0.8) 100%)",
},
];
return (
<div className="space-y-8 animate-in fade-in duration-500 pb-10">
{/* Navigation */}
<Link
to={`/new-content/courses/${programType}`}
className="flex items-center gap-2.5 text-[15px] font-semibold text-grayScale-600 hover:text-brand-500 transition-colors pt-4 group"
>
<ArrowLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
Back to Courses
</Link>
{/* Header section */}
<div className="flex items-start justify-between">
<div className="space-y-2">
<h1 className="text-[28px] font-medium tracking-tight text-grayScale-900">
{courseDisplayName}
</h1>
<p className="max-w-2xl text-[15px] font-medium leading-relaxed text-grayScale-500">
Manage units and modules inside the {courseDisplayName}
</p>
</div>
<div className="flex items-center gap-3 pt-2">
<Dialog>
<DialogTrigger asChild>
<Button className="h-10 px-6 rounded-[6px] bg-brand-500 font-bold text-white shadow-sm hover:bg-brand-600 transition-all flex items-center gap-2">
<Plus className="h-5 w-5" />
Add Unit
</Button>
</DialogTrigger>
<DialogContent className="max-w-[600px] p-0 border-none rounded-[16px] overflow-hidden">
<div className="bg-white">
<DialogHeader className="px-8 py-6 border-b border-grayScale-200 flex flex-row items-center justify-between">
<DialogTitle className="text-[20px] font-bold relative top-2 text-grayScale-900">
Create Courses
</DialogTitle>
</DialogHeader>
<div className="p-8 space-y-8">
<div className="space-y-3">
<label className="text-[15px] text-grayScale-800">
Unit Name
</label>
<Input
placeholder="e.g. Reading"
className="h-12 border-grayScale-400 rounded-[8px] px-4 placeholder:text-grayScale-400 text-[15px] focus:ring-brand-500/20"
/>
</div>
<div className="space-y-3">
<label className="text-[15px] text-grayScale-800">
Thumbnail
</label>
<div className="relative group cursor-pointer">
<div className="flex flex-col items-center justify-center rounded-[12px] border-2 border-dashed border-grayScale-400 bg-white py-8 px-10 transition-all ">
<div className="mb-4">
<img
src={uploadIcon}
alt="Upload icon"
className="h-10 w-10"
/>
</div>
<p className="text-[15px]">
<span className="text-brand-500 font-bold hover:underline">
Click to upload
</span>{" "}
<span className="text-grayScale-500">
or drag and drop
</span>
</p>
<p className="mt-1.5 text-[12px] text-grayScale-400 uppercase tracking-widest">
JPG, PNG (MAX 1 MB)
</p>
</div>
</div>
</div>
</div>
<div className="px-8 py-6 bg-grayScale-50/30 border-t border-grayScale-50 flex justify-end gap-3">
<DialogClose asChild>
<Button
variant="outline"
className="h-11 px-8 rounded-[8px] border-grayScale-200 text-grayScale-700 font-bold"
>
Cancel
</Button>
</DialogClose>
<Button className="h-11 px-8 rounded-[8px] bg-brand-500 text-white font-bold hover:bg-brand-600">
Create Courses
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Button
variant="outline"
className="h-10 px-6 rounded-[6px] border-brand-500 text-brand-500 font-bold hover:bg-brand-50 transition-all flex items-center gap-2"
onClick={() =>
navigate(`/new-content/courses/${programType}/attach-practice`)
}
>
<FileText className="h-5 w-5" />
Attach Practice
</Button>
</div>
</div>
{/* Horizontal Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
{/* Grid of Units */}
<div className="flex flex-wrap gap-4 pt-4">
{units.map((unit) => (
<Card
key={unit.id}
className="group flex w-[400px] flex-col h-full bg-white rounded-[12px] border border-grayScale-100 overflow-hidden shadow-sm hover:shadow-md transition-all"
>
{/* Gradient Header */}
<div
className="h-36 w-full transition-transform duration-500 "
style={{ background: unit.gradient }}
/>
<div className="p-4 flex flex-col flex-1 space-y-6">
<div className="space-y-3 flex-1">
<h3 className="text-[18px] font-medium text-grayScale-900 transition-colors">
{unit.name}
</h3>
<p className="text-[12px] text-grayScale-500 font-medium line-clamp-3">
{unit.description}
</p>
</div>
{/* Stats Pills */}
<div className="flex flex-wrap gap-3">
<div className="h-9 px-3 rounded-[6px] bg-grayScale-100 border border-grayScale-100 flex items-center gap-2 text-grayScale-600">
<LayoutGrid className="h-3.5 w-3.5 text-grayScale-400" />
<span className="text-[12px] font-bold">
{unit.modules} Modules
</span>
</div>
<div className="h-9 px-3 rounded-[6px] bg-grayScale-100 border border-grayScale-100 flex items-center gap-2 text-grayScale-600">
<PlayCircle className="h-3.5 w-3.5 text-grayScale-400" />
<span className="text-[12px] font-bold">
{unit.videos} Videos
</span>
</div>
<div className="h-9 px-3 rounded-[6px] bg-grayScale-100 border border-grayScale-100 flex items-center gap-2 text-grayScale-600">
<ClipboardCheck className="h-3.5 w-3.5 text-grayScale-400" />
<span className="text-[12px] font-bold">
{unit.practices} Practices
</span>
</div>
</div>
{/* Action Button */}
<Button
className="w-full h-10 bg-brand-500 text-white rounded-[6px] font-bold flex items-center justify-center gap-2 group/btn"
onClick={() =>
navigate(
`/new-content/courses/${programType}/${courseId}/${unit.id}`,
)
}
>
View Detail
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</Card>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,248 @@
import { useState } from "react";
import { ArrowLeft, Plus, FileText, MoreVertical, Edit2 } from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Button } from "../../components/ui/button";
import { cn } from "../../lib/utils";
import { Card } from "../../components/ui/card";
const MOCK_VIDEOS = [
{
id: "v1",
title: "1.1 Introduction to Formal Greetings",
duration: "08:45",
status: "Draft",
thumbnailColor: "bg-[#CBD5E1]",
},
{
id: "v2",
title: "1.2 Understanding Email Structure",
duration: "08:45",
status: "Published",
thumbnailColor: "bg-[#DBEAFE]",
},
{
id: "v3",
title: "1.3 Common Business Idioms",
duration: "08:45",
status: "Published",
thumbnailColor: "bg-[#FEF3C7]",
},
{
id: "v4",
title: "1.4 Video Conference Etiquette",
duration: "08:45",
status: "Published",
thumbnailColor: "bg-[#FCE7F3]",
},
];
const MOCK_PRACTICES = [
{
id: "p1",
title: "1.1 Conversation Practice",
duration: "08:45",
status: "Published",
thumbnailColor: "bg-[#E0F2FE]",
},
{
id: "p2",
title: "1.2 Roleplay Scenario",
duration: "08:45",
status: "Draft",
thumbnailColor: "bg-[#F0FDF4]",
},
];
export function CourseModuleDetailPage() {
const navigate = useNavigate();
const { programType, courseId, unitId, moduleId } = useParams<{
programType: string;
courseId: string;
unitId: string;
moduleId: string;
}>();
const [activeTab, setActiveTab] = useState<"video" | "practice">("video");
const [activeFilter, setActiveFilter] = useState("All");
const moduleTitle = "Module 1: Basic Phrases";
const moduleDescription = "Learn essential phrases for daily conversations.";
const content = activeTab === "video" ? MOCK_VIDEOS : MOCK_PRACTICES;
const filteredContent = content.filter((item) => {
if (activeFilter === "All") return true;
if (activeFilter === "Drafts") return item.status === "Draft";
return item.status === activeFilter;
});
return (
<div className="space-y-8 animate-in fade-in duration-500 pb-10">
{/* Navigation */}
<Link
to={`/new-content/courses/${programType}/${courseId}/${unitId}`}
className="flex items-center gap-2.5 text-[15px] font-bold text-grayScale-600 hover:text-brand-500 transition-colors pt-4 group"
>
<ArrowLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
Back to Modules
</Link>
{/* Header section */}
<div className="flex items-start justify-between">
<div className="space-y-2">
<h1 className="text-[32px] font-extrabold tracking-tight text-[#0D1421]">
{moduleTitle}
</h1>
<p className="max-w-2xl text-[16px] font-medium leading-relaxed text-grayScale-400">
{moduleDescription}
</p>
</div>
<div className="flex items-center gap-3 pt-2">
<Button
variant="outline"
className="h-10 px-6 rounded-[6px] border-brand-500 text-brand-500 font-bold hover:bg-brand-50 transition-all flex items-center gap-2 shadow-sm"
onClick={() =>
navigate(
`/new-content/courses/${programType}/${courseId}/unit/${unitId}/module/${moduleId}/attach-practice`,
)
}
>
<FileText className="h-5 w-5" />
Attach Practice
</Button>
<Button className="h-10 px-6 rounded-[6px] bg-brand-500 font-bold text-white shadow-md hover:bg-brand-600 transition-all flex items-center gap-2 text-[15px]">
<Plus className="h-5 w-5" />
Add Video
</Button>
</div>
</div>
{/* Tabs */}
<div className="flex gap-10 border-b border-grayScale-100">
<button
onClick={() => setActiveTab("video")}
className={cn(
"pb-4 text-[16px] font-bold transition-all relative px-2",
activeTab === "video"
? "text-brand-500 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[2px] after:bg-brand-500"
: "text-grayScale-400 hover:text-grayScale-600",
)}
>
Video
</button>
<button
onClick={() => setActiveTab("practice")}
className={cn(
"pb-4 text-[16px] font-bold transition-all relative px-2",
activeTab === "practice"
? "text-brand-500 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[2px] after:bg-brand-500"
: "text-grayScale-400 hover:text-grayScale-600",
)}
>
Practice
</button>
</div>
{/* Filter Bar */}
<div className="bg-white border border-grayScale-100 rounded-[16px] p-4 flex items-center gap-8 shadow-sm">
<div className="text-[12px] font-bold text-grayScale-300 uppercase tracking-widest pl-4">
STATUS:
</div>
<div className="flex items-center gap-2">
{["All", "Published", "Drafts", "Archived"].map((filter) => (
<button
key={filter}
onClick={() => setActiveFilter(filter)}
className={cn(
"px-5 py-2 rounded-full text-[13px] font-bold transition-all",
activeFilter === filter
? "bg-brand-500 text-white shadow-md shadow-brand-500/20"
: "bg-grayScale-100 text-grayScale-500 hover:bg-grayScale-200",
)}
>
{filter}
</button>
))}
</div>
</div>
{/* Grid of Content */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 pt-4">
{filteredContent.map((item) => (
<ContentCard key={item.id} {...item} />
))}
</div>
</div>
);
}
function ContentCard({
title,
duration,
status,
thumbnailColor,
}: {
title: string;
duration: string;
status: string;
thumbnailColor: string;
}) {
return (
<Card className="group flex flex-col bg-white rounded-[20px] border border-grayScale-50 overflow-hidden shadow-sm hover:shadow-xl hover:shadow-grayScale-400/5 transition-all">
{/* Thumbnail Area */}
<div className={cn("h-44 w-full relative", thumbnailColor)}>
<div className="absolute bottom-3 right-3 bg-black/60 text-white text-[11px] font-bold px-2 py-0.5 rounded backdrop-blur-sm">
{duration}
</div>
</div>
<div className="p-5 flex flex-col flex-1 space-y-5">
<div className="flex items-center justify-between">
<div
className={cn(
"px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider flex items-center gap-2 border",
status === "Published"
? "bg-[#F0FDF4] text-[#16A34A] border-[#DCFCE7]"
: "bg-grayScale-50 text-grayScale-400 border-grayScale-100",
)}
>
<div
className={cn(
"h-1.5 w-1.5 rounded-full",
status === "Published" ? "bg-[#16A34A]" : "bg-grayScale-300",
)}
/>
{status}
</div>
<button className="h-8 w-8 rounded-lg flex items-center justify-center text-grayScale-300 hover:text-grayScale-600 transition-colors">
<MoreVertical className="h-5 w-5" />
</button>
</div>
<h3 className="text-[14px] font-bold text-[#0F172A] line-clamp-2 leading-snug">
{title}
</h3>
<div className="pt-2 grid grid-cols-1 gap-2 mt-auto">
<Button
variant="outline"
className="w-full h-10 rounded-[10px] border-grayScale-200 text-grayScale-600 font-bold flex items-center justify-center gap-2 text-xs hover:bg-grayScale-25"
>
<Edit2 className="h-4 w-4" />
Edit
</Button>
<Button
className={cn(
"w-full h-10 rounded-[10px] font-bold text-xs shadow-sm",
status === "Published"
? "bg-[#ECD5E9] text-[#9E2891] hover:bg-[#EBD0E7]"
: "bg-brand-500 text-white hover:bg-brand-600",
)}
>
{status === "Published" ? "Published" : "Publish"}
</Button>
</div>
</div>
</Card>
);
}

View File

@ -52,7 +52,7 @@ export function LearnEnglishPage() {
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="h-11 rounded-xl bg-brand-500 px-6 font-semibold hover:bg-brand-600"> <Button className="h-11 rounded-[6px] bg-brand-500 px-6 font-semibold ">
<Plus className="mr-2 h-5 w-5" /> <Plus className="mr-2 h-5 w-5" />
Add Program Add Program
</Button> </Button>
@ -72,7 +72,7 @@ export function LearnEnglishPage() {
className="absolute inset-0 flex items-center" className="absolute inset-0 flex items-center"
aria-hidden="true" aria-hidden="true"
> >
<div className="w-full border-t border-grayScale-100" /> <div className="w-full border-t border-grayScale-200" />
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<div <div
@ -86,17 +86,17 @@ export function LearnEnglishPage() {
<form className="space-y-6 p-8 pt-4"> <form className="space-y-6 p-8 pt-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700"> <label className="text-[15px] text-grayScale-700">
Program Name Program Name
</label> </label>
<Input <Input
placeholder="e.g. Beginner" placeholder="e.g. Beginner"
className="h-12 rounded-xl" className="h-12 rounded-xl ring-0"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700"> <label className="text-[15px] text-grayScale-700">
Description Description
</label> </label>
<Input <Input
@ -106,7 +106,7 @@ export function LearnEnglishPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700"> <label className="text-[15px] text-grayScale-700">
Program Order Program Order
</label> </label>
<Select className="h-12 rounded-xl"> <Select className="h-12 rounded-xl">
@ -117,7 +117,7 @@ export function LearnEnglishPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700"> <label className="text-[15px] text-grayScale-700">
Thumbnail Thumbnail
</label> </label>
<div className="relative group cursor-pointer"> <div className="relative group cursor-pointer">
@ -148,12 +148,12 @@ export function LearnEnglishPage() {
<DialogClose asChild> <DialogClose asChild>
<Button <Button
variant="outline" variant="outline"
className="h-12 min-w-[120px] rounded-xl border-grayScale-200 font-semibold" className="h-12 min-w-[120px] rounded-[6px] border-grayScale-200 font-semibold"
> >
Cancel Cancel
</Button> </Button>
</DialogClose> </DialogClose>
<Button className="h-12 min-w-[160px] rounded-xl bg-brand-500 font-semibold hover:bg-brand-600"> <Button className="h-12 min-w-[160px] rounded-[6px] bg-brand-500 font-semibold hover:bg-brand-600">
Create Program Create Program
</Button> </Button>
</div> </div>
@ -165,7 +165,7 @@ export function LearnEnglishPage() {
{/* Gradient Divider */} {/* Gradient Divider */}
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true"> <div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-grayScale-100" /> <div className="w-full border-t border-grayScale-200" />
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<div <div
@ -178,11 +178,11 @@ export function LearnEnglishPage() {
</div> </div>
{/* Cards Grid */} {/* Cards Grid */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="flex flex-warp gap-10">
{levels.map((level) => ( {levels.map((level) => (
<Card <Card
key={level.title} key={level.title}
className="group overflow-hidden border-none shadow-soft transition-all duration-300 hover:-translate-y-1 hover:shadow-lg" className="group w-[290px] overflow-hidden border-none shadow-soft transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
> >
{/* Gradient Header */} {/* Gradient Header */}
<div <div
@ -192,15 +192,17 @@ export function LearnEnglishPage() {
"linear-gradient(135deg, #9E289180 0%, #9E2891 100%)", "linear-gradient(135deg, #9E289180 0%, #9E2891 100%)",
}} }}
/> />
<CardContent className="bg-white p-6"> <CardContent className="bg-white p-6 flex flex-col h-[280px]">
<h3 className="text-xl font-bold text-grayScale-700"> <div className="flex-1">
{level.title} <h3 className="text-xl font-bold text-grayScale-700">
</h3> {level.title}
<p className="mt-2 text-sm leading-relaxed text-grayScale-500"> </h3>
{level.description} <p className="mt-2 text-sm leading-relaxed text-grayScale-500">
</p> {level.description}
</p>
</div>
<Link to={`/new-content/learn-english/${level.id}/courses`}> <Link to={`/new-content/learn-english/${level.id}/courses`}>
<Button className="mt-8 h-11 w-full rounded-xl bg-brand-500 font-semibold hover:bg-brand-600"> <Button className="h-11 w-full rounded-[6px] bg-brand-500 font-semibold hover:bg-brand-600">
View Courses View Courses
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" /> <ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</Button> </Button>

View File

@ -94,7 +94,7 @@ export function ModuleDetailPage() {
.join(" ") || "Business English Fundamentals"; .join(" ") || "Business English Fundamentals";
return ( return (
<div className="space-y-10 pb-20 animate-in fade-in duration-500"> <div className="space-y-10 pt-10 pb-20 animate-in fade-in duration-500">
{/* Header Navigation */} {/* Header Navigation */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link <Link
@ -108,11 +108,11 @@ export function ModuleDetailPage() {
{/* Hero Section */} {/* Hero Section */}
<div className="flex flex-col md:flex-row md:items-start justify-between gap-6"> <div className="flex flex-col md:flex-row md:items-start justify-between gap-6">
<div className="space-y-2"> <div className="">
<h1 className="text-3xl font-extrabold text-grayScale-900 tracking-tight"> <h1 className="text-2xl font-medium text-grayScale-900 tracking-tight">
Module 3: {moduleTitle} Module 3: {moduleTitle}
</h1> </h1>
<p className="text-grayScale-500 text-[17px] max-w-2xl font-medium leading-relaxed"> <p className="text-grayScale-500 text-[14px] max-w-2xl">
This module covers essential vocabulary and phrases used in modern This module covers essential vocabulary and phrases used in modern
business environments, including email etiquette and meeting business environments, including email etiquette and meeting
protocols. protocols.
@ -121,7 +121,7 @@ export function ModuleDetailPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
variant="outline" variant="outline"
className="h-12 px-6 rounded-xl border-brand-500 text-brand-500 font-bold hover:bg-brand-50 transition-all flex items-center gap-2" className="rounded-[6px] border-brand-500 text-brand-500 "
onClick={() => onClick={() =>
navigate( navigate(
`/new-content/learn-english/${level}/courses/add-practice?backTo=module&courseId=${courseId}&moduleId=${moduleId}`, `/new-content/learn-english/${level}/courses/add-practice?backTo=module&courseId=${courseId}&moduleId=${moduleId}`,
@ -132,7 +132,7 @@ export function ModuleDetailPage() {
Add Practice Add Practice
</Button> </Button>
<Button <Button
className="h-12 px-6 rounded-xl bg-brand-500 font-bold hover:bg-brand-600 shadow-lg shadow-brand-500/20 text-white transition-all flex items-center gap-2" className="rounded-[6px] bg-brand-500 font-semibold hover:bg-brand-600"
onClick={() => onClick={() =>
navigate( navigate(
`/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}/add-video`, `/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}/add-video`,
@ -148,12 +148,12 @@ export function ModuleDetailPage() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="border-b border-grayScale-50"> <div className="border-b border-grayScale-200">
<div className="flex gap-10"> <div className="flex gap-10">
<button <button
onClick={() => setActiveTab("video")} onClick={() => setActiveTab("video")}
className={cn( className={cn(
"pb-4 text-[17px] font-bold transition-all relative", "pb-4 text-[16px] font-medium transition-all relative",
activeTab === "video" activeTab === "video"
? "text-brand-500 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[3px] after:bg-brand-500 after:rounded-t-full" ? "text-brand-500 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[3px] after:bg-brand-500 after:rounded-t-full"
: "text-grayScale-400 hover:text-grayScale-600", : "text-grayScale-400 hover:text-grayScale-600",
@ -164,7 +164,7 @@ export function ModuleDetailPage() {
<button <button
onClick={() => setActiveTab("practice")} onClick={() => setActiveTab("practice")}
className={cn( className={cn(
"pb-4 text-[17px] font-bold transition-all relative", "pb-4 text-[16px] font-medium transition-all relative",
activeTab === "practice" activeTab === "practice"
? "text-brand-500 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[3px] after:bg-brand-500 after:rounded-t-full" ? "text-brand-500 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-[3px] after:bg-brand-500 after:rounded-t-full"
: "text-grayScale-400 hover:text-grayScale-600", : "text-grayScale-400 hover:text-grayScale-600",
@ -270,12 +270,12 @@ function PracticeCard({
<div className="bg-white rounded-[24px] border border-grayScale-50 shadow-sm overflow-hidden hover:shadow-xl hover:shadow-grayScale-400/5 transition-all group p-6 flex flex-col h-full min-h-[340px]"> <div className="bg-white rounded-[24px] border border-grayScale-50 shadow-sm overflow-hidden hover:shadow-xl hover:shadow-grayScale-400/5 transition-all group p-6 flex flex-col h-full min-h-[340px]">
<div className="flex-1 space-y-6"> <div className="flex-1 space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-[20px] font-bold text-grayScale-900 line-clamp-1"> <h3 className="text-[18px] font-bold text-grayScale-900 line-clamp-1">
{title} {title}
</h3> </h3>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center justify-between gap-3">
<span className="bg-[#22C55E] text-white text-[11px] font-bold px-2 py-1 rounded-[4px]"> <span className="bg-[#22C55E] text-white text-[11px] font-bold px-2 py-1 rounded-[4px]">
{level} {level}
</span> </span>
@ -285,21 +285,21 @@ function PracticeCard({
</div> </div>
</div> </div>
<div className="flex items-center gap-2.5 text-brand-500 bg-brand-50/50 w-fit px-3 py-2 rounded-xl"> <div className="flex items-center gap-2.5 text-brand-400 w-fit py-2 rounded-xl">
<Layers className="h-4 w-4" /> <Layers className="h-4 w-4" />
<span className="text-[14px] font-bold">{variations} Variations</span> <span className="text-[14px] font-bold">{variations} Variations</span>
</div> </div>
<div className="flex items-center justify-between pt-2"> <div className="flex border-t border-grayScale-200 items-center justify-between pt-2">
<div className="bg-grayScale-50 text-grayScale-400 text-[11px] font-bold px-3 py-1.5 rounded-[6px] tracking-wide uppercase"> <div className="bg-grayScale-100 text-grayScale-400 text-[11px] font-bold px-3 py-1.5 rounded-[6px] tracking-wide uppercase">
{status} {status}
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button className="h-8 w-8 rounded-lg border border-grayScale-100 flex items-center justify-center text-grayScale-400 hover:text-brand-500 hover:border-brand-100 transition-all"> <button className="h-8 w-8 rounded-lg flex items-center justify-center text-grayScale-400 hover:text-brand-500 hover:border-brand-100 transition-all">
<Edit2 className="h-4 w-4" /> <Edit2 className="h-5 w-5" />
</button> </button>
<button className="h-8 w-8 rounded-lg border border-grayScale-100 flex items-center justify-center text-grayScale-400 hover:text-red-500 hover:border-red-100 transition-all"> <button className="h-8 w-8 rounded-lg flex items-center justify-center text-grayScale-400 hover:text-red-500 hover:border-red-100 transition-all">
<Trash2 className="h-4 w-4" /> <Trash2 className="h-5 w-5" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -41,7 +41,7 @@ export function NewContentPage() {
<Mic className="h-10 w-10 text-brand-500" /> <Mic className="h-10 w-10 text-brand-500" />
</div> </div>
</div> </div>
<CardContent className="border-t border-grayScale-100 bg-white p-8 text-center"> <CardContent className="border-t border-grayScale-200 bg-white p-8 text-center">
<h3 className="text-xl font-bold text-grayScale-700"> <h3 className="text-xl font-bold text-grayScale-700">
Learn English Learn English
</h3> </h3>
@ -50,7 +50,7 @@ export function NewContentPage() {
modules. modules.
</p> </p>
<Link to="/new-content/learn-english"> <Link to="/new-content/learn-english">
<Button className="mt-8 h-12 w-full rounded-xl bg-brand-500 text-base font-semibold hover:bg-brand-600"> <Button className="mt-8 h-12 w-full rounded-[6px] bg-brand-500 text-base font-semibold ">
Manage Learn English Manage Learn English
</Button> </Button>
</Link> </Link>
@ -64,15 +64,17 @@ export function NewContentPage() {
<Mic className="h-10 w-10 text-brand-500" /> <Mic className="h-10 w-10 text-brand-500" />
</div> </div>
</div> </div>
<CardContent className="border-t border-grayScale-100 bg-white p-8 text-center"> <CardContent className="border-t border-grayScale-200 bg-white p-8 text-center">
<h3 className="text-xl font-bold text-grayScale-700">Courses</h3> <h3 className="text-xl font-bold text-grayScale-700">Courses</h3>
<p className="mt-3 text-sm leading-relaxed text-grayScale-500"> <p className="mt-3 text-sm leading-relaxed text-grayScale-500">
Manage skill-based and exam preparation courses such as Duolingo Manage skill-based and exam preparation courses such as Duolingo
and IELTS. and IELTS.
</p> </p>
<Button className="mt-8 h-12 w-full rounded-xl bg-brand-500 text-base font-semibold hover:bg-brand-600"> <Link to="/new-content/courses" className="block w-full">
Manage Courses <Button className="mt-8 h-12 w-full rounded-[6px] bg-brand-500 text-base font-semibold ">
</Button> Manage Courses
</Button>
</Link>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -45,7 +45,7 @@ export function ProgramCoursesPage() {
]; ];
return ( return (
<div className="space-y-8"> <div className="space-y-8 pt-10">
{/* Navigation */} {/* Navigation */}
<Link <Link
to="/new-content/learn-english" to="/new-content/learn-english"
@ -189,12 +189,27 @@ export function ProgramCoursesPage() {
</div> </div>
</div> </div>
{/* Gradient Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
{/* Cards Grid */} {/* Cards Grid */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="flex flex-warp gap-10 ">
{courses.map((course) => ( {courses.map((course) => (
<Card <Card
key={course.id} key={course.id}
className="group overflow-hidden border border-grayScale-100 shadow-soft transition-all duration-300 hover:shadow-lg" className="group w-[290px] overflow-hidden border border-grayScale-100 shadow-soft transition-all duration-300 hover:shadow-lg"
> >
{/* Gradient Header */} {/* Gradient Header */}
<div <div

View File

@ -0,0 +1,273 @@
import { Link, useParams, useNavigate } from "react-router-dom";
import {
ArrowLeft,
Plus,
FileText,
ClipboardList,
ListChecks,
ChevronRight,
X,
Upload,
} from "lucide-react";
import { Button } from "../../components/ui/button";
import { Card } from "../../components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import { Select } from "../../components/ui/select";
import uploadIcon from "../../assets/icons/upload.png";
export function ProgramDetailPage() {
const navigate = useNavigate();
const { programType } = useParams<{ programType: string }>();
// Mock data for "proficiency" program type
const programs: Record<string, any> = {
proficiency: {
title: "English Proficiency Exams",
description:
"Manage exam-based learning programs such as Duolingo and IELTS.",
courses: [
{
id: "duolingo",
name: "Duolingo English Test",
description:
"Adaptive exam-style practice for speaking, writing, reading, and listening.",
coursesCount: 6,
questionTypesCount: 13,
logo: (
<div className="h-14 w-14 rounded-full bg-[#FFB800] flex items-center justify-center relative overflow-hidden">
{/* Simple Duolingo-like representation if image not available */}
<div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent" />
<div className="h-8 w-8 bg-white rounded-full flex items-center justify-center">
<div className="h-4 w-4 bg-[#FFB800] rounded-sm transform rotate-45" />
</div>
</div>
),
buttonText: "Manage Detail",
},
{
id: "ielts",
name: "IELTS Academic",
description:
"Full preparation for IELTS speaking, writing, listening, and reading.",
coursesCount: 4,
questionTypesCount: 18,
logo: (
<div className="flex items-center gap-1">
<span className="text-[28px] font-black tracking-tighter text-[#E11D48] ">
IELTS
</span>
<span className="text-[8px] font-bold text-[#E11D48] mt-2 tracking-widest uppercase">
</span>
</div>
),
buttonText: "View Detail",
},
],
},
"skill-based": {
title: "Skill-Based Courses",
description:
"Practice-focused communication and skills training for real-world scenarios.",
courses: [], // To be implemented or shown if needed
},
};
const currentProgram =
programs[programType || "proficiency"] || programs.proficiency;
return (
<div className="space-y-8 animate-in fade-in duration-500 pb-10">
{/* Navigation */}
<Link
to="/new-content/courses"
className="flex items-center gap-2.5 text-[15px] font-semibold text-grayScale-600 hover:text-brand-500 transition-colors pt-4 group"
>
<ArrowLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
Back
</Link>
{/* Header section */}
<div className="flex items-start justify-between">
<div className="space-y-2">
<h1 className="text-[26px] font-medium tracking-tight text-grayScale-900">
{currentProgram.title}
</h1>
<p className="max-w-2xl text-[15px] font-medium text-grayScale-500">
{currentProgram.description}
</p>
</div>
<div className="flex items-center gap-3 pt-2">
<Dialog>
<DialogTrigger asChild>
<Button className="h-10 px-6 rounded-[6px] bg-brand-500 font-bold text-white transition-all flex items-center gap-2">
<Plus className="h-5 w-5" />
Create Course
</Button>
</DialogTrigger>
<DialogContent className="max-w-[600px] p-0 border-none rounded-[16px] overflow-hidden">
<div className="bg-white">
<DialogHeader className="px-8 py-6 border-b border-grayScale-200 flex flex-row items-center justify-between">
<DialogTitle className="text-[20px] font-bold relative top-2 text-grayScale-900">
Create Course
</DialogTitle>
</DialogHeader>
<div className="p-8 space-y-8">
<div className="space-y-3">
<label className="text-[15px] text-grayScale-800">
Name
</label>
<Input
placeholder="e.g. TOEFL, IELTS"
className="h-12 border-grayScale-400 rounded-[8px] px-4 placeholder:text-grayScale-400 text-[15px] focus:ring-brand-500/20"
/>
</div>
<div className="space-y-3">
<label className="text-[15px] text-grayScale-800">
Course Order
</label>
<Select defaultValue="1">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</Select>
</div>
{/* Thumbnail Field */}
<div className="space-y-3">
<label className="text-[15px] text-grayScale-800">
Thumbnail
</label>
<div className="relative group cursor-pointer">
<div className="flex flex-col items-center justify-center rounded-[12px] border-2 border-dashed border-grayScale-400 bg-white py-8 px-10 transition-all ">
<div className="mb-4">
<img
src={uploadIcon}
alt="Upload icon"
className="h-10 w-10"
/>
</div>
<p className="text-[15px]">
<span className="text-brand-500 font-bold hover:underline">
Click to upload
</span>{" "}
<span className="text-grayScale-500">
or drag and drop
</span>
</p>
<p className="mt-1.5 text-[12px] text-grayScale-400 uppercase tracking-widest">
JPG, PNG (MAX 1 MB)
</p>
</div>
</div>
</div>
</div>
<div className="px-8 py-6 bg-grayScale-50/30 border-t border-grayScale-50 flex justify-end gap-3">
<DialogClose asChild>
<Button
variant="outline"
className="h-11 px-8 rounded-[8px] border-grayScale-200 text-grayScale-700 font-bold"
>
Cancel
</Button>
</DialogClose>
<Button className="h-11 px-8 rounded-[8px] bg-brand-500 text-white font-bold hover:bg-brand-600">
Create Program
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Button
variant="outline"
className="h-10 px-6 rounded-[6px] border-brand-500 text-brand-500 font-bold flex items-center gap-2"
onClick={() =>
navigate(`/new-content/courses/${programType}/attach-practice`)
}
>
<FileText className="h-5 w-5" />
Attach Practice
</Button>
</div>
</div>
{/* Gradient Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
{/* Cards Grid */}
<div className="flex flex-wrap gap-8 mt-10">
{currentProgram.courses.map((course: any) => (
<Card
key={course.id}
className="bg-white w-[500px] rounded-[20px] border border-grayScale-100 p-6 flex flex-col items-start shadow-sm hover:shadow-md transition-shadow"
>
{/* Logo */}
<div className="h-16 flex items-center">{course.logo}</div>
{/* Content */}
<div className="space-y-4 pt-2 flex-1">
<h3 className="text-[18px] font-medium text-grayScale-900">
{course.name}
</h3>
<p className="text-[14px] text-grayScale-500 font-medium">
{course.description}
</p>
</div>
{/* Badges/Stats */}
<div className="flex items-center pt-4 gap-4">
<div className="h-10 px-4 rounded-[6px] bg-grayScale-100 border border-grayScale-100 flex items-center gap-2 text-grayScale-700">
<ClipboardList className="h-3 w-3 text-grayScale-400" />
<span className="text-[12px] ">
{course.coursesCount} Courses
</span>
</div>
<div className="h-10 px-4 rounded-[6px] bg-grayScale-100 border border-grayScale-100 flex items-center gap-2 text-grayScale-700">
<ListChecks className="h-3 w-3 text-grayScale-400" />
<span className="text-[12px] ">
{course.questionTypesCount} Question Types
</span>
</div>
</div>
{/* Action Button */}
<Button
className="w-full mt-4 h-10 bg-brand-500 text-white rounded-[8px] font-bold flex items-center justify-center gap-2 group/btn"
onClick={() =>
navigate(`/new-content/courses/${programType}/${course.id}`)
}
>
{course.buttonText}
<ChevronRight className="h-5 w-5 transition-transform group-hover/btn:translate-x-1" />
</Button>
</Card>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,82 @@
import { Link } from "react-router-dom";
import { GraduationCap, Brain } from "lucide-react";
import { Button } from "../../components/ui/button";
export function ProgramTypeSelectionPage() {
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Header section */}
<div className="flex items-start justify-between">
<div className="space-y-1.5 pt-2">
<h1 className="text-[28px] font-bold tracking-tight text-grayScale-900">
Courses
</h1>
<p className="max-w-2xl text-[15px] font-medium text-grayScale-500">
Organize courses under skill-based learning or English proficiency
exams. Select a program type to manage curriculum and modules.
</p>
</div>
<Button className="h-10 px-6 rounded-[6px] bg-brand-500 font-bold text-white shadow-sm hover:bg-brand-600 transition-all flex items-center gap-2 mt-4">
Manage Question Types
</Button>
</div>
{/* Gradient Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
{/* Selection Cards Grid */}
<div className="flex flex-warp gap-10 pt-4">
{/* Skill-Based Courses Card */}
<Link to="/new-content/courses/skill-based" className="group h-full">
<div className="bg-white rounded-[6px] w-[500px] border border-grayScale-100 px-10 py-12 h-full transition-all flex flex-col items-start gap-10">
<div className="h-16 w-16 rounded-full bg-brand-50/10 flex items-center justify-center">
<Brain className="h-8 w-8 text-brand-500" />
</div>
<div className="space-y-3 flex-1">
<h3 className="text-[20px] font-bold text-grayScale-900">
Skill-Based Courses
</h3>
<p className="text-[15px] leading-relaxed text-grayScale-500 font-medium">
Practice-focused communication and skills training. Create
modules for vocabulary, grammar, and real-world conversation
scenarios.
</p>
</div>
</div>
</Link>
{/* English Proficiency Exams Card */}
<Link to="/new-content/courses/proficiency" className="group h-full">
<div className="bg-white w-[500px] rounded-[6px] border border-grayScale-100 px-10 py-12 h-full transition-all flex flex-col items-start gap-10">
<div className="h-16 w-16 rounded-full bg-brand-50/10 flex items-center justify-center">
<GraduationCap className="h-8 w-8 text-brand-500" />
</div>
<div className="space-y-3 flex-1">
<h3 className="text-[20px] font-bold text-grayScale-900 transition-colors">
English Proficiency Exams
</h3>
<p className="text-[15px] leading-relaxed text-grayScale-500 font-medium">
Exam preparation courses such as IELTS, and Duolingo. Structure
content by band scores, sections, and mock tests.
</p>
</div>
</div>
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,254 @@
import { Link, useParams, useNavigate } from "react-router-dom";
import {
ArrowLeft,
Plus,
MessageCircle,
PlayCircle,
ClipboardCheck,
ArrowRight,
X,
} from "lucide-react";
import { Button } from "../../components/ui/button";
import { Card } from "../../components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "../../components/ui/dialog";
import { Input } from "../../components/ui/input";
import { Select } from "../../components/ui/select";
import uploadIcon from "../../assets/icons/upload.png";
export function UnitManagementPage() {
const navigate = useNavigate();
const { programType, courseId, unitId } = useParams<{
programType: string;
courseId: string;
unitId: string;
}>();
// Mock titles
const unitTitles: Record<string, string> = {
unit1: "Greetings & Introductions",
unit2: "Speaking",
unit3: "Reading",
};
const unitDisplayName =
unitTitles[unitId || ""] || "Greetings & Introductions";
const modules = [
{
id: "mod1",
name: "Module 1: Basic Phrases",
description: "Learn essential phrases for daily conversations.",
videos: 3,
practices: 3,
gradient:
"linear-gradient(135deg, rgba(158, 40, 145, 0.4) 0%, rgba(158, 40, 145, 0.7) 100%)",
},
{
id: "mod2",
name: "Module 1: Basic Phrases", // Matching Image 2092-1 labels
description: "Learn essential phrases for daily conversations.",
videos: 3,
practices: 3,
gradient:
"linear-gradient(135deg, rgba(79, 70, 229, 0.4) 0%, rgba(79, 70, 229, 0.7) 100%)",
},
{
id: "mod3",
name: "Module 1: Basic Phrases",
description: "Learn essential phrases for daily conversations.",
videos: 3,
practices: 3,
gradient:
"linear-gradient(135deg, rgba(124, 58, 237, 0.4) 0%, rgba(124, 58, 237, 0.7) 100%)",
},
];
return (
<div className="space-y-8 animate-in fade-in duration-500 pb-10">
{/* Navigation */}
<Link
to={`/new-content/courses/${programType}/${courseId}`}
className="flex items-center gap-2.5 text-[15px] font-semibold text-grayScale-600 hover:text-brand-500 transition-colors pt-4 group"
>
<ArrowLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
Back to Courses
</Link>
{/* Header section */}
<div className="flex items-start justify-between">
<h1 className="text-[28px] font-medium tracking-tight text-grayScale-900">
{unitDisplayName}
</h1>
<Dialog>
<DialogTrigger asChild>
<Button className="h-10 px-6 rounded-[6px] bg-brand-500 font-bold text-white shadow-sm hover:bg-brand-600 transition-all flex items-center gap-2">
<Plus className="h-5 w-5" />
Add Modules
</Button>
</DialogTrigger>
<DialogContent className="max-w-[600px] p-0 border-none rounded-[16px] overflow-hidden">
<div className="bg-white">
<DialogHeader className="px-8 py-6 border-b border-grayScale-200 flex flex-row items-center justify-between">
<DialogTitle className="text-[20px] font-bold relative top-2 text-grayScale-900">
Create Modules
</DialogTitle>
<DialogClose className="rounded-full p-1.5 hover:bg-grayScale-50 transition-colors">
<X className="h-5 w-5 text-grayScale-400" />
<span className="sr-only">Close</span>
</DialogClose>
</DialogHeader>
<div className="p-8 space-y-8">
<div className="space-y-3">
<label className="text-[15px] text-grayScale-800">
Module Title
</label>
<Input
placeholder="e.g. 1.1 Exam types"
className="h-12 border-grayScale-400 rounded-[8px] px-4 placeholder:text-grayScale-400 text-[15px] focus:ring-brand-500/20"
/>
</div>
<div className="space-y-3">
<label className="text-[15px] text-grayScale-800">
Module Order
</label>
<Select defaultValue="1">
<option value="1">1</option>
<option value="2">2</option>
</Select>
</div>
<div className="space-y-3">
<label className="text-[15px] text-grayScale-800">Icon</label>
<div className="relative group cursor-pointer">
<div className="flex flex-col items-center justify-center rounded-[12px] border-2 border-dashed border-grayScale-400 bg-white py-8 px-10 transition-all ">
<div className="mb-4">
<img
src={uploadIcon}
alt="Upload icon"
className="h-10 w-10"
/>
</div>
<p className="text-[15px]">
<span className="text-brand-500 font-bold hover:underline">
Click to upload
</span>{" "}
<span className="text-grayScale-500">
or drag and drop
</span>
</p>
<p className="mt-1.5 text-[12px] text-grayScale-400 uppercase tracking-widest">
JPG, PNG (MAX 1 MB)
</p>
</div>
</div>
</div>
</div>
<div className="px-8 py-6 bg-grayScale-50/30 border-t border-grayScale-200 flex justify-end gap-3">
<DialogClose asChild>
<Button
variant="outline"
className="h-11 px-8 rounded-[8px] border-grayScale-200 text-grayScale-700 font-bold"
>
Cancel
</Button>
</DialogClose>
<Button className="h-11 px-8 rounded-[8px] bg-brand-500 text-white font-bold hover:bg-brand-600">
Create Module
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{/* Gradient Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
{/* Grid of Modules */}
<div className="flex flex-wrap gap-4 pt-4">
{modules.map((module, index) => (
<Card
key={`${module.id}-${index}`}
className="group flex w-[400px] flex-col bg-white rounded-[12px] border border-grayScale-100 overflow-hidden shadow-sm hover:shadow-md transition-all"
>
{/* Gradient Header */}
<div
className="h-36 w-full"
style={{ background: module.gradient }}
/>
<div className="p-5 flex flex-col space-y-4">
<div className="flex items-start gap-3">
{/* Chat Icon */}
<div className="mt-1 h-10 w-10 shrink-0 rounded-full bg-[#9E28911A] border border-[#9E289133] flex items-center justify-center">
<MessageCircle className="h-5 w-5 text-brand-500" />
</div>
<div className="space-y-1">
<h3 className="text-[16px] font-medium text-grayScale-900 leading-tight">
{module.name}
</h3>
<p className="text-[12px] text-grayScale-500 font-medium">
{module.description}
</p>
</div>
</div>
{/* Stats Pills */}
<div className="flex items-center gap-3">
<div className="h-8 px-3 rounded-[6px] bg-grayScale-100 border border-grayScale-100 flex items-center gap-2 text-grayScale-600">
<PlayCircle className="h-3.5 w-3.5 text-grayScale-400" />
<span className="text-[12px] font-bold">
{module.videos} Videos
</span>
</div>
<div className="h-8 px-3 rounded-[6px] bg-grayScale-100 border border-grayScale-100 flex items-center gap-2 text-grayScale-600">
<ClipboardCheck className="h-3.5 w-3.5 text-grayScale-400" />
<span className="text-[12px] font-bold">
{module.practices} Practices
</span>
</div>
</div>
{/* Action Button */}
<Button
className="w-full h-10 bg-brand-500 text-white rounded-[6px] font-bold flex items-center justify-center gap-2 group/btn"
onClick={() =>
navigate(
`/new-content/courses/${programType}/${courseId}/${unitId}/${module.id}`,
)
}
>
View Detail
<ArrowRight className="ml-1 h-4 w-4 transition-transform group-hover/btn:translate-x-1" />
</Button>
</div>
</Card>
))}
</div>
</div>
);
}

View File

@ -28,10 +28,6 @@ export function AddModuleModal({ isOpen, onClose }: AddModuleModalProps) {
<DialogDescription className="text-sm text-grayScale-400"> <DialogDescription className="text-sm text-grayScale-400">
Create a module to organize videos and practices. Create a module to organize videos and practices.
</DialogDescription> </DialogDescription>
<DialogClose className="absolute right-8 top-8 flex h-10 w-10 items-center justify-center rounded-full hover:bg-grayScale-50 transition-all">
<X className="h-6 w-6 text-grayScale-400" />
<span className="sr-only">Close</span>
</DialogClose>
</DialogHeader> </DialogHeader>
{/* Gradient Divider */} {/* Gradient Divider */}

View File

@ -67,7 +67,7 @@ export function VideoCard({
</button> </button>
</div> </div>
<h3 className="text-[17px] font-bold text-grayScale-900 line-clamp-2 leading-snug"> <h3 className="text-[16px] font-medium text-grayScale-900 line-clamp-2 leading-snug">
{title} {title}
</h3> </h3>
@ -76,7 +76,7 @@ export function VideoCard({
<Button <Button
variant="outline" variant="outline"
onClick={onEdit} onClick={onEdit}
className="w-full h-11 rounded-xl border-grayScale-100 text-grayScale-600 font-bold hover:bg-grayScale-50 transition-all flex items-center justify-center gap-2" className="w-full h-10 rounded-xl border-grayScale-200 text-grayScale-600 font-bold hover:bg-grayScale-50 transition-all flex items-center justify-center gap-2"
> >
<Edit2 className="h-4 w-4" /> <Edit2 className="h-4 w-4" />
Edit Edit
@ -85,7 +85,7 @@ export function VideoCard({
disabled={status === "Published"} disabled={status === "Published"}
onClick={onPublish} onClick={onPublish}
className={cn( className={cn(
"w-full h-11 rounded-xl font-bold transition-all shadow-sm", "w-full h-10 rounded-xl font-bold transition-all shadow-sm",
status === "Published" status === "Published"
? "bg-[#E9D5E5] text-white opacity-100 cursor-default" ? "bg-[#E9D5E5] text-white opacity-100 cursor-default"
: "bg-brand-500 text-white hover:bg-brand-600 shadow-brand-500/10", : "bg-brand-500 text-white hover:bg-brand-600 shadow-brand-500/10",

View File

@ -0,0 +1,232 @@
import {
Rocket,
GraduationCap,
Folder,
ChevronUp,
ChevronDown,
Eye,
Video as VideoIcon,
ClipboardList,
} from "lucide-react";
import { useState } from "react";
import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card";
import { cn } from "../../../../lib/utils";
interface AttachPracticeReviewStepProps {
formData: any;
prevStep: () => void;
onPublish: () => void;
}
export function AttachPracticeReviewStep({
formData,
prevStep,
onPublish,
}: AttachPracticeReviewStepProps) {
const [isExpanded, setIsExpanded] = useState(true);
const [isConfirmed, setIsConfirmed] = useState(false);
const questions = [
{ order: "01", text: "What is the main idea of the passage?" },
{ order: "02", text: "What does the speaker mainly talk about in the..." },
{ order: "03", text: "Which option best completes the sentence?" },
];
return (
<div className="space-y-10 animate-in fade-in duration-500 mx-auto">
{/* 1. Video Summary Card */}
<Card className="p-6 border-grayScale-200 rounded-2xl bg-white overflow-hidden">
<div className="flex gap-8 items-start">
{/* Thumbnail */}
<div className="relative h-[150px] w-[260px] rounded-xl overflow-hidden shadow-inner flex-shrink-0">
<img
src="https://images.unsplash.com/photo-1557425955-df376b5903c8?auto=format&fit=crop&q=80&w=600"
alt="Video Thumbnail"
className="w-full h-full object-cover"
/>
<div className="absolute bottom-2 right-2 bg-black/70 text-white text-[11px] font-bold px-2 py-0.5 rounded flex items-center gap-1.5">
<VideoIcon className="h-3 w-3" />
12:30
</div>
</div>
{/* Details */}
<div className="pt-12">
<h3 className="text-[20px] font-bold text-grayScale-900 leading-tight">
Intro to Interactive Speaking
</h3>
<div className="flex flex-wrap gap-3">
<div className="h-8 pr-2 rounded-full flex items-center gap-2 text-brand-500 text-[13px]">
<GraduationCap className="h-4 w-4" />
IELTS
</div>
<div className="h-8 pr-2 rounded-full flex items-center gap-2 text-brand-500 text-[13px]">
<Folder className="h-4 w-4" />
Unit 2: Speaking
</div>
<div className="h-8 rounded-full flex items-center gap-2 text-brand-500 text-[13px]">
<Folder className="h-4 w-4" />
Module 4: Interactive Speaking
</div>
</div>
</div>
</div>
</Card>
{/* 2. Attached Practices Section */}
<div className="space-y-6">
<div className="flex items-center justify-between ">
<h2 className="text-[20px] font-bold text-[#0F172A] flex items-center gap-3">
Attached Practices
</h2>
<span className="h-6 px-3 rounded-full bg-grayScale-200/40 text-grayScale-500 text-[12px] font-bold flex items-center justify-center">
Total Items: 3
</span>
</div>
<Card className="overflow-hidden border border-grayScale-200 shadow-sm rounded-2xl bg-white">
{/* Header */}
<div
className="p-6 flex items-center justify-between transition-colors hover:bg-grayScale-25 cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-4">
<div className="h-12 w-12 rounded-full bg-[#FDF2F8] flex items-center justify-center text-[#9E2891]">
<ClipboardList className="h-6 w-6" />
</div>
<div className="space-y-0.5">
<h4 className="text-[17px] font-medium text-grayScale-900">
Multiple Choice
</h4>
<p className="text-[14px] text-grayScale-400 font-medium">
3 Questions ~4 min to complete
</p>
</div>
</div>
<div className="flex items-center gap-6">
<button className="flex items-center gap-2 text-[#9E2891] font-medium text-[14px] hover:opacity-80 transition-opacity">
<Eye className="h-4 w-4" />
Preview
</button>
{isExpanded ? (
<ChevronUp className="h-6 w-6 text-grayScale-300" />
) : (
<ChevronDown className="h-6 w-6 text-grayScale-300" />
)}
</div>
</div>
{/* Expanded Content (Table) */}
{isExpanded && (
<div className="animate-in slide-in-from-top-2 duration-300">
<div className="border-t border-grayScale-100">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-white">
<th className="py-4 pl-10 pr-0 w-[80px]"></th>
<th className="py-4 px-4 text-[13px] font-medium text-[#A5B4C1] uppercase tracking-wide w-24">
Order
</th>
<th className="py-4 px-4 text-[13px] font-medium text-[#A5B4C1] uppercase tracking-wide">
Versions
</th>
</tr>
</thead>
<tbody className="border-t border-grayScale-100">
{questions.map((q, i) => (
<tr
key={i}
className="border-b border-grayScale-200 last:border-0 group hover:bg-grayScale-25 transition-colors"
>
<td className="py-6 pl-10 pr-0 text-center">
<GripVertical className="h-4 w-4 text-[#A5B4C1]" />
</td>
<td className="py-6 px-4">
<span className="text-[15px] text-[#A5B4C1]">
{q.order}
</span>
</td>
<td className="py-6 px-4">
<p className="text-[15px] font-medium text-[#0D1421]">
{q.text}
</p>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</Card>
</div>
{/* 3. Confirmation Checkbox */}
<div className="bg-[#F1F5F9] border border-[#E2E8F0] px-6 py-4 rounded-[12px] flex items-start gap-4">
<input
type="checkbox"
id="confirm"
checked={isConfirmed}
onChange={(e) => setIsConfirmed(e.target.checked)}
className="mt-1 h-5 w-5 rounded border-grayScale-300 text-brand-500 focus:ring-brand-500 cursor-pointer"
/>
<div className="">
<label
htmlFor="confirm"
className="text-[16px] font-bold text-grayScale-900 cursor-pointer"
>
I confirm these details are correct
</label>
<p className="text-[13px] text-grayScale-400">
This action cannot be undone immediately. Rollback requires manual
intervention.
</p>
</div>
</div>
{/* 4. Action Footer */}
<div className="flex items-center justify-between pt-10 px-2">
<Button
onClick={prevStep}
variant="outline"
className="h-12 px-10 rounded-[6px] bg-transparent border-grayScale-400 font-bold text-grayScale-600 transition-all"
>
Back
</Button>
<div className="flex gap-4">
<Button
variant="outline"
className="h-12 px-10 rounded-[6px] border-grayScale-200 font-bold text-grayScale-600 bg-white shadow-none transition-all"
>
Save as Draft
</Button>
<Button
onClick={onPublish}
disabled={!isConfirmed}
className={cn(
"h-12 px-10 rounded-[6px] font-bold text-white shadow-xl flex items-center gap-3 transition-all active:scale-95",
isConfirmed
? "bg-[#9E2891] hover:bg-[#8A237E] shadow-[#9E2891]/20"
: "bg-grayScale-200 cursor-not-allowed opacity-50",
)}
>
<Rocket className="h-5 w-5" />
Publish Now
</Button>
</div>
</div>
</div>
);
}
// Add GripVertical helper since it might not be imported from lucide-react if I missed it
function GripVertical({ className }: { className?: string }) {
return (
<div className={cn("grid grid-cols-2 gap-0.5", className)}>
{[...Array(6)].map((_, i) => (
<div key={i} className="h-0.5 w-0.5 rounded-full bg-current" />
))}
</div>
);
}

View File

@ -0,0 +1,168 @@
import { LayoutGrid, Video, ArrowRight } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card";
import { Select } from "../../../../components/ui/select";
import { cn } from "../../../../lib/utils";
interface AttachPracticeStep1Props {
formData: any;
setFormData: (data: any) => void;
nextStep: () => void;
onCancel: () => void;
}
export function AttachPracticeStep1({
formData,
setFormData,
nextStep,
onCancel,
}: AttachPracticeStep1Props) {
return (
<Card className="overflow-hidden max-w-4xl mx-auto border-grayScale-100 rounded-3xl bg-white shadow-sm animate-in fade-in duration-500">
<div className="space-y-6 p-12">
{/* Select Program */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-[#0F172A] ml-1">
Select Program
</label>
<div className="relative">
<div className="absolute left-5 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<LayoutGrid className="h-5 w-5 text-grayScale-400" />
</div>
<Select
className="h-[56px] w-full rounded-[10px] border-grayScale-200 bg-[#fff] pl-14 text-grayScale-800 font-bold focus:border-brand-500 transition-all text-sm"
value={formData.program}
onChange={(e) =>
setFormData({ ...formData, program: e.target.value })
}
>
<option value="">Choose Program</option>
<option value="skill">Skill-Based Courses</option>
<option value="exams">English Proficiency Exams</option>
</Select>
</div>
</div>
{/* Select Module */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-[#0F172A] ml-1">
Select Module
</label>
<div className="relative">
<div className="absolute left-5 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<LayoutGrid className="h-5 w-5 text-grayScale-400" />
</div>
<Select
className="h-[56px] w-full rounded-[10px] border-grayScale-200 bg-[#fff] pl-14 text-grayScale-800 font-bold focus:border-brand-500 transition-all text-sm"
value={formData.module}
onChange={(e) =>
setFormData({ ...formData, module: e.target.value })
}
>
<option value="">Choose Module</option>
<option value="m1">Module 1: Basic Phrases</option>
<option value="m2">Module 2: Intermediate Grammar</option>
</Select>
</div>
<p className="text-[13px] text-grayScale-400 font-medium px-1">
Select the specific learning module this practice will reinforce.
</p>
</div>
{/* Select Video */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-[#0F172A] ml-1">
Select Video
</label>
<div className="relative">
<div className="absolute left-5 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<Video className="h-5 w-5 text-grayScale-400" />
</div>
<Select
className="h-[56px] w-full rounded-[10px] border-grayScale-200 bg-[#fff] pl-14 text-grayScale-800 font-bold focus:border-brand-500 transition-all text-sm"
value={formData.video}
onChange={(e) =>
setFormData({ ...formData, video: e.target.value })
}
>
<option value="">Choose a video</option>
<option value="v1">Intro to Interactive Speaking</option>
<option value="v2">Business Meeting Etiquette</option>
</Select>
</div>
<p className="text-[13px] text-grayScale-400 font-medium px-1">
Select the specific video this practice will reinforce.
</p>
</div>
{/* Select Question Type */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-[#0F172A] ml-1">
Select Question Type
</label>
<div className="relative">
<div className="absolute left-5 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<LayoutGrid className="h-5 w-5 text-grayScale-400" />
</div>
<Select
className="h-[56px] w-full rounded-[10px] border-grayScale-200 bg-[#fff] pl-14 text-grayScale-800 font-bold focus:border-brand-500 transition-all text-sm"
value={formData.questionType}
onChange={(e) =>
setFormData({ ...formData, questionType: e.target.value })
}
>
<option value="">Choose question type</option>
<option value="speaking">Speaking Practice</option>
<option value="listening">Listening Quiz</option>
</Select>
</div>
<p className="text-[13px] text-grayScale-400 font-medium px-1">
Select one question type that associates with th selected video
</p>
</div>
{/* Set Version */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-[#0F172A] ml-1">
Set Version
</label>
<div className="relative">
<div className="absolute left-5 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<LayoutGrid className="h-5 w-5 text-grayScale-400" />
</div>
<Select
className="h-[56px] w-full rounded-[10px] border-grayScale-200 bg-[#fff] pl-14 text-grayScale-800 font-bold focus:border-brand-500 transition-all text-sm"
value={formData.version}
onChange={(e) =>
setFormData({ ...formData, version: e.target.value })
}
>
<option value="">Choose versions</option>
<option value="v1">Version 1.0</option>
<option value="v2">Version 2.0</option>
</Select>
</div>
<p className="text-[13px] text-grayScale-400 font-medium px-1">
Select one or more versions
</p>
</div>
</div>
<div className="flex items-center justify-between border-t border-grayScale-200 bg-[#F8FAFC] py-4 px-12">
<button
className="text-[14px] text-grayScale-500 transition-colors hover:text-grayScale-700"
onClick={onCancel}
>
Cancel
</button>
<Button
onClick={nextStep}
className="h-10 px-12 rounded-[6px] bg-[#9E2891] text-[14px] font-bold text-white shadow-lg shadow-brand-500/10 transition-all active:scale-95 flex items-center gap-3"
>
Next: Review
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</Card>
);
}

View File

@ -10,6 +10,7 @@ interface ContextStepProps {
navigate: (path: string) => void; navigate: (path: string) => void;
level: string; level: string;
isModuleContext?: boolean; isModuleContext?: boolean;
isCourseContext?: boolean;
} }
export function ContextStep({ export function ContextStep({
@ -19,22 +20,37 @@ export function ContextStep({
navigate, navigate,
level, level,
isModuleContext, isModuleContext,
isCourseContext,
}: ContextStepProps) { }: ContextStepProps) {
return ( return (
<Card className="overflow-hidden border-grayScale-50 shadow-soft rounded-2xl bg-white animate-in fade-in duration-500"> <Card className="overflow-hidden border-grayScale-300 rounded-2xl bg-white animate-in fade-in duration-500">
<div className="border-b border-grayScale-50 p-8"> <div className="border-b border-grayScale-50 px-8 pt-8 pb-4">
<h2 className="text-2xl font-bold text-grayScale-900 leading-none"> <h2 className="text-xl font-bold text-grayScale-900 leading-none">
Step 1: Context Definition Step 1: Context Definition
</h2> </h2>
<p className="text-grayScale-400 text-base mt-3"> <p className="text-grayScale-600 text-base mt-3">
Define the educational level and curriculum module for this practice. Define the educational level and curriculum module for this practice.
</p> </p>
</div> </div>
<div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
<div className="space-y-10 p-10"> <div className="space-y-10 p-10">
{/* Program Field */} {/* Program Field */}
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[17px] font-bold text-grayScale-700 ml-1"> <label className="text-[16px] text-grayScale-700 ml-1">
Program{" "} Program{" "}
<span className="text-grayScale-300 font-medium"> <span className="text-grayScale-300 font-medium">
(Auto-selected) (Auto-selected)
@ -42,10 +58,10 @@ export function ContextStep({
</label> </label>
<div className="relative"> <div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2"> <div className="absolute left-6 top-1/2 -translate-y-1/2">
<GraduationCap className="h-6 w-6 text-grayScale-400" /> <GraduationCap className="h-6 w-6 text-grayScale-600" />
</div> </div>
<Select <Select
className="h-16 w-full rounded-2xl border-grayScale-50 bg-[#F9FAFB] pl-16 text-grayScale-700 font-bold focus:border-brand-500 focus:ring-0 transition-all cursor-default" className="h-12 w-full rounded-[6px] border-grayScale-400 bg-[#fff] pl-16 text-grayScale-800 font-bold focus:border-brand-500 focus:ring-0 transition-all cursor-default"
disabled disabled
> >
<option>{formData.program || "Intermediate"}</option> <option>{formData.program || "Intermediate"}</option>
@ -55,7 +71,7 @@ export function ContextStep({
{/* Course Field */} {/* Course Field */}
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[17px] font-bold text-grayScale-700 ml-1"> <label className="text-[16px] font-bold text-grayScale-700 ml-1">
Course{" "} Course{" "}
<span className="text-grayScale-300 font-medium"> <span className="text-grayScale-300 font-medium">
(Auto-selected) (Auto-selected)
@ -63,10 +79,10 @@ export function ContextStep({
</label> </label>
<div className="relative"> <div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2"> <div className="absolute left-6 top-1/2 -translate-y-1/2">
<GraduationCap className="h-6 w-6 text-grayScale-400" /> <GraduationCap className="h-6 w-6 text-grayScale-600" />
</div> </div>
<Select <Select
className="h-16 w-full rounded-2xl border-grayScale-50 bg-[#F9FAFB] pl-16 text-grayScale-700 font-bold focus:border-brand-500 focus:ring-0 transition-all cursor-default" className="h-12 w-full rounded-[6px] border-grayScale-400 bg-[#fff] pl-16 text-grayScale-800 font-bold focus:border-brand-500 focus:ring-0 transition-all cursor-default"
disabled disabled
> >
<option>{formData.course || "B2"}</option> <option>{formData.course || "B2"}</option>
@ -75,30 +91,32 @@ export function ContextStep({
</div> </div>
{/* Select Module Field */} {/* Select Module Field */}
<div className="space-y-3"> {(isModuleContext || isCourseContext) && (
<label className="text-[17px] font-bold text-grayScale-700 ml-1"> <div className="space-y-3">
Select Module <label className="text-[16px] font-bold text-grayScale-700 ml-1">
</label> Select Module
<div className="relative"> </label>
<div className="absolute left-6 top-1/2 -translate-y-1/2"> <div className="relative">
<LayoutGrid className="h-6 w-6 text-grayScale-400" /> <div className="absolute left-6 top-1/2 -translate-y-1/2">
<LayoutGrid className="h-6 w-6 text-grayScale-400" />
</div>
<Select className="h-12 w-full rounded-[6px] border-grayScale-300 bg-[#fff] pl-16 text-grayScale-800 font-bold focus:border-brand-500 focus:ring-0 transition-all">
<option value="">Choose a module...</option>
<option value="m1">Introduction Basics</option>
<option value="m2">Daily Routines</option>
<option value="m3">Travel Essentials</option>
</Select>
</div> </div>
<Select className="h-16 w-full rounded-2xl border-grayScale-100 bg-white pl-16 text-grayScale-700 font-bold focus:border-brand-500 focus:ring-0 transition-all ring-offset-0"> <p className="text-[13px] text-grayScale-400 font-medium px-2">
<option value="">Choose a module...</option> Select the specific learning module this practice will reinforce.
<option value="m1">Introduction Basics</option> </p>
<option value="m2">Daily Routines</option>
<option value="m3">Travel Essentials</option>
</Select>
</div> </div>
<p className="text-[13px] text-grayScale-400 font-medium px-2"> )}
Select the specific learning module this practice will reinforce.
</p>
</div>
{/* Select Video Field (Conditional) */} {/* Select Video Field (Conditional) */}
{isModuleContext && ( {isModuleContext && (
<div className="space-y-3 animate-in fade-in slide-in-from-top-2 duration-300"> <div className="space-y-3 animate-in fade-in slide-in-from-top-2 duration-300">
<label className="text-[17px] font-bold text-grayScale-700 ml-1"> <label className="text-[16px] font-bold text-grayScale-700 ml-1">
Select Video Select Video
</label> </label>
<div className="relative"> <div className="relative">
@ -106,7 +124,7 @@ export function ContextStep({
<Monitor className="h-6 w-6 text-grayScale-400" /> <Monitor className="h-6 w-6 text-grayScale-400" />
</div> </div>
<Select <Select
className="h-16 w-full rounded-2xl border-grayScale-100 bg-white pl-16 text-grayScale-700 font-bold focus:border-brand-500 focus:ring-0 transition-all" className="h-12 w-full rounded-[6px] border-grayScale-300 bg-[#fff] pl-16 text-grayScale-800 font-bold focus:border-brand-500 focus:ring-0 transition-all"
value={formData.selectedVideo} value={formData.selectedVideo}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, selectedVideo: e.target.value }) setFormData({ ...formData, selectedVideo: e.target.value })
@ -124,9 +142,9 @@ export function ContextStep({
)} )}
</div> </div>
<div className="flex items-center justify-between border-t border-grayScale-50 bg-[#F9FAFB]/50 p-10 px-12"> <div className="flex items-center justify-between border-t border-grayScale-100 bg-[#F8FAFC] p-4 px-12">
<button <button
className="text-[17px] font-bold text-grayScale-500 transition-colors hover:text-grayScale-700" className="text-[14px] font-bold text-grayScale-500 transition-colors hover:text-grayScale-700"
onClick={() => onClick={() =>
navigate(`/new-content/learn-english/${level}/courses`) navigate(`/new-content/learn-english/${level}/courses`)
} }
@ -135,7 +153,7 @@ export function ContextStep({
</button> </button>
<Button <Button
onClick={nextStep} onClick={nextStep}
className="h-14 px-10 rounded-2xl bg-brand-500 text-[17px] font-bold text-white hover:bg-brand-600 shadow-xl shadow-brand-500/20 transition-all active:scale-95 flex items-center gap-2" className="h-10 px-10 rounded-[6px] bg-brand-500 text-[14px] font-bold text-white transition-all active:scale-95 flex items-center gap-2"
> >
Next: {isModuleContext ? "Persona" : "Scenario"}{" "} Next: {isModuleContext ? "Persona" : "Scenario"}{" "}
<ArrowRight className="h-5 w-5" /> <ArrowRight className="h-5 w-5" />

View File

@ -32,56 +32,59 @@ export function PersonaStep({
</p> </p>
</div> </div>
<div className="grid grid-cols-2 gap-6 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-6 sm:grid-cols-4">
{PERSONAS.map((persona) => ( {PERSONAS.map((persona) => {
<div const isSelected = selectedPersona === persona.id;
key={persona.id} return (
onClick={() => setSelectedPersona(persona.id)} <div
className={cn( key={persona.id}
"group relative cursor-pointer rounded-2xl border-2 bg-white p-6 transition-all duration-300", onClick={() => setSelectedPersona(persona.id)}
selectedPersona === persona.id className={cn(
? "border-brand-500 shadow-xl scale-105" "group relative w-[260px] cursor-pointer rounded-2xl border-2 bg-white p-6 transition-all duration-300",
: "border-grayScale-50 hover:border-brand-200", isSelected
)} ? "border-brand-500"
> : "border-grayScale-100 hover:border-brand-200",
<div className="flex flex-col items-center gap-4"> )}
<div className="relative"> >
<Avatar className="h-24 w-24 border-4 border-white shadow-lg"> {/* Top-right checkmark badge */}
<AvatarImage src={persona.avatar} /> {isSelected && (
<AvatarFallback> <div className="absolute right-2.5 top-2.5 grid h-6 w-6 place-items-center rounded-full bg-brand-500 text-white z-10">
{persona.name.substring(0, 2)} <Check className="h-4 w-4 stroke-[3]" />
</AvatarFallback> </div>
</Avatar> )}
{selectedPersona === persona.id && ( <div className="flex flex-col items-center gap-4">
<div className="absolute -right-2 top-0 grid h-7 w-7 place-items-center rounded-full bg-brand-500 text-white shadow-xl ring-4 ring-white"> {/* Avatar with conditional purple ring */}
<Check className="h-4 w-4 stroke-[3]" /> <div
</div> className={cn(
)} "rounded-full p-[3px] transition-all duration-300",
isSelected ? "bg-brand-500" : "bg-transparent",
)}
>
<Avatar className="h-24 w-24 border-2 border-white">
<AvatarImage src={persona.avatar} />
<AvatarFallback>
{persona.name.substring(0, 2)}
</AvatarFallback>
</Avatar>
</div>
<span className="text-lg font-bold text-grayScale-700">
{persona.name}
</span>
</div> </div>
<span
className={cn(
"text-lg font-bold transition-colors",
selectedPersona === persona.id
? "text-brand-600"
: "text-grayScale-700",
)}
>
{persona.name}
</span>
</div> </div>
</div> );
))} })}
</div> </div>
<div className="flex items-center justify-between pt-8"> <div className="flex items-center justify-between pt-8">
<Button <Button
onClick={prevStep} onClick={prevStep}
variant="outline" variant="outline"
className="h-12 w-28 rounded-xl border-grayScale-200 font-bold text-grayScale-600" className="h-10 w-20 rounded-[6px] border-grayScale-200 text-grayScale-600"
> >
Back Back
</Button> </Button>
<Button <Button
onClick={nextStep} onClick={nextStep}
className="h-12 rounded-xl bg-brand-500 px-8 font-bold hover:bg-brand-600 shadow-md shadow-brand-500/20" className="h-10 rounded-[6px] bg-brand-500 px-8 hover:bg-brand-600 shadow-md shadow-brand-500/20"
> >
Next: Questions <ArrowRight className="ml-2 h-4 w-4" /> Next: Questions <ArrowRight className="ml-2 h-4 w-4" />
</Button> </Button>

View File

@ -0,0 +1,107 @@
import { Edit2, Trash2, ArrowRight } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card";
import { cn } from "../../../../lib/utils";
interface ProgramAttachReviewStepProps {
formData: any;
prevStep: () => void;
onPublish: () => void;
onCancel: () => void;
}
const MOCK_QUESTIONS = [
{
id: "q1",
title: "1. Speak About The Photo",
description:
'Passage: "Good morning, everyone. I\'d like to start by reviewing the quarterly figures. As you can see from the chart..."',
},
{
id: "q2",
title: "2. Fill In the Blank",
description:
'Passage: "Attention passengers on Flight 492 to London. We are now inviting passengers with small children..."',
},
{
id: "q3",
title: "3. Writing Part 1",
description:
'Passage: "In today\'s lecture on astrophysics, we will discuss the concept of event horizons and their implications..."',
},
];
export function ProgramAttachReviewStep({
formData,
prevStep,
onPublish,
onCancel,
}: ProgramAttachReviewStepProps) {
return (
<div className="space-y-6 animate-in fade-in duration-500">
{/* Questions List */}
<div className="space-y-6">
{MOCK_QUESTIONS.map((q) => (
<Card
key={q.id}
className="group relative flex border-grayScale-200 rounded-2xl bg-white overflow-hidden transition-all"
>
{/* Grip Area */}
<div className="w-[50px] flex items-center justify-center bg-white border-r border-grayScale-200">
<GripVertical className="h-3 w-3 text-grayScale-600" />
</div>
{/* Content Area */}
<div className="flex-1 p-8 py-10">
<h4 className="text-[18px] font-medium text-[#0F172A] mb-3 leading-tight">
{q.title}
</h4>
<p className="text-[14px] text-grayScale-400 leading-relaxed line-clamp-2">
{q.description}
</p>
</div>
{/* Actions Area (Vertical Stack) */}
<div className="w-[50px] border-l border-grayScale-200 flex flex-col items-center justify-center divide-y divide-grayScale-50">
<button className="flex-1 w-full flex items-center justify-center text-grayScale-600 hover:text-brand-500 transition-colors border-b border-grayScale-200">
<Edit2 className="h-4 w-4" />
</button>
<button className="flex-1 w-full flex items-center justify-center text-grayScale-600 hover:text-red-500 transition-colors">
<Trash2 className="h-4 w-4" />
</button>
</div>
</Card>
))}
</div>
{/* Footer Actions */}
<div className="flex bg-[#F8FAFC] border border-grayScale-200 items-center rounded-[12px] justify-between py-4 px-6">
<Button
onClick={onCancel}
variant="outline"
className="h-10 px-6 rounded-[6px] border-grayScale-100 font-bold text-grayScale-500 bg-white hover:bg-grayScale-50 transition-all text-lg"
>
Cancel
</Button>
<Button
onClick={onPublish}
className="h-10 px-10 rounded-[6px] bg-[#9E2891] text-[16px] font-medium text-white transition-all active:scale-95 flex items-center gap-3"
>
Next: Review & Publish
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
);
}
// GripVertical helper
function GripVertical({ className }: { className?: string }) {
return (
<div className={cn("grid grid-cols-2 gap-0.5", className)}>
{[...Array(6)].map((_, i) => (
<div key={i} className="h-1 w-1 rounded-full bg-current" />
))}
</div>
);
}

View File

@ -0,0 +1,141 @@
import { LayoutGrid, Plus, ArrowRight } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card";
import { Select } from "../../../../components/ui/select";
import { cn } from "../../../../lib/utils";
interface ProgramAttachStep1Props {
formData: any;
setFormData: (data: any) => void;
nextStep: () => void;
onCancel: () => void;
}
export function ProgramAttachStep1({
formData,
setFormData,
nextStep,
onCancel,
}: ProgramAttachStep1Props) {
return (
<Card className="overflow-hidden border-grayScale-100 rounded-[16px] bg-white shadow-sm animate-in fade-in duration-500">
<div className="space-y-6 p-8 pb-16">
{/* Select Program */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-[#0F172A] ml-1">
Select Program
</label>
<div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<LayoutGrid className="h-5 w-5 text-grayScale-400" />
</div>
<Select
className="h-[56px] w-full rounded-[12px] border-grayScale-200 bg-white pl-16 text-grayScale-700 font-medium focus:border-brand-500 transition-all text-sm appearance-none"
value={formData.program}
onChange={(e) =>
setFormData({ ...formData, program: e.target.value })
}
>
<option value="">Choose Program</option>
<option value="exams">English Proficiency Exams</option>
<option value="skill">Skill-Based Courses</option>
</Select>
</div>
</div>
{/* Tests (Auto Select) */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-grayScale-900 ml-1">
Tests{" "}
<span className="text-grayScale-400 font-medium">
(Auto Select)
</span>
</label>
<div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<LayoutGrid className="h-5 w-5 text-grayScale-400" />
</div>
<div className="h-[56px] w-full rounded-[12px] border border-grayScale-200 bg-white flex items-center pl-16 text-grayScale-700 font-medium text-sm">
Mock Exam 1
</div>
</div>
</div>
<div className="w-full border-t border-grayScale-300" />
{/* Select Question Type */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-[#0F172A] ml-1">
Select Question Type
</label>
<div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<LayoutGrid className="h-5 w-5 text-grayScale-400" />
</div>
<Select
className="h-[56px] w-full rounded-[12px] border-grayScale-200 bg-white pl-16 text-grayScale-700 font-medium focus:border-brand-500 transition-all text-sm appearance-none"
value={formData.questionType}
onChange={(e) =>
setFormData({ ...formData, questionType: e.target.value })
}
>
<option value="">Choose question type</option>
<option value="Speaking Practice">Speaking Practice</option>
<option value="Writing Part 1">Writing Part 1</option>
</Select>
</div>
<p className="text-[13px] text-grayScale-400 font-medium px-1">
Select one question type that associates with th selected video
</p>
</div>
{/* Set Version */}
<div className="space-y-4">
<label className="text-[14px] font-medium text-[#0F172A] ml-1">
Set Version
</label>
<div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2 pointer-events-none z-10">
<LayoutGrid className="h-5 w-5 text-grayScale-400" />
</div>
<Select
className="h-[56px] w-full rounded-[12px] border-grayScale-200 bg-white pl-16 text-grayScale-700 font-medium focus:border-brand-500 transition-all text-sm appearance-none"
value={formData.version}
onChange={(e) =>
setFormData({ ...formData, version: e.target.value })
}
>
<option value="">Choose a version</option>
<option value="V 1.0">V 1.0</option>
<option value="V 2.0">V 2.0</option>
</Select>
</div>
</div>
{/* Add More Button */}
<button className="flex items-center gap-2 text-[#9E2891] font-medium text-[14px] group transition-all hover:translate-x-1">
<div className="h-5 w-5 rounded-full border-2 border-[#9E2891] flex items-center justify-center">
<Plus className="h-3 w-3" />
</div>
Add More
</button>
</div>
<div className="flex items-center justify-between border-t border-grayScale-200 bg-[#F8FAFC] py-4 px-12">
<button
className="text-[14px] text-grayScale-500 transition-colors hover:text-grayScale-700"
onClick={onCancel}
>
Cancel
</button>
<Button
onClick={nextStep}
className="h-10 px-12 rounded-[6px] bg-[#9E2891] text-[14px] font-bold text-white shadow-lg shadow-brand-500/10 transition-all active:scale-95 flex items-center gap-3"
>
Next: Review
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</Card>
);
}

View File

@ -34,7 +34,7 @@ export function QuestionsStep({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-1 px-2"> <div className="space-y-1 px-2">
<h2 className="text-2xl font-extrabold text-grayScale-700"> <h2 className="text-2xl font-bold text-grayScale-700">
Create Practice Questions Create Practice Questions
</h2> </h2>
<p className="text-grayScale-400 text-lg"> <p className="text-grayScale-400 text-lg">
@ -52,7 +52,7 @@ export function QuestionsStep({
<div className="flex items-center justify-between border-b border-grayScale-50 pb-4 mb-4"> <div className="flex items-center justify-between border-b border-grayScale-50 pb-4 mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<GripVertical className="h-5 w-5 text-brand-500 cursor-grab" /> <GripVertical className="h-5 w-5 text-brand-500 cursor-grab" />
<span className="font-bold text-grayScale-500 text-lg"> <span className="font-bold text-grayScale-500 text-base">
Question {i + 1} Question {i + 1}
</span> </span>
</div> </div>
@ -67,7 +67,7 @@ export function QuestionsStep({
setFormData({ ...formData, questions: newQuestions }); setFormData({ ...formData, questions: newQuestions });
}} }}
> >
<Trash2 className="h-5 w-5" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-8"> <div className="grid grid-cols-1 md:grid-cols-12 gap-8">
@ -82,7 +82,7 @@ export function QuestionsStep({
newQuestions[i].text = e.target.value; newQuestions[i].text = e.target.value;
setFormData({ ...formData, questions: newQuestions }); setFormData({ ...formData, questions: newQuestions });
}} }}
className="h-16 rounded-xl border-grayScale-200 focus:border-brand-500 font-medium px-6 text-lg placeholder:text-grayScale-300 bg-white" className="h-16 rounded-xl border-grayScale-200 font-medium px-6 text-base placeholder:text-grayScale-400 bg-white text-grayScale-700"
placeholder="e.g. How long have you been studying English?" placeholder="e.g. How long have you been studying English?"
/> />
</div> </div>
@ -90,14 +90,30 @@ export function QuestionsStep({
<label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest"> <label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest">
VOICE PROMPT VOICE PROMPT
</label> </label>
<VoicePrompt filename={q.voicePrompt} /> <VoicePrompt
src={q.voicePrompt}
filename={q.voicePrompt}
onRemove={() => {
const newQuestions = [...formData.questions];
newQuestions[i].voicePrompt = "";
setFormData({ ...formData, questions: newQuestions });
}}
/>
</div> </div>
</div> </div>
<div className="md:w-1/3 space-y-3"> <div className="md:w-1/3 space-y-3">
<label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest"> <label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest">
SAMPLE ANSWER PROMPT SAMPLE ANSWER PROMPT
</label> </label>
<VoicePrompt filename={q.sampleAnswer} /> <VoicePrompt
src={q.sampleAnswer}
filename={q.sampleAnswer}
onRemove={() => {
const newQuestions = [...formData.questions];
newQuestions[i].sampleAnswer = "";
setFormData({ ...formData, questions: newQuestions });
}}
/>
</div> </div>
</div> </div>
</Card> </Card>
@ -124,13 +140,13 @@ export function QuestionsStep({
<Button <Button
onClick={prevStep} onClick={prevStep}
variant="outline" variant="outline"
className="h-12 w-28 rounded-xl border-grayScale-200 font-bold text-grayScale-600 shadow-sm" className="h-10 w-20 rounded-[6px] border-grayScale-200 font-bold text-grayScale-600 shadow-sm"
> >
Back Back
</Button> </Button>
<Button <Button
onClick={nextStep} onClick={nextStep}
className="h-12 rounded-xl bg-brand-500 px-8 font-bold hover:bg-brand-600 shadow-md shadow-brand-500/20" className="h-10 rounded-[6px] bg-brand-500 px-8 font-bold "
> >
Next: Review <ArrowRight className="ml-2 h-4 w-4" /> Next: Review <ArrowRight className="ml-2 h-4 w-4" />
</Button> </Button>

View File

@ -1,6 +1,7 @@
import { Edit2, GripVertical, Trash2, Rocket, Info } from "lucide-react"; import { Edit2, GripVertical, Trash2, Rocket, Info } from "lucide-react";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card"; import { Card } from "../../../../components/ui/card";
import { Input } from "../../../../components/ui/input";
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
@ -8,7 +9,6 @@ import {
} from "../../../../components/ui/avatar"; } from "../../../../components/ui/avatar";
import { PERSONAS } from "./constants"; import { PERSONAS } from "./constants";
import { VoicePrompt } from "./VoicePrompt"; import { VoicePrompt } from "./VoicePrompt";
import { cn } from "../../../../lib/utils";
interface ReviewStepProps { interface ReviewStepProps {
formData: any; formData: any;
@ -36,8 +36,8 @@ export function ReviewStep({
</div> </div>
{/* 1. Basic Info Card (Image 1436.1) */} {/* 1. Basic Info Card (Image 1436.1) */}
<Card className="overflow-hidden border border-grayScale-50 shadow-soft rounded-2xl bg-white shadow-sm"> <Card className="overflow-hidden border border-grayScale-200 rounded-2xl bg-white ">
<div className="border-b border-grayScale-50 p-6 px-10 flex justify-between items-center bg-white"> <div className="border-b border-grayScale-50 p-4 px-5 flex justify-between items-center bg-white">
<h3 className="text-[17px] font-extrabold text-grayScale-900"> <h3 className="text-[17px] font-extrabold text-grayScale-900">
Basic Information Basic Information
</h3> </h3>
@ -50,9 +50,26 @@ export function ReviewStep({
Edit Edit
</Button> </Button>
</div> </div>
<div className="p-8 px-10 flex items-center justify-between "> {/* Gradient Divider */}
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-grayScale-100" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
<div className="p-8 px-5 flex items-center justify-between ">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="h-[72px] w-[110px] rounded-xl bg-grayScale-100 overflow-hidden shadow-inner flex-shrink-0"> <div className="h-[70px] w-[85px] rounded-xl bg-grayScale-100 overflow-hidden shadow-inner flex-shrink-0">
<img <img
src="https://images.unsplash.com/photo-1558403194-611308249627?auto=format&fit=crop&q=80&w=200" src="https://images.unsplash.com/photo-1558403194-611308249627?auto=format&fit=crop&q=80&w=200"
alt="Banner" alt="Banner"
@ -60,21 +77,17 @@ export function ReviewStep({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-[22px] font-extrabold text-grayScale-900 leading-tight"> <h4 className="text-[22px] font-bold text-grayScale-900 leading-tight">
{formData.title || "Business English 101: Communication"} {formData.title || "Business English 101: Communication"}
</h4> </h4>
<div className="flex items-center gap-6 text-[14px]"> <div className="flex items-center gap-6 text-[14px]">
<span className="text-grayScale-900 font-bold"> <span className="text-grayScale-900 ">
Program:{" "} Program:{" "}
<span className="text-brand-500 font-extrabold"> <span className="text-brand-500 ">{formData.program}</span>
{formData.program}
</span>
</span> </span>
<span className="text-grayScale-900 font-bold"> <span className="text-grayScale-900 ">
Course:{" "} Course:{" "}
<span className="text-brand-500 font-extrabold"> <span className="text-brand-500 ">{formData.course}</span>
{formData.course}
</span>
</span> </span>
<span className="text-grayScale-900 font-bold"> <span className="text-grayScale-900 font-bold">
Module:{" "} Module:{" "}
@ -86,15 +99,15 @@ export function ReviewStep({
</div> </div>
</div> </div>
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<span className="text-[11px] font-bold text-grayScale-900 uppercase tracking-widest"> <span className="text-[11px] text-left font-medium text-grayScale-900 ">
Persona Persona
</span> </span>
<div className="flex items-center gap-2 bg-[#FAF5FF] p-2 pl-2.5 pr-4 rounded-full border border-brand-100/30"> <div className="flex items-center gap-2 bg-[#FAF5FF] py-1 pl-2.5 pr-4 rounded-full border border-brand-100/30">
<Avatar className="h-8 w-8 border-2 border-white shadow-sm font-bold"> <Avatar className="h-8 w-8 border-2 border-white shadow-sm font-bold">
<AvatarImage src={persona?.avatar} /> <AvatarImage src={persona?.avatar} />
<AvatarFallback>P</AvatarFallback> <AvatarFallback>P</AvatarFallback>
</Avatar> </Avatar>
<span className="text-[14px] font-extrabold text-brand-500 capitalize"> <span className="text-[14px] text-brand-500 capitalize">
{persona?.name || "Alex Johnson"} {persona?.name || "Alex Johnson"}
</span> </span>
</div> </div>
@ -120,18 +133,18 @@ export function ReviewStep({
{isModuleContext ? ( {isModuleContext ? (
/* 3. Split Questions & Answers Layout (Image 1413.1) */ /* 3. Split Questions & Answers Layout (Image 1413.1) */
<div className="grid grid-cols-1 md:grid-cols-2 bg-white rounded-[24px] border border-grayScale-50 shadow-sm overflow-hidden min-h-[600px]"> <div className="grid grid-cols-1 md:grid-cols-2 bg-white rounded-[12px] border border-grayScale-50 shadow-sm overflow-hidden min-h-[600px]">
{/* Left Column: Questions */} {/* Left Column: Questions */}
<div className="border-r border-grayScale-50 flex flex-col"> <div className="border-r border-grayScale-200 flex flex-col">
<div className="p-6 px-10 border-b border-grayScale-50 flex items-center gap-3 bg-white"> <div className="p-4 border-b border-grayScale-50 flex items-center gap-3 bg-white">
<h3 className="text-[16px] font-extrabold text-[#0F172A]"> <h3 className="text-[16px] font-extrabold text-[#0F172A]">
Questions Questions
</h3> </h3>
<span className="h-6 w-6 rounded-full bg-grayScale-50 flex items-center justify-center text-[12px] font-extrabold text-grayScale-300"> <span className="h-6 w-6 rounded-full bg-grayScale-100 flex items-center justify-center text-[12px] font-extrabold text-grayScale-500">
{formData.questions.length} {formData.questions.length}
</span> </span>
</div> </div>
<div className="p-10 space-y-14"> <div className="p-4 space-y-14">
{formData.questions.map((q: any, i: number) => ( {formData.questions.map((q: any, i: number) => (
<div key={q.id} className="relative pl-12"> <div key={q.id} className="relative pl-12">
<span className="absolute left-0 top-0 text-[18px] font-bold text-grayScale-400 tracking-tighter opacity-70"> <span className="absolute left-0 top-0 text-[18px] font-bold text-grayScale-400 tracking-tighter opacity-70">
@ -163,19 +176,19 @@ export function ReviewStep({
{/* Right Column: Answers */} {/* Right Column: Answers */}
<div className="flex flex-col"> <div className="flex flex-col">
<div className="p-6 px-10 border-b border-grayScale-50 flex items-center justify-between bg-white"> <div className="p-4 border-b border-grayScale-50 flex items-center justify-between bg-white">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h3 className="text-[16px] font-extrabold ">Answers</h3> <h3 className="text-[16px] font-extrabold ">Answers</h3>
<span className="h-6 w-6 rounded-full bg-grayScale-50 flex items-center justify-center text-[12px] font-extrabold text-brand-300"> <span className="h-6 w-6 rounded-full bg-grayScale-100 flex items-center justify-center text-[12px] font-extrabold text-grayScale-500">
{formData.questions.length} {formData.questions.length}
</span> </span>
</div> </div>
<button className="flex items-center gap-2 text-brand-500 font-bold text-[15px] hover:opacity-80 transition-opacity"> <button className="flex items-center gap-2 text-brand-500 font-bold text-[15px] hover:opacity-80 transition-opacity">
<Edit2 className="h-4 w-4" /> <Edit2 className="h-3 w-3" />
Edit Edit
</button> </button>
</div> </div>
<div className="p-10 space-y-14"> <div className="p-4 space-y-14">
{formData.questions.map((q: any, i: number) => ( {formData.questions.map((q: any, i: number) => (
<div key={q.id + "_ans"} className="relative pl-12"> <div key={q.id + "_ans"} className="relative pl-12">
<span className="absolute left-0 top-0 text-[18px] font-bold text-grayScale-400 tracking-tighter opacity-70"> <span className="absolute left-0 top-0 text-[18px] font-bold text-grayScale-400 tracking-tighter opacity-70">
@ -187,7 +200,7 @@ export function ReviewStep({
</span> </span>
<VoicePrompt <VoicePrompt
filename={q.sampleAnswer} filename={q.sampleAnswer}
className="bg-[#FAF5FF]/60 border-[#F3E8FF] h-[72px]" className="bg-[#FAF5FF]/60 border-[#F3E8FF] h-[60px]"
/> />
</div> </div>
</div> </div>
@ -209,20 +222,20 @@ export function ReviewStep({
<Button <Button
onClick={prevStep} onClick={prevStep}
variant="outline" variant="outline"
className="h-12 px-10 rounded-xl border-grayScale-200 font-bold text-grayScale-600 bg-white shadow-sm hover:bg-grayScale-50 transition-all text-sm" className="h-10 px-10 rounded-[6px] border-grayScale-200 font-bold text-grayScale-600 bg-white shadow-sm hover:bg-grayScale-50 transition-all text-sm"
> >
Back Back
</Button> </Button>
<div className="flex gap-4"> <div className="flex gap-4">
<Button <Button
variant="outline" variant="outline"
className="h-12 px-8 rounded-xl border-grayScale-100 font-bold text-grayScale-600 bg-white shadow-sm hover:bg-grayScale-50 transition-all text-sm" className="h-10 px-8 rounded-[6px] border-grayScale-100 font-bold text-grayScale-600 bg-white shadow-sm hover:bg-grayScale-50 transition-all text-sm"
> >
Save as Draft Save as Draft
</Button> </Button>
<Button <Button
onClick={() => setIsPublished(true)} onClick={() => setIsPublished(true)}
className="h-12 px-10 rounded-xl bg-brand-500 font-bold hover:bg-brand-600 shadow-xl shadow-brand-500/20 gap-3 active:scale-95 transition-all text-white text-sm" className="h-10 px-10 rounded-[6px] bg-brand-500 font-bold hover:bg-brand-600 shadow-xl shadow-brand-500/20 gap-3 active:scale-95 transition-all text-white text-sm"
> >
<Rocket className="h-4 w-4" /> <Rocket className="h-4 w-4" />
Publish Now Publish Now
@ -235,53 +248,55 @@ export function ReviewStep({
function ReviewItem({ q, index }: { q: any; index: number }) { function ReviewItem({ q, index }: { q: any; index: number }) {
return ( return (
<Card className="overflow-hidden border border-grayScale-50 shadow-sm rounded-2xl bg-white relative p-8 group"> <Card className="overflow-hidden border-grayScale-50 shadow-soft rounded-2xl bg-white relative">
<div className="absolute left-0 top-0 bottom-0 w-[5px] bg-brand-500 opacity-0 group-hover:opacity-100 transition-opacity" /> <div className="absolute left-0 top-0 bottom-0 w-[5px] bg-brand-500" />
<div className="space-y-8"> <div className="px-5 pb-7 pt-2 space-y-6">
<div className="flex items-center justify-between border-b border-grayScale-50 pb-6"> <div className="flex items-center justify-between border-b border-grayScale-50 pb-4 mb-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3">
<GripVertical className="h-4 w-4 text-grayScale-200" /> <GripVertical className="h-5 w-5 text-brand-500 cursor-grab" />
<span className="font-bold text-grayScale-800 text-[18px]"> <span className="font-bold text-grayScale-500 text-base">
Question {index + 1} Question {index + 1}
</span> </span>
</div> </div>
<div className="flex items-center gap-3"> <Button
<button className="h-9 w-9 rounded-lg border border-grayScale-100 flex items-center justify-center text-grayScale-400 hover:text-brand-500 hover:border-brand-100 transition-all"> variant="ghost"
<Edit2 className="h-4 w-4" /> size="icon"
</button> className="text-brand-500 hover:bg-brand-50 rounded-lg"
<button className="h-9 w-9 rounded-lg border border-grayScale-100 flex items-center justify-center text-grayScale-400 hover:text-red-500 hover:border-red-100 transition-all"> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</button> </Button>
</div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12"> <div className="md:col-span-8 space-y-3">
<div className="space-y-4"> <label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest">
<span className="text-[11px] font-extrabold text-grayScale-300 uppercase tracking-widest block"> QUESTION PROMPT
TEXT PROMPT </label>
</span> <Input
<p className="text-[16px] font-medium text-grayScale-600 leading-relaxed shadow-sm p-6 border border-grayScale-50 rounded-2xl bg-[#F8FAFC]/30"> value={q.text}
{q.text} readOnly
</p> className="h-16 rounded-xl border-grayScale-200 font-medium px-6 text-base placeholder:text-grayScale-400 bg-white text-grayScale-700"
placeholder="e.g. How long have you been studying English?"
/>
</div> </div>
<div className="space-y-4"> <div className="md:col-span-4 space-y-3">
<span className="text-[11px] font-extrabold text-grayScale-300 uppercase tracking-widest block"> <label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest">
VOICE PROMPT VOICE PROMPT
</span> </label>
<VoicePrompt <VoicePrompt
src={q.voicePrompt}
filename={q.voicePrompt} filename={q.voicePrompt}
className="bg-[#FAF5FF]/60 border-[#F3E8FF] h-[76px]" onRemove={() => {}}
/> />
</div> </div>
</div> </div>
<div className="md:w-1/3 space-y-3">
<div className="space-y-4 max-w-sm"> <label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest">
<span className="text-[11px] font-extrabold text-grayScale-300 uppercase tracking-widest block"> SAMPLE ANSWER PROMPT
SAMPLE ANSWER </label>
</span>
<VoicePrompt <VoicePrompt
src={q.sampleAnswer}
filename={q.sampleAnswer} filename={q.sampleAnswer}
className="bg-grayScale-50/10 border-dashed h-[72px]" onRemove={() => {}}
/> />
</div> </div>
</div> </div>

View File

@ -27,44 +27,43 @@ export function ScenarioStep({
Set the scene and context for this English practice session. Set the scene and context for this English practice session.
</p> </p>
</div> </div>
<Card className="p-8 space-y-6 border-grayScale-50 shadow-soft rounded-2xl bg-white"> <Card className="p-8 space-y-6 border-grayScale-200 rounded-2xl bg-white">
<div className="space-y-2"> <div className="space-y-1">
<label className="text-sm font-bold text-grayScale-700"> <label className="text-sm text-grayScale-700">
Practice Banner Image Practice Banner Image
</label> </label>
<p className="text-xs text-grayScale-400"> <p className="text-xs pb-2 text-grayScale-400">
This image will appear as the background for the scenario. This image will appear as the background for the scenario.
</p> </p>
<div className="mt-4 flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-grayScale-100 bg-[#F8F9FA] p-12 hover:bg-grayScale-50 transition-all"> <div className="mt-4 flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-grayScale-200 bg-[#F8F9FA] p-12 hover:bg-grayScale-50 transition-all">
<div className="mb-4 rounded-xl border border-grayScale-100 bg-white p-3 text-brand-500 shadow-sm"> <div className="mb-4 rounded-xl border border-grayScale-100 bg-white p-3 text-brand-500 shadow-sm">
<Upload className="h-6 w-6" /> <Upload className="h-6 w-6" />
</div> </div>
<p className="text-sm"> <p className="text-sm">
<span className="font-bold text-grayScale-700"> <span className="text-grayScale-700">
Click to upload Click to upload or drag and drop
</span>{" "} </span>
<span className="text-grayScale-500">or drag and drop</span>
</p> </p>
<p className="mt-1 text-xs text-grayScale-400 uppercase tracking-wide font-bold"> <p className="mt-1 text-xs text-grayScale-400 uppercase tracking-wide ">
SVG, PNG, JPG (MAX 5MB) SVG, PNG, JPG (MAX 5MB)
</p> </p>
<Button <Button
variant="outline" variant="outline"
className="mt-6 h-10 rounded-xl border-grayScale-200 bg-white px-8 font-bold text-grayScale-600 shadow-sm hover:bg-grayScale-50" className="mt-6 h-10 rounded-[6px] border-grayScale-200 bg-white px-8 font-bold text-brand-500 shadow-sm hover:bg-grayScale-50"
> >
Browse Files Browse Files
</Button> </Button>
</div> </div>
</div> </div>
</Card> </Card>
<Card className="p-8 space-y-6 border-grayScale-50 shadow-soft rounded-2xl bg-white"> <Card className="p-8 space-y-6 border-grayScale-200 rounded-2xl bg-white">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-bold text-grayScale-700"> <label className="text-sm font-medium text-grayScale-700">
Practice Title <span className="text-red-500">*</span> Practice Title <span className="text-red-500">*</span>
</label> </label>
<Input <Input
placeholder="e.g., Ordering Coffee at a Cafe" placeholder="e.g., Ordering Coffee at a Cafe"
className="h-14 rounded-xl border-grayScale-200 focus:border-brand-500 font-bold placeholder:text-grayScale-300 bg-white" className="h-12 rounded-xl border-grayScale-200 focus:border-brand-500 placeholder:text-grayScale-500 bg-white"
value={formData.title} value={formData.title}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, title: e.target.value }) setFormData({ ...formData, title: e.target.value })
@ -72,13 +71,13 @@ export function ScenarioStep({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-bold text-grayScale-700"> <label className="text-sm font-medium text-grayScale-700">
Scenario Description <span className="text-red-500">*</span> Scenario Description <span className="text-red-500">*</span>
</label> </label>
<div className="relative"> <div className="relative">
<Textarea <Textarea
placeholder="Describe the setting..." placeholder="Describe the setting..."
className="min-h-[160px] rounded-xl resize-none p-4 border-grayScale-200 focus:border-brand-500 leading-relaxed font-bold placeholder:text-grayScale-300 bg-white" className="min-h-[160px] rounded-xl resize-none p-4 border-grayScale-200 focus:border-brand-500 leading-relaxed placeholder:text-grayScale-500 bg-white"
maxLength={1000} maxLength={1000}
value={formData.description} value={formData.description}
onChange={(e) => onChange={(e) =>
@ -88,24 +87,28 @@ export function ScenarioStep({
}) })
} }
/> />
<div className="absolute bottom-4 right-4 text-xs font-bold text-grayScale-300"> <div className="absolute bottom-4 right-4 text-xs font-bold text-grayScale-500">
{formData.description.length} / 1000 {formData.description.length} / 1000
</div> </div>
</div> </div>
<span className="text-xs text-grayScale-500">
Provide context for the AI and the student. Be specific about the
location and the goal.
</span>
</div> </div>
</Card> </Card>
<div className="flex items-center justify-between pt-4"> <div className="flex items-center justify-between pt-4">
<Button <Button
onClick={prevStep} onClick={prevStep}
variant="outline" variant="outline"
className="h-12 w-28 rounded-xl border-grayScale-200 font-bold text-grayScale-600 shadow-sm" className="h-10 w-20 rounded-[6px] border-grayScale-200 text-grayScale-600 shadow-sm"
> >
Back Back
</Button> </Button>
<Button <Button
onClick={nextStep} onClick={nextStep}
disabled={!formData.title || !formData.description} disabled={!formData.title || !formData.description}
className="h-12 rounded-xl bg-brand-500 px-8 font-bold hover:bg-brand-600 shadow-md shadow-brand-500/20" className="h-10 rounded-[6px] bg-brand-500 px-8 "
> >
Next: Persona <ArrowRight className="ml-2 h-4 w-4" /> Next: Persona <ArrowRight className="ml-2 h-4 w-4" />
</Button> </Button>

View File

@ -1,44 +1,182 @@
import { Play, X } from "lucide-react"; import { useEffect, useRef, useState } from "react";
import { Button } from "../../../../components/ui/button"; import { Play, Pause, X } from "lucide-react";
import { cn } from "../../../../lib/utils"; import { cn } from "../../../../lib/utils";
interface VoicePromptProps { interface VoicePromptProps {
/** Either a URL/path to the audio file, or a filename string (for display-only mode) */
src?: string;
filename: string; filename: string;
onRemove?: () => void;
className?: string; className?: string;
} }
export function VoicePrompt({ filename, className }: VoicePromptProps) { const BAR_COUNT = 24;
export function VoicePrompt({
src,
filename,
onRemove,
className,
}: VoicePromptProps) {
const [bars, setBars] = useState<number[]>([]);
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0); // 01
const audioRef = useRef<HTMLAudioElement | null>(null);
const rafRef = useRef<number | null>(null);
// ─── Decode audio and build waveform bars ───────────────────────────────────
useEffect(() => {
if (!src) {
// No real audio — generate plausible static bars
setBars(generateFakeBars());
return;
}
let cancelled = false;
const audioCtx = new AudioContext();
fetch(src)
.then((r) => r.arrayBuffer())
.then((buf) => audioCtx.decodeAudioData(buf))
.then((decoded) => {
if (cancelled) return;
const raw = decoded.getChannelData(0);
const blockSize = Math.floor(raw.length / BAR_COUNT);
const barsData = Array.from({ length: BAR_COUNT }, (_, i) => {
let sum = 0;
for (let j = 0; j < blockSize; j++) {
sum += Math.abs(raw[i * blockSize + j]);
}
return sum / blockSize;
});
// Normalize to 01
const max = Math.max(...barsData, 0.001);
setBars(barsData.map((v) => v / max));
})
.catch(() => {
if (!cancelled) setBars(generateFakeBars());
})
.finally(() => audioCtx.close());
return () => {
cancelled = true;
};
}, [src]);
// ─── Sync progress while playing ────────────────────────────────────────────
const startProgressLoop = () => {
const tick = () => {
const el = audioRef.current;
if (!el) return;
setProgress(el.currentTime / (el.duration || 1));
rafRef.current = requestAnimationFrame(tick);
};
rafRef.current = requestAnimationFrame(tick);
};
const stopProgressLoop = () => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
// ─── Play / Pause ────────────────────────────────────────────────────────────
const handlePlayPause = () => {
if (!src) return;
if (!audioRef.current) {
audioRef.current = new Audio(src);
audioRef.current.onended = () => {
setIsPlaying(false);
setProgress(0);
stopProgressLoop();
};
}
if (isPlaying) {
audioRef.current.pause();
stopProgressLoop();
setIsPlaying(false);
} else {
audioRef.current.play().then(() => {
setIsPlaying(true);
startProgressLoop();
});
}
};
// ─── Cleanup on unmount ──────────────────────────────────────────────────────
useEffect(() => {
return () => {
stopProgressLoop();
audioRef.current?.pause();
};
}, []);
const playedBars = Math.round(progress * BAR_COUNT);
return ( return (
<div <div
className={cn( className={cn(
"flex items-center gap-4 p-4 bg-brand-50/5 rounded-xl border border-grayScale-50 h-20 shadow-sm", "flex items-center gap-4 px-4 py-3 bg-[#F9F0FB] rounded-6px border border-brand-100 min-h-[60px]",
className, className,
)} )}
> >
<div className="h-10 w-10 rounded-full bg-brand-500 flex items-center justify-center text-white shadow-md flex-shrink-0 cursor-pointer hover:bg-brand-600 transition-colors"> {/* Play / Pause button */}
<Play className="h-5 w-5 fill-current ml-1" /> <button
</div> onClick={handlePlayPause}
<div className="flex-1 min-w-0"> className="h-8 w-8 flex-shrink-0 rounded-full bg-brand-500 flex items-center justify-center text-white shadow-md hover:bg-brand-600 transition-colors"
<div className="h-6 flex items-end gap-[2px] px-1 overflow-hidden opacity-40 mb-1"> >
{[...Array(20)].map((_, idx) => ( {isPlaying ? (
<Pause className="h-3 w-3 fill-current" />
) : (
<Play className="h-3 w-3 fill-current ml-0.5" />
)}
</button>
{/* Waveform + filename */}
<div className="flex-1 min-w-0 flex flex-col gap-1">
{/* Bars — centered vertically, grow up and down */}
<div className="flex items-center gap-[3.5px] h-6 overflow-hidden">
{(bars.length ? bars : generateFakeBars()).map((v, i) => (
<div <div
key={idx} key={i}
className="w-[3px] bg-brand-500 rounded-full" className="w-[4px] rounded-full flex-shrink-0"
style={{ height: `${Math.random() * 80 + 20}%` }} style={{
height: `${Math.max(v * 100, 14)}%`,
backgroundColor: i < playedBars ? "#9E2891" : "#D5C5DC",
}}
/> />
))} ))}
</div> </div>
<p className="text-[10px] font-bold text-brand-500 truncate"> {/* Filename */}
<p className="text-[11px] font-semibold text-brand-500 truncate">
{filename} {filename}
</p> </p>
</div> </div>
<Button
variant="ghost" {/* Remove button */}
size="icon" {onRemove && (
className="h-8 w-8 text-grayScale-300 rounded-lg" <button
> onClick={onRemove}
<X className="h-5 w-5" color="#9E2891" /> className="flex-shrink-0 text-brand-500 hover:text-brand-700 transition-colors p-1"
</Button> aria-label="Remove"
>
<X className="h-5 w-5" />
</button>
)}
</div> </div>
); );
} }
// ─── Helpers ─────────────────────────────────────────────────────────────────
function generateFakeBars(): number[] {
// Realistic-looking static waveform (peaks in middle, quieter at edges)
return Array.from({ length: BAR_COUNT }, (_, i) => {
const center = BAR_COUNT / 2;
const envelope = 1 - Math.abs(i - center) / center;
return Math.max(0.1, envelope * (0.4 + Math.random() * 0.6));
});
}

View File

@ -1,4 +1,11 @@
import { Rocket, Edit2, Layout } from "lucide-react"; import {
Rocket,
Edit2,
Layout,
Volume2,
Settings,
Maximize2,
} from "lucide-react";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
interface ReviewPublishStepProps { interface ReviewPublishStepProps {
@ -33,17 +40,33 @@ export function ReviewPublishStep({
</div> </div>
</div> </div>
{/* Bottom Controls Placeholder */} {/* Bottom Controls — Matching Image 1884 */}
<div className="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/80 to-transparent"> <div className="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/95 via-black/40 to-transparent space-y-4">
<div className="h-1.5 w-full bg-white/20 rounded-full mb-4 relative overflow-hidden"> {/* Row 1: Seeker and Timestamps */}
<div className="absolute left-0 top-0 bottom-0 w-1/3 bg-brand-500" /> <div className="flex items-center gap-4 text-white">
<div className="absolute left-1/3 top-1/2 -translate-y-1/2 h-3 w-3 rounded-full bg-white shadow-lg" /> <span className="text-[13px] font-medium opacity-90">0:00</span>
<div className="flex-1 h-1 bg-white/20 rounded-full relative cursor-pointer overflow-hidden group/seeker">
<div
className="absolute left-0 top-0 bottom-0 bg-brand-500 rounded-full"
style={{ width: "40%" }}
/>
</div>
<span className="text-[13px] font-medium opacity-90">
12:30
</span>
</div> </div>
<div className="flex items-center justify-between text-white/90 text-sm font-medium">
<span>0:00 / 12:30</span> {/* Row 2: Icons */}
<div className="flex items-center gap-4"> <div className="flex items-center justify-between text-white">
<div className="h-4 w-6 border-2 border-white/40 rounded-[2px]" /> <div className="flex items-center gap-6">
<div className="h-4 w-4 border-2 border-white/40 rounded-full" /> <Volume2 className="h-[22px] w-[22px] opacity-90 cursor-pointer hover:opacity-100 transition-opacity" />
<div className="h-5 w-6 border-2 border-white rounded-[3px] flex items-center justify-center text-[9px] font-bold opacity-90 cursor-pointer hover:opacity-100 transition-opacity">
CC
</div>
</div>
<div className="flex items-center gap-6">
<Settings className="h-5 w-5 opacity-90 cursor-pointer hover:opacity-100 transition-opacity" />
<Maximize2 className="h-5 w-5 opacity-90 cursor-pointer hover:opacity-100 transition-opacity" />
</div> </div>
</div> </div>
</div> </div>
@ -54,14 +77,14 @@ export function ReviewPublishStep({
{/* 2. Content Details Card */} {/* 2. Content Details Card */}
<div className="bg-white rounded-[16px] border border-grayScale-50 shadow-sm overflow-hidden"> <div className="bg-white rounded-[16px] border border-grayScale-50 shadow-sm overflow-hidden">
<div className="px-8 py-5 border-b border-grayScale-50 flex items-center justify-between bg-white"> <div className="px-8 py-5 border-b border-grayScale-50 flex items-center justify-between bg-white">
<h3 className="text-[17px] font-bold text-grayScale-900"> <h3 className="text-[16px] font-bold text-grayScale-900">
Content Details Content Details
</h3> </h3>
<button <button
onClick={prevStep} onClick={prevStep}
className="flex items-center gap-2 text-brand-500 font-bold text-sm hover:opacity-80 transition-opacity" className="flex items-center gap-2 text-brand-500 font-bold text-sm hover:opacity-80 transition-opacity"
> >
<Edit2 className="h-4 w-4" /> <Edit2 className="h-3 w-3" />
Edit Edit
</button> </button>
</div> </div>
@ -70,37 +93,37 @@ export function ReviewPublishStep({
{/* Metadata Grid */} {/* Metadata Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8"> <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="space-y-2"> <div className="space-y-2">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block"> <span className="text-[11px] font-bold text-grayScale-500 uppercase tracking-widest block">
TITLE TITLE
</span> </span>
<p className="text-[15px] font-bold text-grayScale-900"> <p className="text-[15px] font-medium text-grayScale-900">
{formData.title || "Introduction to Past Tense"} {formData.title || "Introduction to Past Tense"}
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block"> <span className="text-[11px] font-bold text-grayScale-500 uppercase tracking-widest block">
ASSIGNED MODULE ASSIGNED MODULE
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Layout className="h-4 w-4 text-grayScale-400" /> <Layout className="h-4 w-4 text-grayScale-400" />
<p className="text-[14px] font-bold text-grayScale-700"> <p className="text-[14px] font-medium text-grayScale-700">
Grammar Basics - Level 1 Grammar Basics - Level 1
</p> </p>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block"> <span className="text-[11px] font-bold text-grayScale-500 uppercase tracking-widest block">
TEACHER NAME TEACHER NAME
</span> </span>
<p className="text-[15px] font-bold text-grayScale-600"> <p className="text-[15px] font-medium text-grayScale-600">
Abebe Kebede Abebe Kebede
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block"> <span className="text-[11px] font-bold text-grayScale-500 uppercase tracking-widest block">
FILE SIZE FILE SIZE
</span> </span>
<div className="flex items-baseline gap-1.5"> <div className="flex items-baseline gap-1.5">
@ -116,11 +139,11 @@ export function ReviewPublishStep({
{/* Description Section */} {/* Description Section */}
<div className="space-y-3"> <div className="space-y-3">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block"> <span className="text-[11px] font-bold text-grayScale-500 uppercase tracking-widest block">
DESCRIPTION DESCRIPTION
</span> </span>
<div <div
className="text-[14px] text-grayScale-600 font-medium leading-relaxed max-w-4xl" className="text-[14px] text-grayScale-600 leading-relaxed max-w-4xl"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: __html:
formData.description || formData.description ||
@ -130,12 +153,30 @@ export function ReviewPublishStep({
</div> </div>
</div> </div>
{/* Gradient Divider */}
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
{/* 3. Normal Footer (Inside Card) */} {/* 3. Normal Footer (Inside Card) */}
<div className="px-8 py-6 border-t border-grayScale-50 flex items-center justify-between bg-white"> <div className="px-8 py-6 border-t border-grayScale-50 flex items-center justify-between bg-white">
<Button <Button
variant="outline" variant="outline"
onClick={prevStep} onClick={prevStep}
className="h-12 px-8 rounded-xl border-grayScale-200 font-bold text-grayScale-600 hover:bg-grayScale-50 transition-all shadow-sm" className="h-10 px-8 rounded-xl border-grayScale-200 font-bold text-grayScale-600 hover:bg-grayScale-50 transition-all shadow-sm"
> >
Back Back
</Button> </Button>
@ -143,13 +184,13 @@ export function ReviewPublishStep({
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button <Button
variant="outline" variant="outline"
className="h-12 px-8 rounded-xl border-grayScale-100 font-bold text-grayScale-600 hover:bg-grayScale-50 transition-all shadow-sm" className="h-12 px-8 rounded-[6px] border-grayScale-100 font-bold text-grayScale-600 hover:bg-grayScale-50 transition-all shadow-sm"
> >
Save as Draft Save as Draft
</Button> </Button>
<Button <Button
onClick={() => setIsPublished(true)} onClick={() => setIsPublished(true)}
className="h-12 px-10 rounded-xl bg-brand-500 font-bold text-white hover:bg-brand-600 shadow-lg shadow-brand-500/20 transition-all flex items-center gap-2.5" className="h-10 px-10 rounded-[6px] bg-brand-500 font-bold text-white shadow-brand-500/20 transition-all flex items-center gap-2.5"
> >
<Rocket className="h-4 w-4" /> <Rocket className="h-4 w-4" />
Publish Now Publish Now

View File

@ -6,6 +6,7 @@ import {
Lightbulb, Lightbulb,
ChevronRight, ChevronRight,
ImageIcon, ImageIcon,
ArrowRight,
} from "lucide-react"; } from "lucide-react";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
import { Input } from "../../../../components/ui/input"; import { Input } from "../../../../components/ui/input";
@ -55,7 +56,7 @@ export function VideoDetailStep({
return ( return (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-700 max-w-[1200px] mx-auto pb-20"> <div className="animate-in fade-in slide-in-from-bottom-4 duration-700 max-w-[1200px] mx-auto pb-20">
{/* Single Unified Card for Everything */} {/* Single Unified Card for Everything */}
<div className="bg-white rounded-[24px] border border-grayScale-50 p-10 shadow-sm space-y-12"> <div className="bg-white rounded-[24px] border border-grayScale-50 p-10 shadow-sm space-y-8">
{/* 1. Upload Video Section */} {/* 1. Upload Video Section */}
<div className="space-y-6"> <div className="space-y-6">
<h3 className="text-[20px] font-bold text-grayScale-900 ml-1"> <h3 className="text-[20px] font-bold text-grayScale-900 ml-1">
@ -71,7 +72,7 @@ export function VideoDetailStep({
</div> </div>
</div> </div>
</div> </div>
<h4 className="text-[17px] font-bold text-grayScale-900 mb-2"> <h4 className="text-[17px] text-grayScale-900 mb-2">
Drag and drop video files here Drag and drop video files here
</h4> </h4>
<p className="text-grayScale-400 font-medium text-[13px] mb-8"> <p className="text-grayScale-400 font-medium text-[13px] mb-8">
@ -79,11 +80,11 @@ export function VideoDetailStep({
</p> </p>
<div className="flex items-center gap-4 w-full max-w-[200px] mb-8"> <div className="flex items-center gap-4 w-full max-w-[200px] mb-8">
<div className="flex-1 h-[1px] bg-grayScale-100" /> <div className="flex-1 h-[1px] bg-grayScale-200" />
<span className="text-[10px] font-bold text-grayScale-300 uppercase tracking-widest"> <span className="text-[12px] font-bold text-grayScale-300 uppercase tracking-widest">
OR OR
</span> </span>
<div className="flex-1 h-[1px] bg-grayScale-100" /> <div className="flex-1 h-[1px] bg-grayScale-200" />
</div> </div>
<Button <Button
@ -95,18 +96,35 @@ export function VideoDetailStep({
</div> </div>
</div> </div>
</div> </div>
{/* Gradient Divider */}
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-grayScale-200" />
</div>
<div className="relative flex justify-center">
<div
className="h-[0.5px] w-full opacity-20 rounded-full"
style={{
background: "gray",
}}
/>
</div>
</div>
{/* 2. Form & Side Panel Grid */} {/* 2. Form & Side Panel Grid */}
<div className="flex flex-col lg:flex-row gap-12 items-start pt-4 border-t border-grayScale-50"> <div className="flex flex-col lg:flex-row gap-12 items-start">
{/* Left Column: Title, Order, Description */} {/* Left Column: Title, Order, Description */}
<div className="flex-1 w-full space-y-10"> <div className="flex-1 w-full space-y-10">
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[14px] font-bold text-grayScale-900 ml-1"> <label className="text-[14px] font-medium text-grayScale-900 ml-1">
Video Title Video Title
</label> </label>
<Input <Input
placeholder="e.g., Introduction to Past Tense Verbs" placeholder="e.g., Introduction to Past Tense Verbs"
className="h-14 rounded-xl border-grayScale-100 bg-white px-6 text-[15px] text-grayScale-800 placeholder:text-grayScale-300 focus:border-brand-500 font-medium transition-all shadow-sm" className="h-12 rounded-xl border-grayScale-200 bg-white px-6 text-[15px] text-grayScale-800 placeholder:text-grayScale-500 focus:border-brand-500 font-medium transition-all shadow-sm"
value={formData.title} value={formData.title}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, title: e.target.value }) setFormData({ ...formData, title: e.target.value })
@ -115,11 +133,11 @@ export function VideoDetailStep({
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[14px] font-bold text-grayScale-900 ml-1"> <label className="text-[14px] font-medium text-grayScale-900 ml-1">
Video Order Video Order
</label> </label>
<Select <Select
className="h-14 rounded-xl border-grayScale-100 bg-white px-6 text-[15px] text-grayScale-800 font-medium cursor-pointer focus:border-brand-500 shadow-sm" className="h-12 rounded-xl border-grayScale-200 bg-white px-6 text-[15px] text-grayScale-800 font-medium cursor-pointer focus:border-brand-500 shadow-sm"
value={formData.order} value={formData.order}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, order: (e.target as any).value }) setFormData({ ...formData, order: (e.target as any).value })
@ -132,12 +150,12 @@ export function VideoDetailStep({
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<label className="text-[14px] font-bold text-grayScale-900 ml-1"> <label className="text-[14px] font-medium text-grayScale-900 ml-1">
Description Description
</label> </label>
<div className="rounded-xl border border-grayScale-100 bg-white overflow-hidden flex flex-col min-h-[380px] shadow-sm focus-within:border-brand-200 transition-all"> <div className="rounded-xl border border-grayScale-200 bg-white overflow-hidden flex flex-col min-h-[200px] shadow-sm focus-within:border-brand-200 transition-all">
{/* Toolbar */} {/* Toolbar */}
<div className="flex items-center gap-1 p-2 bg-[#F8FAFC]"> <div className="flex items-center gap-1 bg-[#F8FAFC]">
<div className="flex items-center gap-1 w-fit bg-transparent px-2 py-1 rounded-lg"> <div className="flex items-center gap-1 w-fit bg-transparent px-2 py-1 rounded-lg">
<button <button
onClick={() => handleCommand("bold")} onClick={() => handleCommand("bold")}
@ -182,8 +200,7 @@ export function VideoDetailStep({
ref={editorRef} ref={editorRef}
contentEditable contentEditable
onInput={handleInput} onInput={handleInput}
className="w-full h-full min-h-[300px] focus:outline-none text-[15px] text-grayScale-700 font-medium leading-relaxed prose prose-sm max-w-none" className="w-full min-h-[140px] focus:outline-none text-[15px] text-grayScale-700 font-medium leading-relaxed prose prose-sm max-w-none"
// Removed dangerouslySetInnerHTML to prevent cursor jumping
/> />
</div> </div>
</div> </div>
@ -191,11 +208,11 @@ export function VideoDetailStep({
</div> </div>
{/* Right Column: Thumbnail, Pro Tip */} {/* Right Column: Thumbnail, Pro Tip */}
<div className="w-full lg:w-[320px] space-y-10"> <div className="w-full lg:w-[320px] space-y-5">
{/* Thumbnail Section */} {/* Thumbnail Section */}
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1 ml-1"> <div className="space-y-1 ml-1">
<h3 className="text-[14px] font-bold text-grayScale-900"> <h3 className="text-[14px] font-medium text-grayScale-900">
Thumbnail Thumbnail
</h3> </h3>
<p className="text-[12px] text-grayScale-400 font-medium leading-relaxed"> <p className="text-[12px] text-grayScale-400 font-medium leading-relaxed">
@ -203,11 +220,11 @@ export function VideoDetailStep({
</p> </p>
</div> </div>
<div className="relative group cursor-pointer aspect-video"> <div className="relative group cursor-pointer aspect-video">
<div className="h-full w-full flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-[#E2E8F0] bg-[#F8FAFC]/50 p-6 transition-all group-hover:border-brand-200"> <div className="h-full w-full flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-grayScale-200 bg-[#F8FAFC]/50 p-6 transition-all group-hover:border-brand-200">
<div className="h-10 w-10 rounded bg-white shadow-sm flex items-center justify-center mb-3"> <div className="h-10 w-10 flex items-center justify-center mb-3">
<ImageIcon className="h-5 w-5 text-grayScale-400" /> <ImageIcon className="h-7 w-7 text-grayScale-400" />
</div> </div>
<p className="text-[13px] font-bold text-brand-500"> <p className="text-[13px] font-bold text-brand-400">
Click to upload Click to upload
</p> </p>
</div> </div>
@ -215,40 +232,38 @@ export function VideoDetailStep({
</div> </div>
{/* Pro Tip Section */} {/* Pro Tip Section */}
<div className="bg-[#FAF5FF] rounded-xl border border-[#F3E8FF] p-6 space-y-3"> <div className="bg-brand-500/5 flex items-start gap-3 rounded-xl border border-[#F3E8FF] p-6 space-y-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-8 w-8 flex-shrink-0 rounded-full bg-white flex items-center justify-center border border-[#F3E8FF] shadow-sm"> <div className="h-8 w-8 flex-shrink-0 flex items-center justify-center">
<Lightbulb className="h-4 w-4 text-brand-50" fill="#A855F7" /> <Lightbulb className="h-4 w-4 text-brand-50" fill="#A855F7" />
<div className="absolute inset-0 flex items-center justify-center">
<Lightbulb className="h-4 w-4 text-brand-500" />
</div>
</div> </div>
</div>
<div className="relative top-[-10px]">
<h3 className="text-[14px] font-bold text-grayScale-900"> <h3 className="text-[14px] font-bold text-grayScale-900">
Pro Tip Pro Tip
</h3> </h3>
<p className="text-[12px] text-grayScale-700 font-medium leading-relaxed">
Short, descriptive titles work best. Include keywords like
"Grammar" or "Vocabulary" to help students find your content.
</p>
</div> </div>
<p className="text-[12px] text-grayScale-700 font-medium leading-relaxed">
Short, descriptive titles work best. Include keywords like
"Grammar" or "Vocabulary" to help students find your content.
</p>
</div> </div>
</div> </div>
</div> </div>
{/* Footer (Inside Card Container) */} {/* Footer (Inside Card Container) */}
<div className="pt-10 border-t border-grayScale-50 flex items-center justify-between"> <div className="pt-5 border-t border-grayScale-200 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-2 w-2 rounded-full bg-brand-500 animate-pulse" /> <span className="text-[14px] font-medium text-grayScale-600">
<span className="text-[14px] font-bold text-grayScale-400">
Last saved: Just now Last saved: Just now
</span> </span>
</div> </div>
<Button <Button
onClick={nextStep} onClick={nextStep}
className="h-12 px-10 rounded-xl bg-brand-500 font-bold text-white hover:bg-brand-600 shadow-lg shadow-brand-500/20 transition-all flex items-center gap-2 text-sm group active:scale-95" className="h-10 px-10 rounded-[6px] bg-brand-500 font-bold text-white transition-all flex items-center gap-2 text-sm group active:scale-95"
> >
Continue Continue
<ChevronRight className="h-4 w-4 transition-transform group-hover:translate-x-1" /> <ArrowRight className="h-4 w-4" />
</Button> </Button>
</div> </div>
</div> </div>