course
This commit is contained in:
parent
1480eefbe6
commit
d4d61bfed2
|
|
@ -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 />}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
193
src/pages/content-management/AttachPracticeFlow.tsx
Normal file
193
src/pages/content-management/AttachPracticeFlow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
157
src/pages/content-management/AttachProgramPracticeFlow.tsx
Normal file
157
src/pages/content-management/AttachProgramPracticeFlow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
264
src/pages/content-management/CourseManagementPage.tsx
Normal file
264
src/pages/content-management/CourseManagementPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
248
src/pages/content-management/CourseModuleDetailPage.tsx
Normal file
248
src/pages/content-management/CourseModuleDetailPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
273
src/pages/content-management/ProgramDetailPage.tsx
Normal file
273
src/pages/content-management/ProgramDetailPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
src/pages/content-management/ProgramTypeSelectionPage.tsx
Normal file
82
src/pages/content-management/ProgramTypeSelectionPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
254
src/pages/content-management/UnitManagementPage.tsx
Normal file
254
src/pages/content-management/UnitManagementPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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); // 0–1
|
||||||
|
|
||||||
|
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 0–1
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user