This commit is contained in:
elnatansamuel25 2026-04-22 17:10:43 +03:00
parent 588b238b49
commit 1480eefbe6
25 changed files with 3238 additions and 176 deletions

10
package-lock.json generated
View File

@ -23,6 +23,7 @@
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-is": "^19.2.5",
"react-router-dom": "^7.10.1", "react-router-dom": "^7.10.1",
"recharts": "^3.6.0", "recharts": "^3.6.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
@ -5315,11 +5316,10 @@
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {
"version": "19.2.3", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz",
"integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/react-redux": { "node_modules/react-redux": {
"version": "9.2.0", "version": "9.2.0",

View File

@ -25,6 +25,7 @@
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-is": "^19.2.5",
"react-router-dom": "^7.10.1", "react-router-dom": "^7.10.1",
"recharts": "^3.6.0", "recharts": "^3.6.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",

View File

@ -1,52 +1,59 @@
import { Navigate, Route, Routes } from "react-router-dom" import { Navigate, Route, Routes } from "react-router-dom";
import { AppLayout } from "../layouts/AppLayout" import { AppLayout } from "../layouts/AppLayout";
import { DashboardPage } from "../pages/DashboardPage" import { DashboardPage } from "../pages/DashboardPage";
import { AnalyticsPage } from "../pages/analytics/AnalyticsPage" import { AnalyticsPage } from "../pages/analytics/AnalyticsPage";
import { ContentManagementLayout } from "../pages/content-management/ContentManagementLayout" import { ContentManagementLayout } from "../pages/content-management/ContentManagementLayout";
import { CourseCategoryPage } from "../pages/content-management/CourseCategoryPage" import { CourseCategoryPage } from "../pages/content-management/CourseCategoryPage";
import { AllCoursesPage } from "../pages/content-management/AllCoursesPage" import { AllCoursesPage } from "../pages/content-management/AllCoursesPage";
import { CourseFlowBuilderPage } from "../pages/content-management/CourseFlowBuilderPage" import { CourseFlowBuilderPage } from "../pages/content-management/CourseFlowBuilderPage";
import { ContentOverviewPage } from "../pages/content-management/ContentOverviewPage" import { ContentOverviewPage } from "../pages/content-management/ContentOverviewPage";
import { CoursesPage } from "../pages/content-management/CoursesPage" import { CoursesPage } from "../pages/content-management/CoursesPage";
import { PracticeQuestionsPage } from "../pages/content-management/PracticeQuestionsPage" import { PracticeQuestionsPage } from "../pages/content-management/PracticeQuestionsPage";
import { AddNewPracticePage } from "../pages/content-management/AddNewPracticePage" import { AddNewPracticePage } from "../pages/content-management/AddNewPracticePage";
import { SubModulesPage } from "../pages/content-management/SubCoursesPage" import { SubModulesPage } from "../pages/content-management/SubCoursesPage";
import { SubModuleContentPage } from "../pages/content-management/SubCourseContentPage" import { SubModuleContentPage } from "../pages/content-management/SubCourseContentPage";
import { SpeakingPage } from "../pages/content-management/SpeakingPage" import { SpeakingPage } from "../pages/content-management/SpeakingPage";
import { AddVideoPage } from "../pages/content-management/AddVideoPage" import { AddVideoPage } from "../pages/content-management/AddVideoPage";
import { AddPracticePage } from "../pages/content-management/AddPracticePage" import { AddPracticePage } from "../pages/content-management/AddPracticePage";
import { NotFoundPage } from "../pages/NotFoundPage" import { NewContentPage } from "../pages/content-management/NewContentPage";
import { NotificationsPage } from "../pages/notifications/NotificationsPage" import { LearnEnglishPage } from "../pages/content-management/LearnEnglishPage";
import { CreateNotificationPage } from "../pages/notifications/CreateNotificationPage" import { ProgramCoursesPage } from "../pages/content-management/ProgramCoursesPage";
import { UserDetailPage } from "../pages/user-management/UserDetailPage" import { CourseDetailPage } from "../pages/content-management/CourseDetailPage";
import { UserManagementLayout } from "../pages/user-management/UserManagementLayout" import { ModuleDetailPage } from "../pages/content-management/ModuleDetailPage";
import { UsersListPage } from "../pages/user-management/UsersListPage" import { AddVideoFlow } from "../pages/content-management/AddVideoFlow";
import { UserManagementDashboard } from "../pages/user-management/UserManagementDashboard" import { AddPracticeFlow } from "../pages/content-management/AddPracticeFlow";
import { UserGroupsPage } from "../pages/user-management/UserGroupsPage" import { NotFoundPage } from "../pages/NotFoundPage";
import { DeletionRequestsPage } from "../pages/user-management/DeletionRequestsPage" import { NotificationsPage } from "../pages/notifications/NotificationsPage";
import { RoleManagementLayout } from "../pages/role-management/RoleManagementLayout" import { CreateNotificationPage } from "../pages/notifications/CreateNotificationPage";
import { RolesListPage } from "../pages/role-management/RolesListPage" import { UserDetailPage } from "../pages/user-management/UserDetailPage";
import { AddRolePage } from "../pages/role-management/AddRolePage" import { UserManagementLayout } from "../pages/user-management/UserManagementLayout";
import { PracticeDetailsPage } from "../pages/content-management/PracticeDetailsPage" import { UsersListPage } from "../pages/user-management/UsersListPage";
import { PracticeMembersPage } from "../pages/content-management/PracticeMembersPage" import { UserManagementDashboard } from "../pages/user-management/UserManagementDashboard";
import { QuestionsPage } from "../pages/content-management/QuestionsPage" import { UserGroupsPage } from "../pages/user-management/UserGroupsPage";
import { AddQuestionPage } from "../pages/content-management/AddQuestionPage" import { DeletionRequestsPage } from "../pages/user-management/DeletionRequestsPage";
import { HumanLanguagePage } from "../pages/content-management/HumanLanguagePage" import { RoleManagementLayout } from "../pages/role-management/RoleManagementLayout";
import { HumanLanguageSubModulePage } from "../pages/content-management/HumanLanguageSubModulePage" import { RolesListPage } from "../pages/role-management/RolesListPage";
import { UserLogPage } from "../pages/user-log/UserLogPage" import { AddRolePage } from "../pages/role-management/AddRolePage";
import { IssuesPage } from "../pages/issues/IssuesPage" import { PracticeDetailsPage } from "../pages/content-management/PracticeDetailsPage";
import { ProfilePage } from "../pages/ProfilePage" import { PracticeMembersPage } from "../pages/content-management/PracticeMembersPage";
import { SettingsPage } from "../pages/SettingsPage" import { QuestionsPage } from "../pages/content-management/QuestionsPage";
import { TeamManagementPage } from "../pages/team/TeamManagementPage" import { AddQuestionPage } from "../pages/content-management/AddQuestionPage";
import { AddTeamMemberPage } from "../pages/team/AddTeamMemberPage" import { HumanLanguagePage } from "../pages/content-management/HumanLanguagePage";
import { TeamMemberDetailPage } from "../pages/team/TeamMemberDetailPage" import { HumanLanguageSubModulePage } from "../pages/content-management/HumanLanguageSubModulePage";
import { LoginPage } from "../pages/auth/LoginPage" import { UserLogPage } from "../pages/user-log/UserLogPage";
import { ForgotPasswordPage } from "../pages/auth/ForgotPasswordPage" import { IssuesPage } from "../pages/issues/IssuesPage";
import { VerificationPage } from "../pages/auth/VerificationPage" import { ProfilePage } from "../pages/ProfilePage";
import { AboutPage } from "../pages/AboutPage" import { SettingsPage } from "../pages/SettingsPage";
import { TermsPage } from "../pages/TermsPage" import { TeamManagementPage } from "../pages/team/TeamManagementPage";
import { PrivacyPage } from "../pages/PrivacyPage" import { AddTeamMemberPage } from "../pages/team/AddTeamMemberPage";
import { AccountDeletionPage } from "../pages/AccountDeletionPage" import { TeamMemberDetailPage } from "../pages/team/TeamMemberDetailPage";
import { LoginPage } from "../pages/auth/LoginPage";
import { ForgotPasswordPage } from "../pages/auth/ForgotPasswordPage";
import { VerificationPage } from "../pages/auth/VerificationPage";
import { AboutPage } from "../pages/AboutPage";
import { TermsPage } from "../pages/TermsPage";
import { PrivacyPage } from "../pages/PrivacyPage";
import { AccountDeletionPage } from "../pages/AccountDeletionPage";
export function AppRoutes() { export function AppRoutes() {
return ( return (
@ -91,19 +98,52 @@ export function AppRoutes() {
path="human-language/:categoryId/:courseId/sub-module/:subModuleId" path="human-language/:categoryId/:courseId/sub-module/:subModuleId"
element={<HumanLanguageSubModulePage />} element={<HumanLanguageSubModulePage />}
/> />
<Route path="category/:categoryId" element={<ContentOverviewPage />} /> <Route
<Route path="category/:categoryId/courses" element={<CoursesPage />} /> path="category/:categoryId"
element={<ContentOverviewPage />}
/>
<Route
path="category/:categoryId/courses"
element={<CoursesPage />}
/>
{/* Course → Sub-module → Lesson/Practice */} {/* Course → Sub-module → Lesson/Practice */}
<Route path="category/:categoryId/courses/:courseId/sub-modules" element={<SubModulesPage />} /> <Route
<Route path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId" element={<SubModuleContentPage />} /> path="category/:categoryId/courses/:courseId/sub-modules"
<Route path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId/add-practice" element={<AddNewPracticePage />} /> element={<SubModulesPage />}
<Route path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId/practices/:practiceId/questions" element={<PracticeQuestionsPage />} /> />
<Route
path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId"
element={<SubModuleContentPage />}
/>
<Route
path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId/add-practice"
element={<AddNewPracticePage />}
/>
<Route
path="category/:categoryId/courses/:courseId/sub-modules/:subModuleId/practices/:practiceId/questions"
element={<PracticeQuestionsPage />}
/>
{/* Legacy aliases */} {/* Legacy aliases */}
<Route path="category/:categoryId/courses/:courseId/sub-courses" element={<SubModulesPage />} /> <Route
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId" element={<SubModuleContentPage />} /> path="category/:categoryId/courses/:courseId/sub-courses"
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId/add-practice" element={<AddNewPracticePage />} /> element={<SubModulesPage />}
<Route path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId/practices/:practiceId/questions" element={<PracticeQuestionsPage />} /> />
<Route path="category/:categoryId/courses/add-video" element={<AddVideoPage />} /> <Route
path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId"
element={<SubModuleContentPage />}
/>
<Route
path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId/add-practice"
element={<AddNewPracticePage />}
/>
<Route
path="category/:categoryId/courses/:courseId/sub-courses/:subModuleId/practices/:practiceId/questions"
element={<PracticeQuestionsPage />}
/>
<Route
path="category/:categoryId/courses/add-video"
element={<AddVideoPage />}
/>
<Route path="speaking" element={<SpeakingPage />} /> <Route path="speaking" element={<SpeakingPage />} />
<Route path="speaking/add-practice" element={<AddPracticePage />} /> <Route path="speaking/add-practice" element={<AddPracticePage />} />
<Route path="practices" element={<PracticeDetailsPage />} /> <Route path="practices" element={<PracticeDetailsPage />} />
@ -113,8 +153,37 @@ export function AppRoutes() {
<Route path="questions/edit/:id" element={<AddQuestionPage />} /> <Route path="questions/edit/:id" element={<AddQuestionPage />} />
</Route> </Route>
<Route path="/new-content" element={<NewContentPage />} />
<Route
path="/new-content/learn-english"
element={<LearnEnglishPage />}
/>
<Route
path="/new-content/learn-english/:level/courses"
element={<ProgramCoursesPage />}
/>
<Route
path="/new-content/learn-english/:level/courses/:courseId"
element={<CourseDetailPage />}
/>
<Route
path="/new-content/learn-english/:level/courses/:courseId/modules/:moduleId"
element={<ModuleDetailPage />}
/>
<Route
path="/new-content/learn-english/:level/courses/:courseId/modules/:moduleId/add-video"
element={<AddVideoFlow />}
/>
<Route
path="/new-content/learn-english/:level/courses/add-practice"
element={<AddPracticeFlow />}
/>
<Route path="/notifications" element={<NotificationsPage />} /> <Route path="/notifications" element={<NotificationsPage />} />
<Route path="/notifications/create" element={<CreateNotificationPage />} /> <Route
path="/notifications/create"
element={<CreateNotificationPage />}
/>
<Route path="/user-log" element={<UserLogPage />} /> <Route path="/user-log" element={<UserLogPage />} />
<Route path="/issues" element={<IssuesPage />} /> <Route path="/issues" element={<IssuesPage />} />
<Route path="/analytics" element={<AnalyticsPage />} /> <Route path="/analytics" element={<AnalyticsPage />} />
@ -128,7 +197,5 @@ export function AppRoutes() {
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
) );
} }

BIN
src/assets/icons/upload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -13,57 +13,65 @@ import {
Users, Users,
Users2, Users2,
X, X,
} from "lucide-react" } from "lucide-react";
import { type ComponentType, useEffect, useState } from "react" import { type ComponentType, useEffect, useState } from "react";
import { NavLink } from "react-router-dom" import { NavLink } from "react-router-dom";
import { cn } from "../../lib/utils" import { cn } from "../../lib/utils";
import { BrandLogo } from "../brand/BrandLogo" import { BrandLogo } from "../brand/BrandLogo";
import { getUnreadCount } from "../../api/notifications.api" import { getUnreadCount } from "../../api/notifications.api";
type NavItem = { type NavItem = {
label: string label: string;
to: string to: string;
icon: ComponentType<{ className?: string }> icon: ComponentType<{ className?: string }>;
} };
const navItems: NavItem[] = [ const navItems: NavItem[] = [
{ label: "Dashboard", to: "/dashboard", icon: LayoutDashboard }, { label: "Dashboard", to: "/dashboard", icon: LayoutDashboard },
{ label: "User Management", to: "/users", icon: Users }, { label: "User Management", to: "/users", icon: Users },
{ label: "Role Management", to: "/roles", icon: Shield }, { label: "Role Management", to: "/roles", icon: Shield },
{ label: "Content Management", to: "/content", icon: BookOpen }, { label: "Content Management", to: "/content", icon: BookOpen },
{ label: "New Content", to: "/new-content", icon: BookOpen },
{ label: "Notifications", to: "/notifications", icon: Bell }, { label: "Notifications", to: "/notifications", icon: Bell },
{ label: "User Log", to: "/user-log", icon: ClipboardList }, { label: "User Log", to: "/user-log", icon: ClipboardList },
{ label: "Issue Reports", to: "/issues", icon: CircleAlert }, { label: "Issue Reports", to: "/issues", icon: CircleAlert },
{ label: "Analytics", to: "/analytics", icon: BarChart3 }, { label: "Analytics", to: "/analytics", icon: BarChart3 },
{ label: "Team Management", to: "/team", icon: Users2 }, { label: "Team Management", to: "/team", icon: Users2 },
{ label: "Profile", to: "/profile", icon: UserCircle2 }, { label: "Profile", to: "/profile", icon: UserCircle2 },
] ];
type SidebarProps = { type SidebarProps = {
isOpen: boolean isOpen: boolean;
isCollapsed: boolean isCollapsed: boolean;
onToggleCollapse: () => void onToggleCollapse: () => void;
onClose: () => void onClose: () => void;
} };
export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: SidebarProps) { export function Sidebar({
const [unreadCount, setUnreadCount] = useState(0) isOpen,
isCollapsed,
onToggleCollapse,
onClose,
}: SidebarProps) {
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => { useEffect(() => {
const fetchUnread = async () => { const fetchUnread = async () => {
try { try {
const res = await getUnreadCount() const res = await getUnreadCount();
setUnreadCount(res.data.unread) setUnreadCount(res.data.unread);
} catch { } catch {
// silently fail // silently fail
} }
} };
fetchUnread() fetchUnread();
window.addEventListener("notifications-updated", fetchUnread) window.addEventListener("notifications-updated", fetchUnread);
return () => window.removeEventListener("notifications-updated", fetchUnread) return () =>
}, []) window.removeEventListener("notifications-updated", fetchUnread);
}, []);
return ( return (
<> <>
@ -86,7 +94,12 @@ export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: Side
isOpen ? "translate-x-0" : "-translate-x-full", isOpen ? "translate-x-0" : "-translate-x-full",
)} )}
> >
<div className={cn("flex items-center justify-between px-2", isCollapsed && "justify-center")}> <div
className={cn(
"flex items-center justify-between px-2",
isCollapsed && "justify-center",
)}
>
{isCollapsed ? ( {isCollapsed ? (
<span className="h-10 w-10 overflow-hidden"> <span className="h-10 w-10 overflow-hidden">
<BrandLogo className="h-10 w-auto max-w-none" /> <BrandLogo className="h-10 w-auto max-w-none" />
@ -103,7 +116,11 @@ export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: Side
onClick={onToggleCollapse} onClick={onToggleCollapse}
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"} aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
> >
{isCollapsed ? <ChevronRight className="h-5 w-5" /> : <ChevronLeft className="h-5 w-5" />} {isCollapsed ? (
<ChevronRight className="h-5 w-5" />
) : (
<ChevronLeft className="h-5 w-5" />
)}
</button> </button>
<button <button
type="button" type="button"
@ -117,7 +134,7 @@ export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: Side
<nav className="mt-6 flex-1 space-y-1 overflow-y-auto"> <nav className="mt-6 flex-1 space-y-1 overflow-y-auto">
{navItems.map((item) => { {navItems.map((item) => {
const Icon = item.icon const Icon = item.icon;
return ( return (
<NavLink <NavLink
key={item.to} key={item.to}
@ -143,25 +160,36 @@ export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: Side
)} )}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
{isCollapsed && item.to === "/notifications" && unreadCount > 0 && ( {isCollapsed &&
<span className="absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full bg-destructive" /> item.to === "/notifications" &&
)} unreadCount > 0 && (
<span className="absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full bg-destructive" />
)}
</span> </span>
{!isCollapsed && <span className="truncate">{item.label}</span>} {!isCollapsed && (
{!isCollapsed && item.to === "/notifications" && unreadCount > 0 && ( <span className="truncate">{item.label}</span>
<span className="ml-auto flex h-5 min-w-[20px] items-center justify-center rounded-full bg-destructive px-1.5 text-[10px] font-bold text-white">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)} )}
{!isCollapsed && item.to !== "/notifications" && isActive ? ( {!isCollapsed &&
item.to === "/notifications" &&
unreadCount > 0 && (
<span className="ml-auto flex h-5 min-w-[20px] items-center justify-center rounded-full bg-destructive px-1.5 text-[10px] font-bold text-white">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
{!isCollapsed &&
item.to !== "/notifications" &&
isActive ? (
<span className="ml-auto h-6 w-1 rounded-full bg-brand-500/80" /> <span className="ml-auto h-6 w-1 rounded-full bg-brand-500/80" />
) : !isCollapsed && item.to === "/notifications" && unreadCount === 0 && isActive ? ( ) : !isCollapsed &&
item.to === "/notifications" &&
unreadCount === 0 &&
isActive ? (
<span className="ml-auto h-6 w-1 rounded-full bg-brand-500/80" /> <span className="ml-auto h-6 w-1 rounded-full bg-brand-500/80" />
) : null} ) : null}
</> </>
)} )}
</NavLink> </NavLink>
) );
})} })}
</nav> </nav>
@ -169,8 +197,8 @@ export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: Side
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
localStorage.clear() localStorage.clear();
window.location.href = "/login" window.location.href = "/login";
}} }}
className={cn( className={cn(
"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-grayScale-500 hover:bg-grayScale-100 hover:text-brand-600", "flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-grayScale-500 hover:bg-grayScale-100 hover:text-brand-600",
@ -184,5 +212,5 @@ export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: Side
</div> </div>
</aside> </aside>
</> </>
) );
} }

View File

@ -1,21 +1,21 @@
import * as React from "react" import * as React from "react";
import { cn } from "../../lib/utils" import { cn } from "../../lib/utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => { export const Input = React.forwardRef<HTMLInputElement, InputProps>(
return ( ({ className, type, ...props }, ref) => {
<input return (
type={type} <input
className={cn( type={type}
"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-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", className={cn(
className, "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",
)} className,
ref={ref} )}
{...props} ref={ref}
/> {...props}
) />
}) );
Input.displayName = "Input" },
);
Input.displayName = "Input";

View File

@ -1,61 +1,52 @@
import * as React from "react" import { cn } from "../../lib/utils";
import { Check } from "lucide-react"
import { cn } from "../../lib/utils"
export interface StepperProps { export interface StepperProps {
steps: string[] steps: string[];
currentStep: number currentStep: number;
className?: string className?: string;
} }
export function Stepper({ steps, currentStep, className }: StepperProps) { export function Stepper({ steps, currentStep, className }: StepperProps) {
return ( return (
<div className={cn("flex w-full items-center", className)}> <div className={cn("flex w-full items-start justify-between", className)}>
{steps.map((step, index) => { {steps.map((step, index) => {
const stepNumber = index + 1 const stepNumber = index + 1;
const isCompleted = stepNumber < currentStep const isCurrent = stepNumber === currentStep;
const isCurrent = stepNumber === currentStep
return ( return (
<React.Fragment key={step}> <div
<div className="flex items-center"> key={step}
<div className="flex flex-col items-center"> className="flex-1 relative flex flex-col items-center group"
<div >
className={cn( {/* Connector Line (Behind) */}
"grid h-10 w-10 place-items-center rounded-full border-2 text-sm font-semibold transition-colors",
isCompleted && "border-brand-500 bg-brand-500 text-white",
// Active step should be visually prominent.
isCurrent && "border-brand-500 bg-brand-500 text-white",
!isCompleted && !isCurrent && "border-grayScale-300 bg-white text-grayScale-400",
)}
>
{isCompleted ? <Check className="h-5 w-5" /> : stepNumber}
</div>
<span
className={cn(
"mt-2 text-xs font-medium",
isCurrent && "text-brand-600",
!isCurrent && "text-grayScale-500",
)}
>
{step}
</span>
</div>
</div>
{index < steps.length - 1 && ( {index < steps.length - 1 && (
<div <div className="absolute left-1/2 w-[100%] mx-auto top-5 h-[2px] bg-grayScale-200 z-0" />
className={cn(
// Keep the connector visually continuous with the step circles.
"mx-2 h-0.5 flex-1",
// Color the track up to the current step.
isCompleted || isCurrent ? "bg-brand-500" : "bg-grayScale-200",
)}
/>
)} )}
</React.Fragment>
) {/* Circle */}
<div
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",
isCurrent
? "border-brand-500 bg-brand-500 text-white shadow-md scale-110"
: "border-grayScale-100 bg-white text-grayScale-400 font-medium",
)}
>
{stepNumber}
</div>
{/* Label */}
<span
className={cn(
"relative z-10 text-[13px] font-bold transition-colors duration-300",
isCurrent ? "text-brand-500" : "text-grayScale-400 font-medium",
)}
>
{step}
</span>
</div>
);
})} })}
</div> </div>
) );
} }

View File

@ -0,0 +1,259 @@
import { useState } from "react";
import {
Link,
useNavigate,
useParams,
useSearchParams,
} from "react-router-dom";
import { ArrowLeft } from "lucide-react";
import { Button } from "../../components/ui/button";
import { Stepper } from "../../components/ui/stepper";
import successIcon from "../../assets/success.svg";
import { ContextStep } from "./components/practice-steps/ContextStep";
import { ScenarioStep } from "./components/practice-steps/ScenarioStep";
import { PersonaStep } from "./components/practice-steps/PersonaStep";
import { QuestionsStep } from "./components/practice-steps/QuestionsStep";
import { ReviewStep } from "./components/practice-steps/ReviewStep";
export function AddPracticeFlow() {
const navigate = useNavigate();
const { level } = useParams<{ level: string }>();
const [searchParams] = useSearchParams();
const backTo = searchParams.get("backTo");
const courseId = searchParams.get("courseId");
const moduleId = searchParams.get("moduleId");
const isModuleContext = backTo === "module";
const backLabel =
backTo === "module"
? "Back to Module"
: backTo === "modules"
? "Back to Modules"
: "Back to Courses";
const backPath =
backTo === "module" && courseId && moduleId
? `/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}`
: backTo === "modules" && courseId
? `/new-content/learn-english/${level}/courses/${courseId}`
: `/new-content/learn-english/${level}/courses`;
const flowSteps = isModuleContext
? ["Context", "Persona", "Questions", "Review"]
: ["Context", "Scenario", "Persona", "Questions", "Review"];
const [currentStep, setCurrentStep] = useState(1);
const [selectedPersona, setSelectedPersona] = useState<string | null>(
"dawit",
);
const [isPublished, setIsPublished] = useState(false);
const [formData, setFormData] = useState({
program: "Intermediate",
course: "A2",
title: "",
description: "",
selectedVideo: "",
tips: "Focus on using the present perfect continuous tense to describe an action that started in the past and continues now.",
questions: [
{
id: "q1",
text: "How long have you been studying English?",
type: "Speaking",
voicePrompt: "prompt_q1_en.mp3",
sampleAnswer: "prompt_q1_en.mp3",
},
],
});
const nextStep = () =>
setCurrentStep((prev) => Math.min(prev + 1, flowSteps.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-500 bg-white">
<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-[32px] font-bold text-grayScale-900 mb-4">
Practice Published Successfully!
</h1>
<p className="text-grayScale-600 text-lg mb-14 max-w-lg font-medium leading-relaxed">
Your speaking practice is now active and available inside the module.
</p>
<div className="flex flex-col gap-4 w-full max-w-[400px]">
<Button
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"
>
Go back to Module
</Button>
<Button
onClick={() => {
setIsPublished(false);
setCurrentStep(1);
setFormData({
...formData,
title: "",
description: "",
});
}}
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"
>
Add Another Practice
</Button>
</div>
</div>
);
}
// Helper to map currentStep to the actual component for the module flow
const renderStep = () => {
if (!isModuleContext) {
switch (currentStep) {
case 1:
return (
<ContextStep
formData={formData}
setFormData={setFormData}
nextStep={nextStep}
navigate={navigate}
level={level!}
isModuleContext={isModuleContext}
/>
);
case 2:
return (
<ScenarioStep
formData={formData}
setFormData={setFormData}
nextStep={nextStep}
prevStep={prevStep}
/>
);
case 3:
return (
<PersonaStep
selectedPersona={selectedPersona}
setSelectedPersona={setSelectedPersona}
nextStep={nextStep}
prevStep={prevStep}
/>
);
case 4:
return (
<QuestionsStep
formData={formData}
setFormData={setFormData}
nextStep={nextStep}
prevStep={prevStep}
/>
);
case 5:
return (
<ReviewStep
formData={formData}
selectedPersona={selectedPersona}
prevStep={prevStep}
setIsPublished={setIsPublished}
isModuleContext={isModuleContext}
/>
);
default:
return null;
}
} else {
// Module Context Flow (Skips Scenario)
switch (currentStep) {
case 1:
return (
<ContextStep
formData={formData}
setFormData={setFormData}
nextStep={nextStep}
navigate={navigate}
level={level!}
isModuleContext={isModuleContext}
/>
);
case 2:
return (
<PersonaStep
selectedPersona={selectedPersona}
setSelectedPersona={setSelectedPersona}
nextStep={nextStep}
prevStep={prevStep}
/>
);
case 3:
return (
<QuestionsStep
formData={formData}
setFormData={setFormData}
nextStep={nextStep}
prevStep={prevStep}
/>
);
case 4:
return (
<ReviewStep
formData={formData}
selectedPersona={selectedPersona}
prevStep={prevStep}
setIsPublished={setIsPublished}
isModuleContext={isModuleContext}
/>
);
default:
return null;
}
}
};
return (
<div className="space-y-8 pb-32 px-6 pt-6 min-h-screen bg-[#F8FAFC]">
{/* Header */}
<div className="mx-auto max-w-7xl w-full">
<div className="flex items-center justify-between mb-8">
<Link
to={backPath}
className="flex items-center gap-2 text-[15px] font-medium text-grayScale-500 transition-colors hover:text-brand-500 decoration-none"
>
<ArrowLeft className="h-4 w-4" />
{backLabel}
</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 className="space-y-4 mb-10">
<h1 className="text-4xl font-bold text-[#0F172A]">
Add New Practice
</h1>
<p className="text-grayScale-400 text-lg">
Create a new immersive practice session for students.
</p>
</div>
<div className="mx-auto max-w-4xl mb-12">
<Stepper steps={flowSteps} currentStep={currentStep} />
</div>
<div className="mx-auto max-w-4xl">{renderStep()}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,155 @@
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 { VideoDetailStep } from "./components/video-steps/VideoDetailStep";
import { ReviewPublishStep } from "./components/video-steps/ReviewPublishStep";
const STEPS = [
{ id: 1, label: "Video Detail" },
{ id: 2, label: "Review & Publish" },
];
export function AddVideoFlow() {
const navigate = useNavigate();
const { level, courseId, moduleId } = useParams<{
level: string;
courseId: string;
moduleId: string;
}>();
const [currentStep, setCurrentStep] = useState(1);
const [isPublished, setIsPublished] = useState(false);
const [formData, setFormData] = useState({
title: "",
order: "1",
description: "",
thumbnail: null,
videoFile: null,
});
const nextStep = () => setCurrentStep((prev) => Math.min(prev + 1, 2));
const prevStep = () => setCurrentStep((prev) => Math.max(prev - 1, 1));
const backPath = `/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}`;
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-500 bg-white">
{/* Success Icon Wrapper (Jagged Circle Style) */}
<div className="mb-12 relative scale-110">
<div className="absolute inset-0 bg-brand-500/5 blur-3xl rounded-full" />
<div className="relative">
<div
className="h-24 w-24 bg-brand-500 flex items-center justify-center"
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%)",
}}
>
<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>
<h1 className="text-[32px] font-bold text-grayScale-900 mb-4">
Video Published Successfully!
</h1>
<p className="text-grayScale-600 text-lg mb-14 max-w-lg font-medium leading-relaxed">
Your video is now live and available inside the selected module.
</p>
<div className="flex flex-col gap-4 w-full max-w-[400px]">
<Button
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"
>
Go back to Learn English
</Button>
<Button
onClick={() => {
setFormData({
title: "",
order: "1",
description: "",
thumbnail: null,
videoFile: null,
});
setIsPublished(false);
setCurrentStep(1);
}}
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"
>
Add Another Video
</Button>
</div>
</div>
);
}
return (
<div className="space-y-8 pb-32 px-6 pt-6 min-h-screen bg-[#F8FAFC]">
{/* Header */}
<div className="mx-auto max-w-7xl w-full">
<div className="flex items-center justify-between mb-8">
<Link
to={backPath}
className="flex items-center gap-2 text-[15px] font-medium text-grayScale-500 transition-colors hover:text-brand-500 decoration-none"
>
<ArrowLeft className="h-4 w-4" />
Back to Modules
</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>
<h1 className="text-4xl font-bold text-[#0F172A] mb-10">
Add New Video
</h1>
<div className="mx-auto max-w-4xl mb-12">
<Stepper
steps={STEPS.map((s) => s.label)}
currentStep={currentStep}
/>
</div>
{/* Step Content */}
<div className="mx-auto max-w-7xl">
{currentStep === 1 && (
<VideoDetailStep
formData={formData}
setFormData={setFormData}
nextStep={nextStep}
/>
)}
{currentStep === 2 && (
<ReviewPublishStep
formData={formData}
prevStep={prevStep}
setIsPublished={setIsPublished}
/>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,160 @@
import { useState } from "react";
import { ArrowLeft, Plus, Calendar, Plane, Clock, Hand } from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Button } from "../../components/ui/button";
import { Card } from "../../components/ui/card";
import { cn } from "../../lib/utils";
const MODULES = [
{
id: "m1",
title: "Introduction Basics",
description: "Learn basic English words, phrases, and simple sentences.",
icon: Hand,
status: "Published",
gradient: "from-[#8E44AD] to-[#C39BD3]",
},
{
id: "m2",
title: "Daily Routines",
description: "Vocabulary related to waking up, and evening activities.",
icon: Clock,
status: "Draft",
gradient: "from-[#8E44AD] to-[#C39BD3]",
},
{
id: "m3",
title: "Travel Essentials",
description:
"Key phrases for airports, hotels, and asking for help while abroad.",
icon: Plane,
status: "Draft",
gradient: "from-[#8E44AD] to-[#C39BD3]",
},
];
import { AddModuleModal } from "./components/AddModuleModal";
export function CourseDetailPage() {
const navigate = useNavigate();
const { level, courseId } = useParams<{ level: string; courseId: string }>();
const [isAddModuleOpen, setIsAddModuleOpen] = useState(false);
return (
<div className="space-y-10 pb-20">
{/* Header Navigation */}
<div className="flex items-center gap-2">
<Link
to={`/new-content/learn-english/${level}/courses`}
className="flex items-center gap-2 text-sm font-medium text-grayScale-600 transition-colors hover:text-brand-500"
>
<ArrowLeft className="h-5 w-5" />
Back to Levels
</Link>
</div>
{/* Hero Section */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div className="space-y-2">
<h1 className="text-4xl font-extrabold text-grayScale-900 tracking-tight">
{courseId?.toUpperCase() || "A1"}
</h1>
<p className="text-grayScale-500 text-lg max-w-2xl font-medium">
Learn basic English words, phrases, and simple sentences for daily
situations.
</p>
</div>
<div className="flex items-center gap-4">
<Button
variant="outline"
className="h-12 px-6 rounded-[6px] border-brand-500 text-brand-500 font-bold transition-all gap-2"
onClick={() =>
navigate(
`/new-content/learn-english/${level}/courses/add-practice?backTo=modules&courseId=${courseId}`,
)
}
>
<Calendar className="h-5 w-5" />
Add Practice
</Button>
<Button
className="h-12 px-6 rounded-[6px] bg-brand-500 font-bold shadow-lg shadow-brand-500/20 transition-all gap-2"
onClick={() => setIsAddModuleOpen(true)}
>
<Plus className="h-5 w-5" />
Add Module
</Button>
</div>
</div>
<AddModuleModal
isOpen={isAddModuleOpen}
onClose={() => setIsAddModuleOpen(false)}
/>
{/* Gradient Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{MODULES.map((module) => (
<Card
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"
>
{/* Gradient Banner */}
<div
className={cn(
"h-36 w-full bg-gradient-to-b opacity-90 transition-transform duration-700",
module.gradient,
)}
/>
<div className="p-2 pb-4 pt-8 flex-1 flex flex-col">
<div className="flex gap-4 mb-8">
{/* 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">
<module.icon className="h-6 w-6 text-brand-500" />
</div>
{/* Content */}
<div className="space-y-1">
<h3 className="text-xl font-bold text-[#0F172A] tracking-tight">
{module.title}
</h3>
<p className="text-grayScale-400 font-medium leading-normal text-[14px]">
{module.description}
</p>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-3 mt-auto">
<Button
variant="outline"
className="flex-1 h-12 rounded-[6px] border-[#9E2891] text-[#9E2891] font-bold transition-all text-sm"
onClick={() =>
navigate(
`/new-content/learn-english/${level}/courses/${courseId}/modules/${module.id}`,
)
}
>
View Detail
</Button>
{module.status === "Published" ? (
<Button
disabled
className="flex-1 h-12 rounded-[6px] bg-[#D291BC] text-white font-bold opacity-100 cursor-default border-none shadow-none text-sm"
>
Published
</Button>
) : (
<Button className="flex-1 h-12 rounded-[6px] bg-brand-500 text-white font-bold shadow-md shadow-brand-500/10 text-sm">
Publish Practice
</Button>
)}
</div>
</div>
</Card>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,214 @@
import { Plus, ArrowRight } from "lucide-react";
import { Link } from "react-router-dom";
import { Card, CardContent } from "../../components/ui/card";
import { Button } from "../../components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
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 LearnEnglishPage() {
const levels = [
{
id: "beginner",
title: "Beginner",
description:
"Designed for learners starting from scratch. Focuses on simple grammar, and everyday communication.",
},
{
id: "intermediate",
title: "Intermediate",
description:
"For learners who can communicate at a basic level and want to improve fluency, accuracy, and confidence.",
},
{
id: "advanced",
title: "Advanced",
description:
"Targets advanced learners aiming for professional, academic, and complex conversational English.",
},
];
return (
<div className="space-y-8">
{/* Header section */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight text-grayScale-700">
Learn English
</h1>
<p className="mt-1 text-sm text-grayScale-500">
Manage learning content by level
</p>
</div>
<Dialog>
<DialogTrigger asChild>
<Button className="h-11 rounded-xl bg-brand-500 px-6 font-semibold hover:bg-brand-600">
<Plus className="mr-2 h-5 w-5" />
Add Program
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl gap-0 border-none p-0">
<DialogHeader className="p-8 pb-4">
<DialogTitle className="text-2xl font-bold text-grayScale-700">
Add New Program
</DialogTitle>
<DialogDescription className="text-sm text-grayScale-400">
Create a learning program to group courses by learner level
</DialogDescription>
</DialogHeader>
{/* 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>
<form className="space-y-6 p-8 pt-4">
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Program Name
</label>
<Input
placeholder="e.g. Beginner"
className="h-12 rounded-xl"
/>
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Description
</label>
<Input
placeholder="Short description explaining who this program is for"
className="h-12 rounded-xl"
/>
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Program Order
</label>
<Select className="h-12 rounded-xl">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</Select>
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Thumbnail
</label>
<div className="relative group cursor-pointer">
<div className="flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-[#9E289133] bg-white p-10 transition-all ">
<div className="mb-4">
<img
src={uploadIcon}
alt="Upload icon"
className="h-10 w-10"
/>
</div>
<p className="text-sm">
<span className="font-bold text-[#9E2891]">
Click to upload
</span>{" "}
<span className="text-grayScale-500">
or drag and drop
</span>
</p>
<p className="mt-1 text-xs font-medium text-grayScale-400 uppercase tracking-wider">
JPG, PNG (MAX 1 MB)
</p>
</div>
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<DialogClose asChild>
<Button
variant="outline"
className="h-12 min-w-[120px] rounded-xl border-grayScale-200 font-semibold"
>
Cancel
</Button>
</DialogClose>
<Button className="h-12 min-w-[160px] rounded-xl bg-brand-500 font-semibold hover:bg-brand-600">
Create Program
</Button>
</div>
</form>
</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-100" />
</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="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{levels.map((level) => (
<Card
key={level.title}
className="group overflow-hidden border-none shadow-soft transition-all duration-300 hover:-translate-y-1 hover:shadow-lg"
>
{/* Gradient Header */}
<div
className="h-32 w-full"
style={{
background:
"linear-gradient(135deg, #9E289180 0%, #9E2891 100%)",
}}
/>
<CardContent className="bg-white p-6">
<h3 className="text-xl font-bold text-grayScale-700">
{level.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-grayScale-500">
{level.description}
</p>
<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">
View Courses
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</Button>
</Link>
</CardContent>
</Card>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,321 @@
import { useState } from "react";
import {
ArrowLeft,
Video,
Calendar,
Mic,
Layers,
Edit2,
Trash2,
} from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Button } from "../../components/ui/button";
import { cn } from "../../lib/utils";
import { VideoCard } from "./components/VideoCard";
const MOCK_VIDEOS = [
{
id: "v1",
title: "1.1 Introduction to Formal Greetings",
duration: "08:45",
status: "Draft",
thumbnailGradient: "from-[#CBD5E1] to-[#94A3B8]",
},
{
id: "v2",
title: "1.2 Understanding Email Structure",
duration: "08:45",
status: "Published",
thumbnailGradient: "from-[#DBEAFE] to-[#93C5FD]",
},
{
id: "v3",
title: "1.3 Common Business Idioms",
duration: "08:45",
status: "Published",
thumbnailGradient: "from-[#FEF3C7] to-[#FCD34D]",
},
{
id: "v4",
title: "1.4 Video Conference Etiquette",
duration: "08:45",
status: "Published",
thumbnailGradient: "from-[#FCE7F3] to-[#F9A8D4]",
},
];
const MOCK_PRACTICES = [
{
id: "p1",
title: "Describe a Photo",
level: "IELTS",
variations: 12,
status: "Draft",
},
{
id: "p2",
title: "Describe a Photo",
level: "IELTS",
variations: 12,
status: "Draft",
},
{
id: "p3",
title: "Describe a Photo",
level: "IELTS",
variations: 12,
status: "Draft",
},
{
id: "p4",
title: "Describe a Photo",
level: "IELTS",
variations: 12,
status: "Draft",
},
];
export function ModuleDetailPage() {
const navigate = useNavigate();
const { level, courseId, moduleId } = useParams<{
level: string;
courseId: string;
moduleId: string;
}>();
const [activeTab, setActiveTab] = useState<"video" | "practice">("video");
const [activeFilter, setActiveFilter] = useState("Draft");
const [videos] = useState(MOCK_VIDEOS);
const [practices] = useState(MOCK_PRACTICES);
const moduleTitle =
moduleId
?.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ") || "Business English Fundamentals";
return (
<div className="space-y-10 pb-20 animate-in fade-in duration-500">
{/* Header Navigation */}
<div className="flex items-center gap-2">
<Link
to={`/new-content/learn-english/${level}/courses/${courseId}`}
className="flex items-center gap-2 text-[15px] font-medium text-grayScale-600 transition-colors hover:text-brand-500"
>
<ArrowLeft className="h-5 w-5" />
Back to Modules
</Link>
</div>
{/* Hero Section */}
<div className="flex flex-col md:flex-row md:items-start justify-between gap-6">
<div className="space-y-2">
<h1 className="text-3xl font-extrabold text-grayScale-900 tracking-tight">
Module 3: {moduleTitle}
</h1>
<p className="text-grayScale-500 text-[17px] max-w-2xl font-medium leading-relaxed">
This module covers essential vocabulary and phrases used in modern
business environments, including email etiquette and meeting
protocols.
</p>
</div>
<div className="flex items-center gap-3">
<Button
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"
onClick={() =>
navigate(
`/new-content/learn-english/${level}/courses/add-practice?backTo=module&courseId=${courseId}&moduleId=${moduleId}`,
)
}
>
<Calendar className="h-4 w-4" />
Add Practice
</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"
onClick={() =>
navigate(
`/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}/add-video`,
)
}
>
<div className="h-4 w-4 flex items-center justify-center">
<span className="text-xl leading-none font-light">+</span>
</div>
Add Video
</Button>
</div>
</div>
{/* Tabs */}
<div className="border-b border-grayScale-50">
<div className="flex gap-10">
<button
onClick={() => setActiveTab("video")}
className={cn(
"pb-4 text-[17px] font-bold transition-all relative",
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-grayScale-400 hover:text-grayScale-600",
)}
>
Video
</button>
<button
onClick={() => setActiveTab("practice")}
className={cn(
"pb-4 text-[17px] font-bold transition-all relative",
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-grayScale-400 hover:text-grayScale-600",
)}
>
Practice
</button>
</div>
</div>
{/* Content */}
<div className="mt-8">
{activeTab === "video" ? (
videos.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{videos.map((video) => (
<VideoCard
key={video.id}
{...(video as any)}
onEdit={() => console.log("Edit", video.id)}
onPublish={() => console.log("Publish", video.id)}
/>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-32 px-4 rounded-[40px] border-2 border-dashed border-[#F1F5F9] bg-white max-w-4xl mx-auto shadow-sm">
<div className="h-20 w-20 rounded-full bg-[#FAF5FF] flex items-center justify-center mb-6">
<div className="h-14 w-14 rounded-full bg-[#F5EBFF] flex items-center justify-center">
<Video className="h-7 w-7 text-brand-500 fill-brand-500/10" />
</div>
</div>
<h2 className="text-2xl font-extrabold text-grayScale-900 mb-3">
No videos added to this module yet
</h2>
<p className="text-grayScale-400 font-medium text-[15px] text-center max-w-sm mb-10 leading-relaxed">
Videos are a great way to engage students. Start building your
module by adding your first video lesson now.
</p>
<Button
variant="outline"
className="h-12 px-8 rounded-xl border-brand-500 text-brand-500 font-bold hover:bg-brand-50 transition-all flex items-center gap-2"
onClick={() =>
navigate(
`/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}/add-video`,
)
}
>
<Video className="h-5 w-5" />
Add Video
</Button>
</div>
)
) : (
<div className="space-y-8">
{/* Practice Tab Filter Bar */}
<div className="bg-white border border-grayScale-100 rounded-2xl p-4 flex items-center gap-10 shadow-sm overflow-x-auto whitespace-nowrap px-8">
<div className="flex items-center gap-2 text-[12px] font-bold text-grayScale-300 uppercase tracking-widest mr-2">
STATUS:
</div>
<div className="flex items-center gap-3">
{["All", "Published", "Draft", "Archived"].map((label) => (
<button
key={label}
onClick={() => setActiveFilter(label)}
className={cn(
"h-9 px-5 rounded-full text-[13px] font-bold transition-all",
activeFilter === label
? "bg-brand-500 text-white shadow-md shadow-brand-500/20"
: "bg-[#F1F5F9] text-grayScale-500 hover:bg-grayScale-100",
)}
>
{label}
</button>
))}
</div>
</div>
{/* Practice Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{practices.map((practice) => (
<PracticeCard key={practice.id} {...practice} />
))}
</div>
</div>
)}
</div>
</div>
);
}
function PracticeCard({
title,
level,
variations,
status,
}: {
title: string;
level: string;
variations: number;
status: string;
}) {
return (
<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 items-center justify-between">
<h3 className="text-[20px] font-bold text-grayScale-900 line-clamp-1">
{title}
</h3>
</div>
<div className="flex items-center gap-3">
<span className="bg-[#22C55E] text-white text-[11px] font-bold px-2 py-1 rounded-[4px]">
{level}
</span>
<div className="flex items-center gap-1.5 text-grayScale-500">
<Mic className="h-4 w-4" />
<span className="text-[13px] font-bold">Speaking</span>
</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">
<Layers className="h-4 w-4" />
<span className="text-[14px] font-bold">{variations} Variations</span>
</div>
<div className="flex 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">
{status}
</div>
<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">
<Edit2 className="h-4 w-4" />
</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">
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
<div className="mt-8 grid grid-cols-2 gap-3">
<Button className="bg-brand-500 text-white rounded-xl h-11 text-[13px] font-bold shadow-md shadow-brand-500/10 hover:bg-brand-600 transition-all px-0">
Publish Practice
</Button>
<Button
variant="outline"
className="border-brand-500 text-brand-500 rounded-xl h-11 text-[13px] font-bold bg-white hover:bg-brand-50 transition-all px-0"
>
Publish Video
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,81 @@
import { Link } from "react-router-dom";
import { Mic } from "lucide-react";
import { Card, CardContent } from "../../components/ui/card";
import { Button } from "../../components/ui/button";
export function NewContentPage() {
return (
<div className="space-y-8">
{/* Header section */}
<div>
<h1 className="text-2xl font-bold tracking-tight text-grayScale-700">
Content Management
</h1>
<p className="mt-1 text-sm text-grayScale-500">
Upload, organize, and manage learning content across programs and
courses
</p>
</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-100" />
</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="grid max-w-5xl gap-8 grid-cols-1 md:grid-cols-2">
{/* Learn English Card */}
<Card className="overflow-hidden border-none shadow-soft">
<div className="flex h-56 items-center justify-center bg-white/50">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-brand-100/30">
<Mic className="h-10 w-10 text-brand-500" />
</div>
</div>
<CardContent className="border-t border-grayScale-100 bg-white p-8 text-center">
<h3 className="text-xl font-bold text-grayScale-700">
Learn English
</h3>
<p className="mt-3 text-sm leading-relaxed text-grayScale-500">
Manage structured English learning content based on levels and
modules.
</p>
<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">
Manage Learn English
</Button>
</Link>
</CardContent>
</Card>
{/* Courses Card */}
<Card className="overflow-hidden border-none shadow-soft">
<div className="flex h-56 items-center justify-center bg-white/50">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-brand-100/30">
<Mic className="h-10 w-10 text-brand-500" />
</div>
</div>
<CardContent className="border-t border-grayScale-100 bg-white p-8 text-center">
<h3 className="text-xl font-bold text-grayScale-700">Courses</h3>
<p className="mt-3 text-sm leading-relaxed text-grayScale-500">
Manage skill-based and exam preparation courses such as Duolingo
and IELTS.
</p>
<Button className="mt-8 h-12 w-full rounded-xl bg-brand-500 text-base font-semibold hover:bg-brand-600">
Manage Courses
</Button>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@ -0,0 +1,265 @@
import { ArrowLeft, Plus, FileText } from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Card, CardContent } from "../../components/ui/card";
import { Button } from "../../components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
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 ProgramCoursesPage() {
const navigate = useNavigate();
const { level } = useParams<{ level: string }>();
const courses = [
{
id: "a1",
title: "A1",
description:
"Learn basic English words, phrases, and simple sentences for daily situations.",
stats: {
modules: 3,
videos: 15,
practices: 18,
},
},
{
id: "a2",
title: "A2",
description:
"Build on basic skills with longer sentences, and practical conversations.",
stats: {
modules: 3,
videos: 15,
practices: 18,
},
},
];
return (
<div className="space-y-8">
{/* Navigation */}
<Link
to="/new-content/learn-english"
className="flex items-center gap-2 text-sm font-medium text-grayScale-500 transition-colors hover:text-brand-500"
>
<ArrowLeft className="h-4 w-4" />
Back to Programs
</Link>
{/* Header section */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<h1 className="text-3xl font-bold tracking-tight text-grayScale-700 capitalize">
{level || "Program"}
</h1>
<p className="max-w-2xl text-[15px] leading-relaxed text-grayScale-400">
Designed for learners starting from scratch. Focuses on simple
grammar, and everyday communication.
</p>
</div>
<div className="flex gap-3">
<Link to={`/new-content/learn-english/${level}/courses/add-practice`}>
<Button
variant="outline"
className="rounded-[6px] border-brand-500 text-brand-500 "
>
<FileText className="mr-2 h-4 w-4" />
Add Practice
</Button>
</Link>
<Dialog>
<DialogTrigger asChild>
<Button className="rounded-[6px] bg-brand-500 font-semibold hover:bg-brand-600">
<Plus className="mr-2 h-5 w-5" />
Add Courses
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl gap-0 border-none p-0">
<DialogHeader className="p-8 pb-4">
<DialogTitle className="text-2xl font-bold text-grayScale-700">
Add New Course
</DialogTitle>
<DialogDescription className="text-sm text-grayScale-400">
Create a CEFR-aligned course inside this program.
</DialogDescription>
</DialogHeader>
{/* 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>
<form className="space-y-6 p-8 pt-4">
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Course Name
</label>
<Input placeholder="e.g. A1" className="h-12 rounded-xl" />
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Description
</label>
<Input
placeholder="Brief overview of what learners will achieve in this course"
className="h-12 rounded-xl"
/>
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Course Order
</label>
<Select className="h-12 rounded-xl">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</Select>
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Thumbnail
</label>
<div className="relative group cursor-pointer">
<div className="flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-[#9E289133] bg-white p-10 transition-all">
<div className="mb-4">
<img
src={uploadIcon}
alt="Upload icon"
className="h-10 w-10"
/>
</div>
<p className="text-sm">
<span className="font-bold text-[#9E2891]">
Click to upload
</span>{" "}
<span className="text-grayScale-500">
or drag and drop
</span>
</p>
<p className="mt-1 text-xs font-medium text-grayScale-400 uppercase tracking-wider">
JPG, PNG (MAX 1 MB)
</p>
</div>
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<DialogClose asChild>
<Button
variant="outline"
className="h-12 min-w-[120px] rounded-xl border-grayScale-200 font-semibold"
>
Cancel
</Button>
</DialogClose>
<Button className="h-12 min-w-[160px] rounded-xl bg-brand-500 font-semibold hover:bg-brand-600">
Create Course
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
</div>
{/* Cards Grid */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{courses.map((course) => (
<Card
key={course.id}
className="group overflow-hidden border border-grayScale-100 shadow-soft transition-all duration-300 hover:shadow-lg"
>
{/* Gradient Header */}
<div
className="h-32 w-full"
style={{
background:
"linear-gradient(135deg, #9E289180 0%, #9E2891 100%)",
}}
/>
<CardContent className="p-6">
<h3 className="text-xl font-bold text-grayScale-700">
{course.title}
</h3>
<p className="mt-2 text-[13px] leading-relaxed text-grayScale-500 line-clamp-2">
{course.description}
</p>
{/* Stats */}
<div className="my-6 grid grid-cols-3 gap-4 border-y border-grayScale-50 py-4">
<div className="text-center">
<p className="text-base font-bold text-grayScale-700">
{course.stats.modules}
</p>
<p className="text-[10px] font-medium text-grayScale-400 uppercase tracking-wider">
Modules
</p>
</div>
<div className="text-center">
<p className="text-base font-bold text-grayScale-700">
{course.stats.videos}
</p>
<p className="text-[10px] font-medium text-grayScale-400 uppercase tracking-wider">
Videos
</p>
</div>
<div className="text-center">
<p className="text-base font-bold text-grayScale-700">
{course.stats.practices}
</p>
<p className="text-[10px] font-medium text-grayScale-400 uppercase tracking-wider">
Practices
</p>
</div>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="h-10 flex-1 rounded-[6px] border-brand-500 text-[13px] font-semibold text-brand-500 "
onClick={() =>
navigate(
`/new-content/learn-english/${level}/courses/${course.title.toLowerCase()}`,
)
}
>
View Detail
</Button>
<Button className="h-10 flex-1 rounded-[6px] bg-brand-500 text-[13px] font-semibold ">
Publish Practice
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,131 @@
import { X } from "lucide-react";
import { Button } from "../../../components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogClose,
} from "../../../components/ui/dialog";
import { Input } from "../../../components/ui/input";
import { Select } from "../../../components/ui/select";
import uploadIcon from "../../../assets/icons/upload.png";
interface AddModuleModalProps {
isOpen: boolean;
onClose: () => void;
}
export function AddModuleModal({ isOpen, onClose }: AddModuleModalProps) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-2xl gap-0 border-none p-0 overflow-hidden rounded-[16px] shadow-2xl">
<DialogHeader className="p-8 pb-4 relative">
<DialogTitle className="text-2xl font-bold text-grayScale-700">
Add New Module
</DialogTitle>
<DialogDescription className="text-sm text-grayScale-400">
Create a module to organize videos and practices.
</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>
{/* 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"
style={{ background: "gray" }}
/>
</div>
</div>
<form className="space-y-6 p-8 pt-4">
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Module Title
</label>
<Input
placeholder="e.g. Daily Introductions"
className="h-12 rounded-xl"
/>
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Description
</label>
<Input
placeholder="Short description of this module"
className="h-12 rounded-xl"
/>
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Module Order
</label>
<Select className="h-12 rounded-xl">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</Select>
</div>
<div className="space-y-2">
<label className="text-[15px] font-medium text-grayScale-700">
Icon
</label>
<div className="relative group cursor-pointer">
<div className="flex flex-col items-center justify-center rounded-2xl border-2 border-dashed border-[#9E289133] bg-white p-10 transition-all">
<div className="mb-4">
<img
src={uploadIcon}
alt="Upload icon"
className="h-10 w-10"
/>
</div>
<p className="text-sm">
<span className="font-bold text-[#9E2891]">
Click to upload
</span>{" "}
<span className="text-grayScale-500">or drag and drop</span>
</p>
<p className="mt-1 text-xs font-medium text-grayScale-400 uppercase tracking-wider">
JPG, PNG (MAX 1 MB)
</p>
</div>
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<DialogClose asChild>
<Button
variant="outline"
className="h-12 min-w-[120px] rounded-xl border-grayScale-200 font-semibold"
>
Cancel
</Button>
</DialogClose>
<Button
type="submit"
className="h-12 min-w-[160px] rounded-xl bg-brand-500 font-semibold hover:bg-brand-600 text-white shadow-lg shadow-brand-500/20"
>
Create Module
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,100 @@
import { MoreVertical, Edit2, Play } from "lucide-react";
import { Button } from "../../../components/ui/button";
import { cn } from "../../../lib/utils";
interface VideoCardProps {
id: string;
title: string;
duration: string;
status: "Draft" | "Published";
thumbnailGradient: string;
onEdit?: () => void;
onPublish?: () => void;
}
export function VideoCard({
title,
duration,
status,
thumbnailGradient,
onEdit,
onPublish,
}: VideoCardProps) {
return (
<div className="group bg-white rounded-[24px] border border-grayScale-50 overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 flex flex-col">
{/* Thumbnail */}
<div
className={cn(
"relative h-44 w-full bg-gradient-to-br",
thumbnailGradient,
)}
>
{/* Duration Badge */}
<div className="absolute bottom-3 right-3 bg-black/70 text-white text-[11px] font-bold px-2 py-1 rounded-md backdrop-blur-sm">
{duration}
</div>
{/* Play Overlay */}
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/10">
<div className="h-12 w-12 rounded-full bg-white/20 backdrop-blur-md flex items-center justify-center border border-white/30">
<Play className="h-6 w-6 text-white fill-current" />
</div>
</div>
</div>
{/* Content */}
<div className="p-5 space-y-4 flex-1 flex flex-col">
<div className="flex items-center justify-between">
{/* Status Badge */}
<div
className={cn(
"flex items-center gap-1.5 px-3 py-1 rounded-full text-[11px] font-bold uppercase tracking-wider border",
status === "Published"
? "bg-[#ECFDF5] text-[#059669] border-[#D1FAE5]"
: "bg-[#F3F4F6] text-[#6B7280] border-[#E5E7EB]",
)}
>
<div
className={cn(
"h-1.5 w-1.5 rounded-full",
status === "Published" ? "bg-[#10B981]" : "bg-[#9CA3AF]",
)}
/>
{status}
</div>
{/* Menu */}
<button className="h-8 w-8 flex items-center justify-center rounded-full hover:bg-grayScale-50 transition-colors text-grayScale-400">
<MoreVertical className="h-5 w-5" />
</button>
</div>
<h3 className="text-[17px] font-bold text-grayScale-900 line-clamp-2 leading-snug">
{title}
</h3>
{/* Actions */}
<div className="pt-2 space-y-3 mt-auto">
<Button
variant="outline"
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"
>
<Edit2 className="h-4 w-4" />
Edit
</Button>
<Button
disabled={status === "Published"}
onClick={onPublish}
className={cn(
"w-full h-11 rounded-xl font-bold transition-all shadow-sm",
status === "Published"
? "bg-[#E9D5E5] text-white opacity-100 cursor-default"
: "bg-brand-500 text-white hover:bg-brand-600 shadow-brand-500/10",
)}
>
{status === "Published" ? "Published" : "Publish"}
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,146 @@
import { GraduationCap, ArrowRight, LayoutGrid, Monitor } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card";
import { Select } from "../../../../components/ui/select";
interface ContextStepProps {
formData: any;
setFormData: (data: any) => void;
nextStep: () => void;
navigate: (path: string) => void;
level: string;
isModuleContext?: boolean;
}
export function ContextStep({
formData,
setFormData,
nextStep,
navigate,
level,
isModuleContext,
}: ContextStepProps) {
return (
<Card className="overflow-hidden border-grayScale-50 shadow-soft rounded-2xl bg-white animate-in fade-in duration-500">
<div className="border-b border-grayScale-50 p-8">
<h2 className="text-2xl font-bold text-grayScale-900 leading-none">
Step 1: Context Definition
</h2>
<p className="text-grayScale-400 text-base mt-3">
Define the educational level and curriculum module for this practice.
</p>
</div>
<div className="space-y-10 p-10">
{/* Program Field */}
<div className="space-y-3">
<label className="text-[17px] font-bold text-grayScale-700 ml-1">
Program{" "}
<span className="text-grayScale-300 font-medium">
(Auto-selected)
</span>
</label>
<div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2">
<GraduationCap className="h-6 w-6 text-grayScale-400" />
</div>
<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"
disabled
>
<option>{formData.program || "Intermediate"}</option>
</Select>
</div>
</div>
{/* Course Field */}
<div className="space-y-3">
<label className="text-[17px] font-bold text-grayScale-700 ml-1">
Course{" "}
<span className="text-grayScale-300 font-medium">
(Auto-selected)
</span>
</label>
<div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2">
<GraduationCap className="h-6 w-6 text-grayScale-400" />
</div>
<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"
disabled
>
<option>{formData.course || "B2"}</option>
</Select>
</div>
</div>
{/* Select Module Field */}
<div className="space-y-3">
<label className="text-[17px] font-bold text-grayScale-700 ml-1">
Select Module
</label>
<div className="relative">
<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-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">
<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>
<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) */}
{isModuleContext && (
<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">
Select Video
</label>
<div className="relative">
<div className="absolute left-6 top-1/2 -translate-y-1/2">
<Monitor className="h-6 w-6 text-grayScale-400" />
</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"
value={formData.selectedVideo}
onChange={(e) =>
setFormData({ ...formData, selectedVideo: e.target.value })
}
>
<option value="">Choose a video</option>
<option value="v1">Intro to Greetings</option>
<option value="v2">Advanced Grammar</option>
</Select>
</div>
<p className="text-[13px] text-grayScale-400 font-medium px-2">
Select the specific learning module this practice will reinforce.
</p>
</div>
)}
</div>
<div className="flex items-center justify-between border-t border-grayScale-50 bg-[#F9FAFB]/50 p-10 px-12">
<button
className="text-[17px] font-bold text-grayScale-500 transition-colors hover:text-grayScale-700"
onClick={() =>
navigate(`/new-content/learn-english/${level}/courses`)
}
>
Cancel
</button>
<Button
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"
>
Next: {isModuleContext ? "Persona" : "Scenario"}{" "}
<ArrowRight className="h-5 w-5" />
</Button>
</div>
</Card>
);
}

View File

@ -0,0 +1,91 @@
import { Check, ArrowRight } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "../../../../components/ui/avatar";
import { cn } from "../../../../lib/utils";
import { PERSONAS } from "./constants";
interface PersonaStepProps {
selectedPersona: string | null;
setSelectedPersona: (id: string) => void;
nextStep: () => void;
prevStep: () => void;
}
export function PersonaStep({
selectedPersona,
setSelectedPersona,
nextStep,
prevStep,
}: PersonaStepProps) {
return (
<div className="space-y-8">
<div className="space-y-1 px-2">
<h2 className="text-2xl font-extrabold text-grayScale-700">
Select Personas
</h2>
<p className="text-grayScale-400 text-lg">
Choose the characters that will participate in this practice scenario.
</p>
</div>
<div className="grid grid-cols-2 gap-6 sm:grid-cols-4">
{PERSONAS.map((persona) => (
<div
key={persona.id}
onClick={() => setSelectedPersona(persona.id)}
className={cn(
"group relative cursor-pointer rounded-2xl border-2 bg-white p-6 transition-all duration-300",
selectedPersona === persona.id
? "border-brand-500 shadow-xl scale-105"
: "border-grayScale-50 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">
<AvatarImage src={persona.avatar} />
<AvatarFallback>
{persona.name.substring(0, 2)}
</AvatarFallback>
</Avatar>
{selectedPersona === persona.id && (
<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">
<Check className="h-4 w-4 stroke-[3]" />
</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 className="flex items-center justify-between pt-8">
<Button
onClick={prevStep}
variant="outline"
className="h-12 w-28 rounded-xl border-grayScale-200 font-bold text-grayScale-600"
>
Back
</Button>
<Button
onClick={nextStep}
className="h-12 rounded-xl bg-brand-500 px-8 font-bold hover:bg-brand-600 shadow-md shadow-brand-500/20"
>
Next: Questions <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,140 @@
import { GripVertical, Trash2, Plus, ArrowRight } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card";
import { Input } from "../../../../components/ui/input";
import { VoicePrompt } from "./VoicePrompt";
interface QuestionsStepProps {
formData: any;
setFormData: (data: any) => void;
nextStep: () => void;
prevStep: () => void;
}
export function QuestionsStep({
formData,
setFormData,
nextStep,
prevStep,
}: QuestionsStepProps) {
const addQuestion = () => {
const newQuestion = {
id: `q${formData.questions.length + 1}`,
text: "",
type: "Speaking",
voicePrompt: "upload_audio.mp3",
sampleAnswer: "upload_audio.mp3",
};
setFormData({
...formData,
questions: [...formData.questions, newQuestion],
});
};
return (
<div className="space-y-6">
<div className="space-y-1 px-2">
<h2 className="text-2xl font-extrabold text-grayScale-700">
Create Practice Questions
</h2>
<p className="text-grayScale-400 text-lg">
Define the dialogue flow and interactions for this scenario.
</p>
</div>
<div className="space-y-6">
{formData.questions.map((q: any, i: number) => (
<Card
key={q.id}
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" />
<div className="px-5 pb-7 pt-2 space-y-6">
<div className="flex items-center justify-between border-b border-grayScale-50 pb-4 mb-4">
<div className="flex items-center gap-3">
<GripVertical className="h-5 w-5 text-brand-500 cursor-grab" />
<span className="font-bold text-grayScale-500 text-lg">
Question {i + 1}
</span>
</div>
<Button
variant="ghost"
size="icon"
className="text-brand-500 hover:bg-brand-50 rounded-lg"
onClick={() => {
const newQuestions = formData.questions.filter(
(item: any) => item.id !== q.id,
);
setFormData({ ...formData, questions: newQuestions });
}}
>
<Trash2 className="h-5 w-5" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-8">
<div className="md:col-span-8 space-y-3">
<label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest">
QUESTION PROMPT
</label>
<Input
value={q.text}
onChange={(e) => {
const newQuestions = [...formData.questions];
newQuestions[i].text = e.target.value;
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"
placeholder="e.g. How long have you been studying English?"
/>
</div>
<div className="md:col-span-4 space-y-3">
<label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest">
VOICE PROMPT
</label>
<VoicePrompt filename={q.voicePrompt} />
</div>
</div>
<div className="md:w-1/3 space-y-3">
<label className="text-[10px] font-bold text-grayScale-700 uppercase tracking-widest">
SAMPLE ANSWER PROMPT
</label>
<VoicePrompt filename={q.sampleAnswer} />
</div>
</div>
</Card>
))}
<div className="flex items-center gap-8 pt-4">
<button
onClick={addQuestion}
className="flex items-center gap-3 text-brand-500 font-bold text-base hover:opacity-80 transition-all"
>
<div className="flex h-5 w-5 items-center justify-center rounded-full border-2 border-brand-500">
<Plus className="h-3 w-3 stroke-[4]" />
</div>{" "}
Add New Question
</button>
<button className="flex items-center gap-3 text-brand-500 font-bold text-base hover:opacity-80 transition-all">
<div className="flex h-5 w-5 items-center justify-center rounded-full border-2 border-brand-500">
<Plus className="h-3 w-3 stroke-[4]" />
</div>{" "}
Add Tips
</button>
</div>
</div>
<div className="flex items-center justify-between pt-8">
<Button
onClick={prevStep}
variant="outline"
className="h-12 w-28 rounded-xl border-grayScale-200 font-bold text-grayScale-600 shadow-sm"
>
Back
</Button>
<Button
onClick={nextStep}
className="h-12 rounded-xl bg-brand-500 px-8 font-bold hover:bg-brand-600 shadow-md shadow-brand-500/20"
>
Next: Review <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,290 @@
import { Edit2, GripVertical, Trash2, Rocket, Info } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "../../../../components/ui/avatar";
import { PERSONAS } from "./constants";
import { VoicePrompt } from "./VoicePrompt";
import { cn } from "../../../../lib/utils";
interface ReviewStepProps {
formData: any;
selectedPersona: string | null;
prevStep: () => void;
setIsPublished: (val: boolean) => void;
isModuleContext?: boolean;
}
export function ReviewStep({
formData,
selectedPersona,
prevStep,
setIsPublished,
isModuleContext,
}: ReviewStepProps) {
const persona = PERSONAS.find((p) => p.id === selectedPersona);
return (
<div className="space-y-10 animate-in fade-in duration-700">
<div className="flex items-center justify-between px-2">
<h2 className="text-2xl font-bold text-grayScale-900 tracking-tight">
Review Practice Questions
</h2>
</div>
{/* 1. Basic Info Card (Image 1436.1) */}
<Card className="overflow-hidden border border-grayScale-50 shadow-soft rounded-2xl bg-white shadow-sm">
<div className="border-b border-grayScale-50 p-6 px-10 flex justify-between items-center bg-white">
<h3 className="text-[17px] font-extrabold text-grayScale-900">
Basic Information
</h3>
<Button
variant="ghost"
size="sm"
className="text-brand-500 font-bold hover:bg-brand-50 gap-2 h-9"
>
<Edit2 className="h-4 w-4" />
Edit
</Button>
</div>
<div className="p-8 px-10 flex items-center justify-between ">
<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">
<img
src="https://images.unsplash.com/photo-1558403194-611308249627?auto=format&fit=crop&q=80&w=200"
alt="Banner"
className="w-full h-full object-cover opacity-80"
/>
</div>
<div className="space-y-2">
<h4 className="text-[22px] font-extrabold text-grayScale-900 leading-tight">
{formData.title || "Business English 101: Communication"}
</h4>
<div className="flex items-center gap-6 text-[14px]">
<span className="text-grayScale-900 font-bold">
Program:{" "}
<span className="text-brand-500 font-extrabold">
{formData.program}
</span>
</span>
<span className="text-grayScale-900 font-bold">
Course:{" "}
<span className="text-brand-500 font-extrabold">
{formData.course}
</span>
</span>
<span className="text-grayScale-900 font-bold">
Module:{" "}
<span className="text-brand-500 font-extrabold">
Module 101
</span>
</span>
</div>
</div>
</div>
<div className="flex flex-col items-center gap-2">
<span className="text-[11px] font-bold text-grayScale-900 uppercase tracking-widest">
Persona
</span>
<div className="flex items-center gap-2 bg-[#FAF5FF] p-2 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">
<AvatarImage src={persona?.avatar} />
<AvatarFallback>P</AvatarFallback>
</Avatar>
<span className="text-[14px] font-extrabold text-brand-500 capitalize">
{persona?.name || "Alex Johnson"}
</span>
</div>
</div>
</div>
</Card>
{/* 2. Tips Section (Image 1436.1) */}
<div className="space-y-4 px-2">
<div className="flex items-center gap-2">
<label className="text-[12px] font-bold text-grayScale-900 uppercase tracking-widest leading-none">
TIPS / GUIDANCE
</label>
<Info className="h-4 w-4 text-brand-500" />
</div>
<div className="px-5 pt-2 pb-8 bg-white border border-[#E2E8F0] shadow-sm rounded-xl">
<p className="text-[14px] text-grayScale-500 font-medium leading-relaxed">
{formData.tips ||
"Focus on using the present perfect continuous tense to describe an action that started in the past and continues now."}
</p>
</div>
</div>
{isModuleContext ? (
/* 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]">
{/* Left Column: Questions */}
<div className="border-r border-grayScale-50 flex flex-col">
<div className="p-6 px-10 border-b border-grayScale-50 flex items-center gap-3 bg-white">
<h3 className="text-[16px] font-extrabold text-[#0F172A]">
Questions
</h3>
<span className="h-6 w-6 rounded-full bg-grayScale-50 flex items-center justify-center text-[12px] font-extrabold text-grayScale-300">
{formData.questions.length}
</span>
</div>
<div className="p-10 space-y-14">
{formData.questions.map((q: any, i: number) => (
<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">
{(i + 1).toString().padStart(2, "0")}
</span>
<div className="space-y-8">
<div className="space-y-4">
<span className="text-[11px] font-extrabold text-grayScale-600 uppercase tracking-[0.1em] block">
TEXT PROMPT
</span>
<p className="text-[16px] font-medium text-grayScale-600 leading-relaxed max-w-[90%]">
{q.text}
</p>
</div>
<div className="space-y-4">
<span className="text-[11px] font-extrabold text-grayScale-300 uppercase tracking-[0.1em] block">
VOICE PROMPT
</span>
<VoicePrompt
filename={q.voicePrompt}
className="bg-[#FAF5FF]/60 border-[#F3E8FF] h-[72px]"
/>
</div>
</div>
</div>
))}
</div>
</div>
{/* Right Column: Answers */}
<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="flex items-center gap-3">
<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">
{formData.questions.length}
</span>
</div>
<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" />
Edit
</button>
</div>
<div className="p-10 space-y-14">
{formData.questions.map((q: any, i: number) => (
<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">
{(i + 1).toString().padStart(2, "0")}
</span>
<div className="space-y-4">
<span className="text-[11px] font-extrabold text-grayScale-600 uppercase tracking-[0.1em] block">
VOICE PROMPT
</span>
<VoicePrompt
filename={q.sampleAnswer}
className="bg-[#FAF5FF]/60 border-[#F3E8FF] h-[72px]"
/>
</div>
</div>
))}
</div>
</div>
</div>
) : (
/* Original Non-Module View */
<div className="space-y-6">
{formData.questions.map((q: any, i: number) => (
<ReviewItem key={q.id} q={q} index={i} />
))}
</div>
)}
{/* Action Footer */}
<div className="flex items-center justify-between pt-12">
<Button
onClick={prevStep}
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"
>
Back
</Button>
<div className="flex gap-4">
<Button
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"
>
Save as Draft
</Button>
<Button
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"
>
<Rocket className="h-4 w-4" />
Publish Now
</Button>
</div>
</div>
</div>
);
}
function ReviewItem({ q, index }: { q: any; index: number }) {
return (
<Card className="overflow-hidden border border-grayScale-50 shadow-sm rounded-2xl bg-white relative p-8 group">
<div className="absolute left-0 top-0 bottom-0 w-[5px] bg-brand-500 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="space-y-8">
<div className="flex items-center justify-between border-b border-grayScale-50 pb-6">
<div className="flex items-center gap-4">
<GripVertical className="h-4 w-4 text-grayScale-200" />
<span className="font-bold text-grayScale-800 text-[18px]">
Question {index + 1}
</span>
</div>
<div className="flex items-center gap-3">
<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">
<Edit2 className="h-4 w-4" />
</button>
<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" />
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="space-y-4">
<span className="text-[11px] font-extrabold text-grayScale-300 uppercase tracking-widest block">
TEXT PROMPT
</span>
<p className="text-[16px] font-medium text-grayScale-600 leading-relaxed shadow-sm p-6 border border-grayScale-50 rounded-2xl bg-[#F8FAFC]/30">
{q.text}
</p>
</div>
<div className="space-y-4">
<span className="text-[11px] font-extrabold text-grayScale-300 uppercase tracking-widest block">
VOICE PROMPT
</span>
<VoicePrompt
filename={q.voicePrompt}
className="bg-[#FAF5FF]/60 border-[#F3E8FF] h-[76px]"
/>
</div>
</div>
<div className="space-y-4 max-w-sm">
<span className="text-[11px] font-extrabold text-grayScale-300 uppercase tracking-widest block">
SAMPLE ANSWER
</span>
<VoicePrompt
filename={q.sampleAnswer}
className="bg-grayScale-50/10 border-dashed h-[72px]"
/>
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1,115 @@
import { Upload, ArrowRight } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { Card } from "../../../../components/ui/card";
import { Input } from "../../../../components/ui/input";
import { Textarea } from "../../../../components/ui/textarea";
interface ScenarioStepProps {
formData: any;
setFormData: (data: any) => void;
nextStep: () => void;
prevStep: () => void;
}
export function ScenarioStep({
formData,
setFormData,
nextStep,
prevStep,
}: ScenarioStepProps) {
return (
<div className="space-y-6">
<div className="space-y-1 px-2">
<h2 className="text-2xl font-extrabold text-grayScale-700">
Define Scenario Details
</h2>
<p className="text-grayScale-400 text-lg">
Set the scene and context for this English practice session.
</p>
</div>
<Card className="p-8 space-y-6 border-grayScale-50 shadow-soft rounded-2xl bg-white">
<div className="space-y-2">
<label className="text-sm font-bold text-grayScale-700">
Practice Banner Image
</label>
<p className="text-xs text-grayScale-400">
This image will appear as the background for the scenario.
</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="mb-4 rounded-xl border border-grayScale-100 bg-white p-3 text-brand-500 shadow-sm">
<Upload className="h-6 w-6" />
</div>
<p className="text-sm">
<span className="font-bold text-grayScale-700">
Click to upload
</span>{" "}
<span className="text-grayScale-500">or drag and drop</span>
</p>
<p className="mt-1 text-xs text-grayScale-400 uppercase tracking-wide font-bold">
SVG, PNG, JPG (MAX 5MB)
</p>
<Button
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"
>
Browse Files
</Button>
</div>
</div>
</Card>
<Card className="p-8 space-y-6 border-grayScale-50 shadow-soft rounded-2xl bg-white">
<div className="space-y-2">
<label className="text-sm font-bold text-grayScale-700">
Practice Title <span className="text-red-500">*</span>
</label>
<Input
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"
value={formData.title}
onChange={(e) =>
setFormData({ ...formData, title: e.target.value })
}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-grayScale-700">
Scenario Description <span className="text-red-500">*</span>
</label>
<div className="relative">
<Textarea
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"
maxLength={1000}
value={formData.description}
onChange={(e) =>
setFormData({
...formData,
description: e.target.value,
})
}
/>
<div className="absolute bottom-4 right-4 text-xs font-bold text-grayScale-300">
{formData.description.length} / 1000
</div>
</div>
</div>
</Card>
<div className="flex items-center justify-between pt-4">
<Button
onClick={prevStep}
variant="outline"
className="h-12 w-28 rounded-xl border-grayScale-200 font-bold text-grayScale-600 shadow-sm"
>
Back
</Button>
<Button
onClick={nextStep}
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"
>
Next: Persona <ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@ -0,0 +1,44 @@
import { Play, X } from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { cn } from "../../../../lib/utils";
interface VoicePromptProps {
filename: string;
className?: string;
}
export function VoicePrompt({ filename, className }: VoicePromptProps) {
return (
<div
className={cn(
"flex items-center gap-4 p-4 bg-brand-50/5 rounded-xl border border-grayScale-50 h-20 shadow-sm",
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 className="h-5 w-5 fill-current ml-1" />
</div>
<div className="flex-1 min-w-0">
<div className="h-6 flex items-end gap-[2px] px-1 overflow-hidden opacity-40 mb-1">
{[...Array(20)].map((_, idx) => (
<div
key={idx}
className="w-[3px] bg-brand-500 rounded-full"
style={{ height: `${Math.random() * 80 + 20}%` }}
/>
))}
</div>
<p className="text-[10px] font-bold text-brand-500 truncate">
{filename}
</p>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-grayScale-300 rounded-lg"
>
<X className="h-5 w-5" color="#9E2891" />
</Button>
</div>
);
}

View File

@ -0,0 +1,44 @@
export const PERSONAS = [
{
id: "dawit",
name: "Dawit",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Dawit",
},
{
id: "mahlet",
name: "Mahlet",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Mahlet",
},
{
id: "amanuel",
name: "Amanuel",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Amanuel",
},
{
id: "bethel",
name: "Bethel",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Bethel",
},
{
id: "liya",
name: "Liya",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Liya",
},
{
id: "aseffa",
name: "Aseffa",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Aseffa",
},
{
id: "hana",
name: "Hana",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Hana",
},
{
id: "nahom",
name: "Nahom",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Nahom",
},
];
export const STEPS = ["Context", "Scenario", "Persona", "Questions", "Review"];

View File

@ -0,0 +1,162 @@
import { Rocket, Edit2, Layout } from "lucide-react";
import { Button } from "../../../../components/ui/button";
interface ReviewPublishStepProps {
formData: any;
prevStep: () => void;
setIsPublished: (val: boolean) => void;
}
export function ReviewPublishStep({
formData,
prevStep,
setIsPublished,
}: ReviewPublishStepProps) {
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500 pb-20">
{/* 1. Video Preview Card */}
<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">
<h3 className="text-[17px] font-bold text-grayScale-900">
Video Preview
</h3>
<span className="bg-[#FAF5FF] text-brand-500 text-[10px] font-bold px-3 py-1.5 rounded-[6px] tracking-wider uppercase border border-brand-100/50">
PROCESSED
</span>
</div>
<div className="p-10 flex items-center justify-center bg-[#F8FAFC]/30">
<div className="relative w-full max-w-4xl aspect-video rounded-[12px] overflow-hidden bg-black shadow-2xl group border-4 border-white">
{/* Mock Player Control Overlays */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="h-16 w-16 rounded-full bg-white/20 backdrop-blur-md flex items-center justify-center border border-white/30 cursor-pointer hover:scale-110 transition-transform">
<div className="w-0 h-0 border-t-[10px] border-t-transparent border-l-[18px] border-l-white border-b-[10px] border-b-transparent ml-1" />
</div>
</div>
{/* Bottom Controls Placeholder */}
<div className="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/80 to-transparent">
<div className="h-1.5 w-full bg-white/20 rounded-full mb-4 relative overflow-hidden">
<div className="absolute left-0 top-0 bottom-0 w-1/3 bg-brand-500" />
<div className="absolute left-1/3 top-1/2 -translate-y-1/2 h-3 w-3 rounded-full bg-white shadow-lg" />
</div>
<div className="flex items-center justify-between text-white/90 text-sm font-medium">
<span>0:00 / 12:30</span>
<div className="flex items-center gap-4">
<div className="h-4 w-6 border-2 border-white/40 rounded-[2px]" />
<div className="h-4 w-4 border-2 border-white/40 rounded-full" />
</div>
</div>
</div>
</div>
</div>
</div>
{/* 2. Content Details Card */}
<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">
<h3 className="text-[17px] font-bold text-grayScale-900">
Content Details
</h3>
<button
onClick={prevStep}
className="flex items-center gap-2 text-brand-500 font-bold text-sm hover:opacity-80 transition-opacity"
>
<Edit2 className="h-4 w-4" />
Edit
</button>
</div>
<div className="p-8 space-y-10">
{/* Metadata Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="space-y-2">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block">
TITLE
</span>
<p className="text-[15px] font-bold text-grayScale-900">
{formData.title || "Introduction to Past Tense"}
</p>
</div>
<div className="space-y-2">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block">
ASSIGNED MODULE
</span>
<div className="flex items-center gap-2">
<Layout className="h-4 w-4 text-grayScale-400" />
<p className="text-[14px] font-bold text-grayScale-700">
Grammar Basics - Level 1
</p>
</div>
</div>
<div className="space-y-2">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block">
TEACHER NAME
</span>
<p className="text-[15px] font-bold text-grayScale-600">
Abebe Kebede
</p>
</div>
<div className="space-y-2">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block">
FILE SIZE
</span>
<div className="flex items-baseline gap-1.5">
<span className="text-[15px] font-bold text-grayScale-900">
245 MB
</span>
<span className="text-[13px] text-grayScale-400 font-medium">
(1080p MP4)
</span>
</div>
</div>
</div>
{/* Description Section */}
<div className="space-y-3">
<span className="text-[11px] font-bold text-grayScale-300 uppercase tracking-widest block">
DESCRIPTION
</span>
<div
className="text-[14px] text-grayScale-600 font-medium leading-relaxed max-w-4xl"
dangerouslySetInnerHTML={{
__html:
formData.description ||
"This video covers the fundamental rules of forming the past tense in English, focusing on regular verbs ending in -ed. Suitable for beginners. Includes examples and common pitfalls.",
}}
/>
</div>
</div>
{/* 3. Normal Footer (Inside Card) */}
<div className="px-8 py-6 border-t border-grayScale-50 flex items-center justify-between bg-white">
<Button
variant="outline"
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"
>
Back
</Button>
<div className="flex items-center gap-4">
<Button
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"
>
Save as Draft
</Button>
<Button
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"
>
<Rocket className="h-4 w-4" />
Publish Now
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,257 @@
import { useRef, useEffect } from "react";
import {
Video,
List,
Link as LinkIcon,
Lightbulb,
ChevronRight,
ImageIcon,
} from "lucide-react";
import { Button } from "../../../../components/ui/button";
import { Input } from "../../../../components/ui/input";
import { Select } from "../../../../components/ui/select";
interface VideoDetailStepProps {
formData: any;
setFormData: (data: any) => void;
nextStep: () => void;
}
export function VideoDetailStep({
formData,
setFormData,
nextStep,
}: VideoDetailStepProps) {
const editorRef = useRef<HTMLDivElement>(null);
const isInternalChange = useRef(false);
// Initialize editor content only once or when needed from outside
useEffect(() => {
if (editorRef.current && !isInternalChange.current) {
editorRef.current.innerHTML = formData.description || "";
}
}, []);
const handleCommand = (command: string, value?: string) => {
document.execCommand(command, false, value);
syncState();
};
const syncState = () => {
if (editorRef.current) {
isInternalChange.current = true;
setFormData({ ...formData, description: editorRef.current.innerHTML });
// Reset after a short delay to allow exterior updates if any (e.g., from step change)
setTimeout(() => {
isInternalChange.current = false;
}, 0);
}
};
const handleInput = () => {
syncState();
};
return (
<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 */}
<div className="bg-white rounded-[24px] border border-grayScale-50 p-10 shadow-sm space-y-12">
{/* 1. Upload Video Section */}
<div className="space-y-6">
<h3 className="text-[20px] font-bold text-grayScale-900 ml-1">
Upload Video
</h3>
<div className="relative group cursor-pointer">
<div className="flex flex-col items-center justify-center rounded-[20px] border-2 border-dashed border-[#E2E8F0] bg-[#F8FAFC]/30 p-14 transition-all hover:border-brand-200 hover:bg-brand-50/5">
<div className="h-16 w-16 rounded-full bg-white shadow-sm flex items-center justify-center mb-6">
<div className="h-10 w-10 rounded-full bg-[#FAF5FF] flex items-center justify-center">
<div className="h-6 w-6 relative flex items-center justify-center">
<div className="absolute inset-0 bg-brand-500/10 rounded-full blur-sm" />
<Video className="h-5 w-5 text-brand-500 relative" />
</div>
</div>
</div>
<h4 className="text-[17px] font-bold text-grayScale-900 mb-2">
Drag and drop video files here
</h4>
<p className="text-grayScale-400 font-medium text-[13px] mb-8">
MP4, MOV, WebM. Max size 2GB.
</p>
<div className="flex items-center gap-4 w-full max-w-[200px] mb-8">
<div className="flex-1 h-[1px] bg-grayScale-100" />
<span className="text-[10px] font-bold text-grayScale-300 uppercase tracking-widest">
OR
</span>
<div className="flex-1 h-[1px] bg-grayScale-100" />
</div>
<Button
variant="outline"
className="h-11 px-8 rounded-xl border-grayScale-200 bg-white font-bold text-brand-500 hover:border-brand-500 hover:bg-brand-50 transition-all shadow-sm text-sm"
>
Browse Files
</Button>
</div>
</div>
</div>
{/* 2. Form & Side Panel Grid */}
<div className="flex flex-col lg:flex-row gap-12 items-start pt-4 border-t border-grayScale-50">
{/* Left Column: Title, Order, Description */}
<div className="flex-1 w-full space-y-10">
<div className="space-y-3">
<label className="text-[14px] font-bold text-grayScale-900 ml-1">
Video Title
</label>
<Input
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"
value={formData.title}
onChange={(e) =>
setFormData({ ...formData, title: e.target.value })
}
/>
</div>
<div className="space-y-3">
<label className="text-[14px] font-bold text-grayScale-900 ml-1">
Video Order
</label>
<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"
value={formData.order}
onChange={(e) =>
setFormData({ ...formData, order: (e.target as any).value })
}
>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</Select>
</div>
<div className="space-y-3">
<label className="text-[14px] font-bold text-grayScale-900 ml-1">
Description
</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">
{/* Toolbar */}
<div className="flex items-center gap-1 p-2 bg-[#F8FAFC]">
<div className="flex items-center gap-1 w-fit bg-transparent px-2 py-1 rounded-lg">
<button
onClick={() => handleCommand("bold")}
className="h-9 w-9 flex items-center justify-center rounded-lg hover:bg-white hover:shadow-sm text-grayScale-900 transition-all font-serif font-bold text-[17px] pb-0.5 active:bg-grayScale-50"
>
B
</button>
<button
onClick={() => handleCommand("italic")}
className="h-9 w-9 flex items-center justify-center rounded-lg hover:bg-white hover:shadow-sm text-grayScale-900 transition-all font-serif italic text-[17px] pr-0.5 active:bg-grayScale-50"
>
I
</button>
<button
onClick={() => handleCommand("insertUnorderedList")}
className="h-9 w-9 flex items-center justify-center rounded-lg hover:bg-white hover:shadow-sm text-grayScale-900 transition-all active:bg-grayScale-50"
>
<List className="h-5 w-5" />
</button>
<button
onClick={() => {
const url = prompt("Enter URL:");
if (url) handleCommand("createLink", url);
}}
className="h-9 w-9 flex items-center justify-center rounded-lg hover:bg-white hover:shadow-sm text-grayScale-900 transition-all active:bg-grayScale-50"
>
<LinkIcon className="h-4 w-4" />
</button>
</div>
</div>
<div className="relative p-6 flex-1">
{(!formData.description ||
formData.description === "<br>" ||
formData.description === "" ||
formData.description === "<div><br></div>") && (
<div className="absolute top-6 left-6 text-grayScale-300 font-medium text-[15px] pointer-events-none">
Provide a brief summary of what the student will learn...
</div>
)}
<div
ref={editorRef}
contentEditable
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"
// Removed dangerouslySetInnerHTML to prevent cursor jumping
/>
</div>
</div>
</div>
</div>
{/* Right Column: Thumbnail, Pro Tip */}
<div className="w-full lg:w-[320px] space-y-10">
{/* Thumbnail Section */}
<div className="space-y-4">
<div className="space-y-1 ml-1">
<h3 className="text-[14px] font-bold text-grayScale-900">
Thumbnail
</h3>
<p className="text-[12px] text-grayScale-400 font-medium leading-relaxed">
Upload your video thumbnail. 1280×720px recommended.
</p>
</div>
<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-10 w-10 rounded bg-white shadow-sm flex items-center justify-center mb-3">
<ImageIcon className="h-5 w-5 text-grayScale-400" />
</div>
<p className="text-[13px] font-bold text-brand-500">
Click to upload
</p>
</div>
</div>
</div>
{/* Pro Tip Section */}
<div className="bg-[#FAF5FF] rounded-xl border border-[#F3E8FF] p-6 space-y-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">
<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>
<h3 className="text-[14px] font-bold text-grayScale-900">
Pro Tip
</h3>
</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>
{/* Footer (Inside Card Container) */}
<div className="pt-10 border-t border-grayScale-50 flex items-center justify-between">
<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-bold text-grayScale-400">
Last saved: Just now
</span>
</div>
<Button
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"
>
Continue
<ChevronRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
</Button>
</div>
</div>
</div>
);
}