From 588b238b497cc89748d3f3f753bc94d80f468415 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 14 Apr 2026 05:09:28 -0700 Subject: [PATCH 1/3] fix human language hierarchy rendering after create Normalize flat hierarchy rows from backend into the nested shape expected by the content-management page so new sub-categories and courses show immediately. Made-with: Cursor --- src/api/courses.api.ts | 180 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index 2565f64..c93de0f 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -507,7 +507,185 @@ export const getHumanLanguageLessonsByCourse = (courseId: number, cefr_level: st }) export const getHumanLanguageHierarchy = () => - http.get("/course-management/hierarchy") + http.get("/course-management/hierarchy").then(async (res) => { + const payload = res.data?.data as unknown + if (payload && typeof payload === "object" && !Array.isArray(payload) && "sub_categories" in payload) { + return res + } + + const rows: UnifiedHierarchyRow[] = Array.isArray(payload) ? payload : [] + const categoryMap = new Map< + number, + { + category_id: number + category_name: string + sub_categories: Map< + number, + { + sub_category_id: number + sub_category_name: string + courses: Map< + number, + { + course_id: number + course_name: string + } + > + } + > + } + >() + + rows.forEach((row) => { + const categoryId = Number(row.category_id) + if (!Number.isFinite(categoryId)) return + + if (!categoryMap.has(categoryId)) { + categoryMap.set(categoryId, { + category_id: categoryId, + category_name: row.category_name ?? "", + sub_categories: new Map(), + }) + } + + if (!row.sub_category_id) return + const subCategoryId = Number(row.sub_category_id) + if (!Number.isFinite(subCategoryId)) return + + const categoryNode = categoryMap.get(categoryId)! + if (!categoryNode.sub_categories.has(subCategoryId)) { + categoryNode.sub_categories.set(subCategoryId, { + sub_category_id: subCategoryId, + sub_category_name: row.sub_category_name ?? "", + courses: new Map(), + }) + } + + if (!row.course_id) return + const courseId = Number(row.course_id) + if (!Number.isFinite(courseId)) return + + const subCategoryNode = categoryNode.sub_categories.get(subCategoryId)! + if (!subCategoryNode.courses.has(courseId)) { + subCategoryNode.courses.set(courseId, { + course_id: courseId, + course_name: row.course_title ?? "", + }) + } + }) + + const selectedCategory = + Array.from(categoryMap.values()).find((c) => c.category_name.toLowerCase().includes("human")) ?? + Array.from(categoryMap.values())[0] + + if (!selectedCategory) { + return { + ...res, + data: { + ...res.data, + data: { + category_id: 0, + category_name: "", + sub_categories: [], + }, + }, + } as unknown as { data: GetHumanLanguageHierarchyResponse } + } + + const courses = Array.from(selectedCategory.sub_categories.values()).flatMap((sub) => + Array.from(sub.courses.values()).map((course) => ({ sub_category_id: sub.sub_category_id, course })), + ) + + const hierarchyResponses = await Promise.all( + courses.map(({ course }) => + http + .get(`/course-management/courses/${course.course_id}/hierarchy`) + .then((courseRes) => ({ course_id: course.course_id, rows: (courseRes.data?.data ?? []) as CourseHierarchyRow[] })) + .catch(() => ({ course_id: course.course_id, rows: [] as CourseHierarchyRow[] })), + ), + ) + + const hierarchyByCourse = new Map( + hierarchyResponses.map((h) => [h.course_id, h.rows]), + ) + + const subCategories = Array.from(selectedCategory.sub_categories.values()).map((sub) => ({ + sub_category_id: sub.sub_category_id, + sub_category_name: sub.sub_category_name, + courses: Array.from(sub.courses.values()).map((course) => { + const levelMap = new Map< + string, + { + level: string + modules: Map< + number, + { + id: number + title: string + sub_modules: Map + } + > + } + >() + + ;(hierarchyByCourse.get(course.course_id) ?? []).forEach((row) => { + if (!row.level_id || !row.cefr_level) return + const levelKey = String(row.cefr_level).toUpperCase() + if (!levelMap.has(levelKey)) { + levelMap.set(levelKey, { level: levelKey, modules: new Map() }) + } + + if (!row.module_id) return + const levelNode = levelMap.get(levelKey)! + const moduleId = Number(row.module_id) + if (!levelNode.modules.has(moduleId)) { + levelNode.modules.set(moduleId, { + id: moduleId, + title: row.module_title ?? "", + sub_modules: new Map(), + }) + } + + if (!row.sub_module_id) return + const moduleNode = levelNode.modules.get(moduleId)! + const subModuleId = Number(row.sub_module_id) + if (!moduleNode.sub_modules.has(subModuleId)) { + moduleNode.sub_modules.set(subModuleId, { + id: subModuleId, + title: row.sub_module_title ?? "", + videos: [], + practices: [], + }) + } + }) + + return { + course_id: course.course_id, + course_name: course.course_name, + levels: Array.from(levelMap.values()).map((levelNode) => ({ + level: levelNode.level, + modules: Array.from(levelNode.modules.values()).map((moduleNode) => ({ + id: moduleNode.id, + title: moduleNode.title, + sub_modules: Array.from(moduleNode.sub_modules.values()), + })), + })), + } + }), + })) + + return { + ...res, + data: { + ...res.data, + data: { + category_id: selectedCategory.category_id, + category_name: selectedCategory.category_name, + sub_categories: subCategories, + }, + }, + } as unknown as { data: GetHumanLanguageHierarchyResponse } + }) export const createHumanLanguageLesson = (data: CreateHumanLanguageLessonRequest) => http From 1480eefbe6c09bd98037015cc81fcc9cd9fdf761 Mon Sep 17 00:00:00 2001 From: elnatansamuel25 Date: Wed, 22 Apr 2026 17:10:43 +0300 Subject: [PATCH 2/3] yes --- package-lock.json | 10 +- package.json | 1 + src/app/AppRoutes.tsx | 195 +++++++---- src/assets/icons/upload.png | Bin 0 -> 4035 bytes src/components/sidebar/Sidebar.tsx | 112 +++--- src/components/ui/input.tsx | 36 +- src/components/ui/stepper.tsx | 85 +++-- .../content-management/AddPracticeFlow.tsx | 259 ++++++++++++++ src/pages/content-management/AddVideoFlow.tsx | 155 +++++++++ .../content-management/CourseDetailPage.tsx | 160 +++++++++ .../content-management/LearnEnglishPage.tsx | 214 ++++++++++++ .../content-management/ModuleDetailPage.tsx | 321 ++++++++++++++++++ .../content-management/NewContentPage.tsx | 81 +++++ .../content-management/ProgramCoursesPage.tsx | 265 +++++++++++++++ .../components/AddModuleModal.tsx | 131 +++++++ .../components/VideoCard.tsx | 100 ++++++ .../components/practice-steps/ContextStep.tsx | 146 ++++++++ .../components/practice-steps/PersonaStep.tsx | 91 +++++ .../practice-steps/QuestionsStep.tsx | 140 ++++++++ .../components/practice-steps/ReviewStep.tsx | 290 ++++++++++++++++ .../practice-steps/ScenarioStep.tsx | 115 +++++++ .../components/practice-steps/VoicePrompt.tsx | 44 +++ .../components/practice-steps/constants.ts | 44 +++ .../video-steps/ReviewPublishStep.tsx | 162 +++++++++ .../video-steps/VideoDetailStep.tsx | 257 ++++++++++++++ 25 files changed, 3238 insertions(+), 176 deletions(-) create mode 100644 src/assets/icons/upload.png create mode 100644 src/pages/content-management/AddPracticeFlow.tsx create mode 100644 src/pages/content-management/AddVideoFlow.tsx create mode 100644 src/pages/content-management/CourseDetailPage.tsx create mode 100644 src/pages/content-management/LearnEnglishPage.tsx create mode 100644 src/pages/content-management/ModuleDetailPage.tsx create mode 100644 src/pages/content-management/NewContentPage.tsx create mode 100644 src/pages/content-management/ProgramCoursesPage.tsx create mode 100644 src/pages/content-management/components/AddModuleModal.tsx create mode 100644 src/pages/content-management/components/VideoCard.tsx create mode 100644 src/pages/content-management/components/practice-steps/ContextStep.tsx create mode 100644 src/pages/content-management/components/practice-steps/PersonaStep.tsx create mode 100644 src/pages/content-management/components/practice-steps/QuestionsStep.tsx create mode 100644 src/pages/content-management/components/practice-steps/ReviewStep.tsx create mode 100644 src/pages/content-management/components/practice-steps/ScenarioStep.tsx create mode 100644 src/pages/content-management/components/practice-steps/VoicePrompt.tsx create mode 100644 src/pages/content-management/components/practice-steps/constants.ts create mode 100644 src/pages/content-management/components/video-steps/ReviewPublishStep.tsx create mode 100644 src/pages/content-management/components/video-steps/VideoDetailStep.tsx diff --git a/package-lock.json b/package-lock.json index 0fb2f56..3a96594 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "lucide-react": "^0.561.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-is": "^19.2.5", "react-router-dom": "^7.10.1", "recharts": "^3.6.0", "sonner": "^2.0.7", @@ -5315,11 +5316,10 @@ } }, "node_modules/react-is": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", - "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", - "license": "MIT", - "peer": true + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT" }, "node_modules/react-redux": { "version": "9.2.0", diff --git a/package.json b/package.json index eba2940..57f998e 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "lucide-react": "^0.561.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-is": "^19.2.5", "react-router-dom": "^7.10.1", "recharts": "^3.6.0", "sonner": "^2.0.7", diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 5c75232..7502414 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -1,52 +1,59 @@ -import { Navigate, Route, Routes } from "react-router-dom" -import { AppLayout } from "../layouts/AppLayout" -import { DashboardPage } from "../pages/DashboardPage" -import { AnalyticsPage } from "../pages/analytics/AnalyticsPage" -import { ContentManagementLayout } from "../pages/content-management/ContentManagementLayout" -import { CourseCategoryPage } from "../pages/content-management/CourseCategoryPage" -import { AllCoursesPage } from "../pages/content-management/AllCoursesPage" -import { CourseFlowBuilderPage } from "../pages/content-management/CourseFlowBuilderPage" -import { ContentOverviewPage } from "../pages/content-management/ContentOverviewPage" -import { CoursesPage } from "../pages/content-management/CoursesPage" -import { PracticeQuestionsPage } from "../pages/content-management/PracticeQuestionsPage" -import { AddNewPracticePage } from "../pages/content-management/AddNewPracticePage" -import { SubModulesPage } from "../pages/content-management/SubCoursesPage" -import { SubModuleContentPage } from "../pages/content-management/SubCourseContentPage" -import { SpeakingPage } from "../pages/content-management/SpeakingPage" -import { AddVideoPage } from "../pages/content-management/AddVideoPage" -import { AddPracticePage } from "../pages/content-management/AddPracticePage" -import { NotFoundPage } from "../pages/NotFoundPage" -import { NotificationsPage } from "../pages/notifications/NotificationsPage" -import { CreateNotificationPage } from "../pages/notifications/CreateNotificationPage" -import { UserDetailPage } from "../pages/user-management/UserDetailPage" -import { UserManagementLayout } from "../pages/user-management/UserManagementLayout" -import { UsersListPage } from "../pages/user-management/UsersListPage" -import { UserManagementDashboard } from "../pages/user-management/UserManagementDashboard" -import { UserGroupsPage } from "../pages/user-management/UserGroupsPage" -import { DeletionRequestsPage } from "../pages/user-management/DeletionRequestsPage" -import { RoleManagementLayout } from "../pages/role-management/RoleManagementLayout" -import { RolesListPage } from "../pages/role-management/RolesListPage" -import { AddRolePage } from "../pages/role-management/AddRolePage" -import { PracticeDetailsPage } from "../pages/content-management/PracticeDetailsPage" -import { PracticeMembersPage } from "../pages/content-management/PracticeMembersPage" -import { QuestionsPage } from "../pages/content-management/QuestionsPage" -import { AddQuestionPage } from "../pages/content-management/AddQuestionPage" -import { HumanLanguagePage } from "../pages/content-management/HumanLanguagePage" -import { HumanLanguageSubModulePage } from "../pages/content-management/HumanLanguageSubModulePage" -import { UserLogPage } from "../pages/user-log/UserLogPage" -import { IssuesPage } from "../pages/issues/IssuesPage" -import { ProfilePage } from "../pages/ProfilePage" -import { SettingsPage } from "../pages/SettingsPage" -import { TeamManagementPage } from "../pages/team/TeamManagementPage" -import { AddTeamMemberPage } from "../pages/team/AddTeamMemberPage" -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" +import { Navigate, Route, Routes } from "react-router-dom"; +import { AppLayout } from "../layouts/AppLayout"; +import { DashboardPage } from "../pages/DashboardPage"; +import { AnalyticsPage } from "../pages/analytics/AnalyticsPage"; +import { ContentManagementLayout } from "../pages/content-management/ContentManagementLayout"; +import { CourseCategoryPage } from "../pages/content-management/CourseCategoryPage"; +import { AllCoursesPage } from "../pages/content-management/AllCoursesPage"; +import { CourseFlowBuilderPage } from "../pages/content-management/CourseFlowBuilderPage"; +import { ContentOverviewPage } from "../pages/content-management/ContentOverviewPage"; +import { CoursesPage } from "../pages/content-management/CoursesPage"; +import { PracticeQuestionsPage } from "../pages/content-management/PracticeQuestionsPage"; +import { AddNewPracticePage } from "../pages/content-management/AddNewPracticePage"; +import { SubModulesPage } from "../pages/content-management/SubCoursesPage"; +import { SubModuleContentPage } from "../pages/content-management/SubCourseContentPage"; +import { SpeakingPage } from "../pages/content-management/SpeakingPage"; +import { AddVideoPage } from "../pages/content-management/AddVideoPage"; +import { AddPracticePage } from "../pages/content-management/AddPracticePage"; +import { NewContentPage } from "../pages/content-management/NewContentPage"; +import { LearnEnglishPage } from "../pages/content-management/LearnEnglishPage"; +import { ProgramCoursesPage } from "../pages/content-management/ProgramCoursesPage"; +import { CourseDetailPage } from "../pages/content-management/CourseDetailPage"; +import { ModuleDetailPage } from "../pages/content-management/ModuleDetailPage"; +import { AddVideoFlow } from "../pages/content-management/AddVideoFlow"; +import { AddPracticeFlow } from "../pages/content-management/AddPracticeFlow"; +import { NotFoundPage } from "../pages/NotFoundPage"; +import { NotificationsPage } from "../pages/notifications/NotificationsPage"; +import { CreateNotificationPage } from "../pages/notifications/CreateNotificationPage"; +import { UserDetailPage } from "../pages/user-management/UserDetailPage"; +import { UserManagementLayout } from "../pages/user-management/UserManagementLayout"; +import { UsersListPage } from "../pages/user-management/UsersListPage"; +import { UserManagementDashboard } from "../pages/user-management/UserManagementDashboard"; +import { UserGroupsPage } from "../pages/user-management/UserGroupsPage"; +import { DeletionRequestsPage } from "../pages/user-management/DeletionRequestsPage"; +import { RoleManagementLayout } from "../pages/role-management/RoleManagementLayout"; +import { RolesListPage } from "../pages/role-management/RolesListPage"; +import { AddRolePage } from "../pages/role-management/AddRolePage"; +import { PracticeDetailsPage } from "../pages/content-management/PracticeDetailsPage"; +import { PracticeMembersPage } from "../pages/content-management/PracticeMembersPage"; +import { QuestionsPage } from "../pages/content-management/QuestionsPage"; +import { AddQuestionPage } from "../pages/content-management/AddQuestionPage"; +import { HumanLanguagePage } from "../pages/content-management/HumanLanguagePage"; +import { HumanLanguageSubModulePage } from "../pages/content-management/HumanLanguageSubModulePage"; +import { UserLogPage } from "../pages/user-log/UserLogPage"; +import { IssuesPage } from "../pages/issues/IssuesPage"; +import { ProfilePage } from "../pages/ProfilePage"; +import { SettingsPage } from "../pages/SettingsPage"; +import { TeamManagementPage } from "../pages/team/TeamManagementPage"; +import { AddTeamMemberPage } from "../pages/team/AddTeamMemberPage"; +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() { return ( @@ -91,19 +98,52 @@ export function AppRoutes() { path="human-language/:categoryId/:courseId/sub-module/:subModuleId" element={} /> - } /> - } /> + } + /> + } + /> {/* Course → Sub-module → Lesson/Practice */} - } /> - } /> - } /> - } /> + } + /> + } + /> + } + /> + } + /> {/* Legacy aliases */} - } /> - } /> - } /> - } /> - } /> + } + /> + } + /> + } + /> + } + /> + } + /> } /> } /> } /> @@ -113,8 +153,37 @@ export function AppRoutes() { } /> + } /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } /> - } /> + } + /> } /> } /> } /> @@ -128,7 +197,5 @@ export function AppRoutes() { } /> - ) + ); } - - diff --git a/src/assets/icons/upload.png b/src/assets/icons/upload.png new file mode 100644 index 0000000000000000000000000000000000000000..44342e3ce74dc8fb6c15cf95483301a1af018e01 GIT binary patch literal 4035 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 literal 0 HcmV?d00001 diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index 10bd6da..ce361c2 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -13,57 +13,65 @@ import { Users, Users2, X, -} from "lucide-react" -import { type ComponentType, useEffect, useState } from "react" -import { NavLink } from "react-router-dom" -import { cn } from "../../lib/utils" -import { BrandLogo } from "../brand/BrandLogo" -import { getUnreadCount } from "../../api/notifications.api" +} from "lucide-react"; +import { type ComponentType, useEffect, useState } from "react"; +import { NavLink } from "react-router-dom"; +import { cn } from "../../lib/utils"; +import { BrandLogo } from "../brand/BrandLogo"; +import { getUnreadCount } from "../../api/notifications.api"; type NavItem = { - label: string - to: string - icon: ComponentType<{ className?: string }> -} + label: string; + to: string; + icon: ComponentType<{ className?: string }>; +}; const navItems: NavItem[] = [ { label: "Dashboard", to: "/dashboard", icon: LayoutDashboard }, { label: "User Management", to: "/users", icon: Users }, { label: "Role Management", to: "/roles", icon: Shield }, { label: "Content Management", to: "/content", icon: BookOpen }, + { label: "New Content", to: "/new-content", icon: BookOpen }, + { label: "Notifications", to: "/notifications", icon: Bell }, { label: "User Log", to: "/user-log", icon: ClipboardList }, { label: "Issue Reports", to: "/issues", icon: CircleAlert }, { label: "Analytics", to: "/analytics", icon: BarChart3 }, { label: "Team Management", to: "/team", icon: Users2 }, { label: "Profile", to: "/profile", icon: UserCircle2 }, -] +]; type SidebarProps = { - isOpen: boolean - isCollapsed: boolean - onToggleCollapse: () => void - onClose: () => void -} + isOpen: boolean; + isCollapsed: boolean; + onToggleCollapse: () => void; + onClose: () => void; +}; -export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: SidebarProps) { - const [unreadCount, setUnreadCount] = useState(0) +export function Sidebar({ + isOpen, + isCollapsed, + onToggleCollapse, + onClose, +}: SidebarProps) { + const [unreadCount, setUnreadCount] = useState(0); useEffect(() => { const fetchUnread = async () => { try { - const res = await getUnreadCount() - setUnreadCount(res.data.unread) + const res = await getUnreadCount(); + setUnreadCount(res.data.unread); } catch { // silently fail } - } + }; - fetchUnread() + fetchUnread(); - window.addEventListener("notifications-updated", fetchUnread) - return () => window.removeEventListener("notifications-updated", fetchUnread) - }, []) + window.addEventListener("notifications-updated", fetchUnread); + return () => + window.removeEventListener("notifications-updated", fetchUnread); + }, []); return ( <> @@ -86,7 +94,12 @@ export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: Side isOpen ? "translate-x-0" : "-translate-x-full", )} > -
+
{isCollapsed ? ( @@ -103,7 +116,11 @@ export function Sidebar({ isOpen, isCollapsed, onToggleCollapse, onClose }: Side onClick={onToggleCollapse} aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"} > - {isCollapsed ? : } + {isCollapsed ? ( + + ) : ( + + )}
- ) + ); } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index c46b5cd..83cec90 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,21 +1,21 @@ -import * as React from "react" -import { cn } from "../../lib/utils" +import * as React from "react"; +import { cn } from "../../lib/utils"; export interface InputProps extends React.InputHTMLAttributes {} -export const Input = React.forwardRef(({ className, type, ...props }, ref) => { - return ( - - ) -}) -Input.displayName = "Input" - - +export const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; diff --git a/src/components/ui/stepper.tsx b/src/components/ui/stepper.tsx index a4627aa..a0c87a2 100644 --- a/src/components/ui/stepper.tsx +++ b/src/components/ui/stepper.tsx @@ -1,61 +1,52 @@ -import * as React from "react" -import { Check } from "lucide-react" -import { cn } from "../../lib/utils" +import { cn } from "../../lib/utils"; export interface StepperProps { - steps: string[] - currentStep: number - className?: string + steps: string[]; + currentStep: number; + className?: string; } export function Stepper({ steps, currentStep, className }: StepperProps) { return ( -
+
{steps.map((step, index) => { - const stepNumber = index + 1 - const isCompleted = stepNumber < currentStep - const isCurrent = stepNumber === currentStep + const stepNumber = index + 1; + const isCurrent = stepNumber === currentStep; return ( - -
-
-
- {isCompleted ? : stepNumber} -
- - {step} - -
-
+
+ {/* Connector Line (Behind) */} {index < steps.length - 1 && ( -
+
)} - - ) + + {/* Circle */} +
+ {stepNumber} +
+ + {/* Label */} + + {step} + +
+ ); })}
- ) + ); } - diff --git a/src/pages/content-management/AddPracticeFlow.tsx b/src/pages/content-management/AddPracticeFlow.tsx new file mode 100644 index 0000000..7e3b742 --- /dev/null +++ b/src/pages/content-management/AddPracticeFlow.tsx @@ -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( + "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 ( +
+
+
+ Success +
+

+ Practice Published Successfully! +

+

+ Your speaking practice is now active and available inside the module. +

+
+ + +
+
+ ); + } + + // Helper to map currentStep to the actual component for the module flow + const renderStep = () => { + if (!isModuleContext) { + switch (currentStep) { + case 1: + return ( + + ); + case 2: + return ( + + ); + case 3: + return ( + + ); + case 4: + return ( + + ); + case 5: + return ( + + ); + default: + return null; + } + } else { + // Module Context Flow (Skips Scenario) + switch (currentStep) { + case 1: + return ( + + ); + case 2: + return ( + + ); + case 3: + return ( + + ); + case 4: + return ( + + ); + default: + return null; + } + } + }; + + return ( +
+ {/* Header */} +
+
+ + + {backLabel} + + +
+ +
+

+ Add New Practice +

+

+ Create a new immersive practice session for students. +

+
+ +
+ +
+ +
{renderStep()}
+
+
+ ); +} diff --git a/src/pages/content-management/AddVideoFlow.tsx b/src/pages/content-management/AddVideoFlow.tsx new file mode 100644 index 0000000..7086bd4 --- /dev/null +++ b/src/pages/content-management/AddVideoFlow.tsx @@ -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 ( +
+ {/* Success Icon Wrapper (Jagged Circle Style) */} +
+
+
+
+ +
+ {/* Sub-Jagged layer for depth if needed */} +
+
+
+ +

+ Video Published Successfully! +

+

+ Your video is now live and available inside the selected module. +

+ +
+ + +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ + + Back to Modules + + +
+ +

+ Add New Video +

+ +
+ s.label)} + currentStep={currentStep} + /> +
+ + {/* Step Content */} +
+ {currentStep === 1 && ( + + )} + + {currentStep === 2 && ( + + )} +
+
+
+ ); +} diff --git a/src/pages/content-management/CourseDetailPage.tsx b/src/pages/content-management/CourseDetailPage.tsx new file mode 100644 index 0000000..52896fd --- /dev/null +++ b/src/pages/content-management/CourseDetailPage.tsx @@ -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 ( +
+ {/* Header Navigation */} +
+ + + Back to Levels + +
+ + {/* Hero Section */} +
+
+

+ {courseId?.toUpperCase() || "A1"} +

+

+ Learn basic English words, phrases, and simple sentences for daily + situations. +

+
+
+ + +
+
+ + setIsAddModuleOpen(false)} + /> + + {/* Gradient Grid */} +
+ {MODULES.map((module) => ( + + {/* Gradient Banner */} +
+ +
+
+ {/* Icon Circle */} +
+ +
+ + {/* Content */} +
+

+ {module.title} +

+

+ {module.description} +

+
+
+ + {/* Actions */} +
+ + {module.status === "Published" ? ( + + ) : ( + + )} +
+
+ + ))} +
+
+ ); +} diff --git a/src/pages/content-management/LearnEnglishPage.tsx b/src/pages/content-management/LearnEnglishPage.tsx new file mode 100644 index 0000000..05e5070 --- /dev/null +++ b/src/pages/content-management/LearnEnglishPage.tsx @@ -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 ( +
+ {/* Header section */} +
+
+

+ Learn English +

+

+ Manage learning content by level +

+
+ + + + + + + + + Add New Program + + + Create a learning program to group courses by learner level + + + {/* Gradient Divider */} +
+
+
+ + {/* Gradient Divider */} +
+