yes
This commit is contained in:
parent
588b238b49
commit
1480eefbe6
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
src/assets/icons/upload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -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 &&
|
||||||
|
item.to === "/notifications" &&
|
||||||
|
unreadCount > 0 && (
|
||||||
<span className="absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full bg-destructive" />
|
<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>
|
||||||
|
)}
|
||||||
|
{!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">
|
<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}
|
{unreadCount > 99 ? "99+" : unreadCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isCollapsed && item.to !== "/notifications" && isActive ? (
|
{!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>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-lg border bg-white px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-grayScale-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-10 w-full rounded-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,
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})
|
},
|
||||||
Input.displayName = "Input"
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
>
|
||||||
|
{/* Connector Line (Behind) */}
|
||||||
|
{index < steps.length - 1 && (
|
||||||
|
<div className="absolute left-1/2 w-[100%] mx-auto top-5 h-[2px] bg-grayScale-200 z-0" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Circle */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid h-10 w-10 place-items-center rounded-full border-2 text-sm font-semibold transition-colors",
|
"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",
|
||||||
isCompleted && "border-brand-500 bg-brand-500 text-white",
|
isCurrent
|
||||||
// Active step should be visually prominent.
|
? "border-brand-500 bg-brand-500 text-white shadow-md scale-110"
|
||||||
isCurrent && "border-brand-500 bg-brand-500 text-white",
|
: "border-grayScale-100 bg-white text-grayScale-400 font-medium",
|
||||||
!isCompleted && !isCurrent && "border-grayScale-300 bg-white text-grayScale-400",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCompleted ? <Check className="h-5 w-5" /> : stepNumber}
|
{stepNumber}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Label */}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-2 text-xs font-medium",
|
"relative z-10 text-[13px] font-bold transition-colors duration-300",
|
||||||
isCurrent && "text-brand-600",
|
isCurrent ? "text-brand-500" : "text-grayScale-400 font-medium",
|
||||||
!isCurrent && "text-grayScale-500",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{step}
|
{step}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<div
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
259
src/pages/content-management/AddPracticeFlow.tsx
Normal file
259
src/pages/content-management/AddPracticeFlow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
src/pages/content-management/AddVideoFlow.tsx
Normal file
155
src/pages/content-management/AddVideoFlow.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
160
src/pages/content-management/CourseDetailPage.tsx
Normal file
160
src/pages/content-management/CourseDetailPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
214
src/pages/content-management/LearnEnglishPage.tsx
Normal file
214
src/pages/content-management/LearnEnglishPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
321
src/pages/content-management/ModuleDetailPage.tsx
Normal file
321
src/pages/content-management/ModuleDetailPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
src/pages/content-management/NewContentPage.tsx
Normal file
81
src/pages/content-management/NewContentPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
265
src/pages/content-management/ProgramCoursesPage.tsx
Normal file
265
src/pages/content-management/ProgramCoursesPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
src/pages/content-management/components/AddModuleModal.tsx
Normal file
131
src/pages/content-management/components/AddModuleModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
src/pages/content-management/components/VideoCard.tsx
Normal file
100
src/pages/content-management/components/VideoCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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"];
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user