changes
This commit is contained in:
parent
afccaf9892
commit
79e2ef6ce1
93
package-lock.json
generated
93
package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
|
@ -1332,6 +1333,98 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-direction": {
|
"node_modules/@radix-ui/react-direction": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,22 @@ import { ContentManagementLayout } from "../pages/content-management/ContentMana
|
||||||
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 { SpeakingPage } from "../pages/content-management/SpeakingPage"
|
import { SpeakingPage } from "../pages/content-management/SpeakingPage"
|
||||||
|
import { AddVideoPage } from "../pages/content-management/AddVideoPage"
|
||||||
|
import { AddPracticePage } from "../pages/content-management/AddPracticePage"
|
||||||
import { NotFoundPage } from "../pages/NotFoundPage"
|
import { NotFoundPage } from "../pages/NotFoundPage"
|
||||||
import { NotificationsPage } from "../pages/notifications/NotificationsPage"
|
import { NotificationsPage } from "../pages/notifications/NotificationsPage"
|
||||||
import { PlaceholderPage } from "../pages/PlaceholderPage"
|
import { PlaceholderPage } from "../pages/PlaceholderPage"
|
||||||
import { UserDetailPage } from "../pages/user-management/UserDetailPage"
|
import { UserDetailPage } from "../pages/user-management/UserDetailPage"
|
||||||
import { UserManagementLayout } from "../pages/user-management/UserManagementLayout"
|
import { UserManagementLayout } from "../pages/user-management/UserManagementLayout"
|
||||||
import { UsersListPage } from "../pages/user-management/UsersListPage"
|
import { UsersListPage } from "../pages/user-management/UsersListPage"
|
||||||
|
import { UserManagementDashboard } from "../pages/user-management/UserManagementDashboard"
|
||||||
|
import { UserGroupsPage } from "../pages/user-management/UserGroupsPage"
|
||||||
|
import { RegisterUserPage } from "../pages/user-management/RegisterUserPage"
|
||||||
|
import { RoleManagementLayout } from "../pages/role-management/RoleManagementLayout"
|
||||||
|
import { RolesListPage } from "../pages/role-management/RolesListPage"
|
||||||
|
import { AddRolePage } from "../pages/role-management/AddRolePage"
|
||||||
|
import { PracticeDetailsPage } from "../pages/content-management/PracticeDetailsPage"
|
||||||
|
import { PracticeMembersPage } from "../pages/content-management/PracticeMembersPage"
|
||||||
import { UserLogPage } from "../pages/user-log/UserLogPage"
|
import { UserLogPage } from "../pages/user-log/UserLogPage"
|
||||||
|
|
||||||
export function AppRoutes() {
|
export function AppRoutes() {
|
||||||
|
|
@ -21,14 +31,26 @@ export function AppRoutes() {
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
<Route path="/dashboard" element={<DashboardPage />} />
|
<Route path="/dashboard" element={<DashboardPage />} />
|
||||||
<Route path="/users" element={<UserManagementLayout />}>
|
<Route path="/users" element={<UserManagementLayout />}>
|
||||||
<Route index element={<UsersListPage />} />
|
<Route index element={<UserManagementDashboard />} />
|
||||||
|
<Route path="list" element={<UsersListPage />} />
|
||||||
|
<Route path="register" element={<RegisterUserPage />} />
|
||||||
|
<Route path="groups" element={<UserGroupsPage />} />
|
||||||
<Route path=":id" element={<UserDetailPage />} />
|
<Route path=":id" element={<UserDetailPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/roles" element={<RoleManagementLayout />}>
|
||||||
|
<Route index element={<RolesListPage />} />
|
||||||
|
<Route path="add" element={<AddRolePage />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="/content" element={<ContentManagementLayout />}>
|
<Route path="/content" element={<ContentManagementLayout />}>
|
||||||
<Route index element={<ContentOverviewPage />} />
|
<Route index element={<ContentOverviewPage />} />
|
||||||
<Route path="courses" element={<CoursesPage />} />
|
<Route path="courses" element={<CoursesPage />} />
|
||||||
|
<Route path="courses/add-video" element={<AddVideoPage />} />
|
||||||
<Route path="speaking" element={<SpeakingPage />} />
|
<Route path="speaking" element={<SpeakingPage />} />
|
||||||
|
<Route path="speaking/add-practice" element={<AddPracticePage />} />
|
||||||
|
<Route path="practices" element={<PracticeDetailsPage />} />
|
||||||
|
<Route path="practices/members" element={<PracticeMembersPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/notifications" element={<NotificationsPage />} />
|
<Route path="/notifications" element={<NotificationsPage />} />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
Shield,
|
||||||
UserCircle2,
|
UserCircle2,
|
||||||
Users,
|
Users,
|
||||||
Users2,
|
Users2,
|
||||||
|
|
@ -23,6 +24,7 @@ type NavItem = {
|
||||||
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: "Content Management", to: "/content", icon: BookOpen },
|
{ label: "Content Management", to: "/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 },
|
||||||
|
|
@ -33,12 +35,12 @@ const navItems: NavItem[] = [
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
return (
|
return (
|
||||||
<aside className="flex w-[264px] flex-col border-r bg-grayScale-50 px-4 py-5">
|
<aside className="fixed left-0 top-0 flex h-screen w-[264px] flex-col border-r bg-grayScale-50 px-4 py-5">
|
||||||
<div className="px-2">
|
<div className="px-2">
|
||||||
<BrandLogo />
|
<BrandLogo />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="mt-6 space-y-1">
|
<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 (
|
||||||
|
|
@ -75,7 +77,7 @@ export function Sidebar() {
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="mt-auto px-2 pt-6">
|
<div className="px-2 pt-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="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"
|
className="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"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export function Topbar() {
|
||||||
<header className="sticky top-0 z-10 flex h-16 items-center justify-end gap-3 border-b bg-grayScale-50/85 px-6 backdrop-blur">
|
<header className="sticky top-0 z-10 flex h-16 items-center justify-end gap-3 border-b bg-grayScale-50/85 px-6 backdrop-blur">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid h-10 w-10 place-items-center rounded-full border bg-white text-grayScale-500 hover:text-brand-600"
|
className="grid h-10 w-10 place-items-center rounded-full border bg-white text-grayScale-500 hover:text-brand-600 transition-colors"
|
||||||
aria-label="Notifications"
|
aria-label="Notifications"
|
||||||
>
|
>
|
||||||
<Bell className="h-5 w-5" />
|
<Bell className="h-5 w-5" />
|
||||||
|
|
@ -14,7 +14,7 @@ export function Topbar() {
|
||||||
|
|
||||||
<Avatar className="h-10 w-10 ring-2 ring-brand-100">
|
<Avatar className="h-10 w-10 ring-2 ring-brand-100">
|
||||||
<AvatarImage src="" alt="Admin" />
|
<AvatarImage src="" alt="Admin" />
|
||||||
<AvatarFallback className="bg-brand-500 text-white">JA</AvatarFallback>
|
<AvatarFallback className="bg-brand-500 text-sm font-medium text-white">JA</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
99
src/components/ui/dialog.tsx
Normal file
99
src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
||||||
|
|
||||||
103
src/components/ui/file-upload.tsx
Normal file
103
src/components/ui/file-upload.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Upload } from "lucide-react"
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
export interface FileUploadProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
|
||||||
|
onFileSelect?: (file: File | null) => void
|
||||||
|
accept?: string
|
||||||
|
label?: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileUpload = React.forwardRef<HTMLInputElement, FileUploadProps>(
|
||||||
|
({ className, onFileSelect, accept, label, description, ...props }, ref) => {
|
||||||
|
const [file, setFile] = React.useState<File | null>(null)
|
||||||
|
const [dragActive, setDragActive] = React.useState(false)
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)
|
||||||
|
|
||||||
|
const handleFile = (selectedFile: File | null) => {
|
||||||
|
setFile(selectedFile)
|
||||||
|
onFileSelect?.(selectedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrag = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.type === "dragenter" || e.type === "dragover") {
|
||||||
|
setDragActive(true)
|
||||||
|
} else if (e.type === "dragleave") {
|
||||||
|
setDragActive(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setDragActive(false)
|
||||||
|
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||||
|
handleFile(e.dataTransfer.files[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
handleFile(e.target.files[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors",
|
||||||
|
dragActive ? "border-brand-500 bg-brand-50" : "border-grayScale-200 bg-grayScale-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onDragEnter={handleDrag}
|
||||||
|
onDragLeave={handleDrag}
|
||||||
|
onDragOver={handleDrag}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept={accept}
|
||||||
|
onChange={handleChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||||||
|
<div className="mb-4 grid h-16 w-16 place-items-center rounded-full bg-brand-100 text-brand-600">
|
||||||
|
<Upload className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
{file ? (
|
||||||
|
<>
|
||||||
|
<p className="mb-1 text-sm font-medium text-grayScale-900">{file.name}</p>
|
||||||
|
<p className="text-xs text-grayScale-500">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="mb-1 text-sm font-medium text-grayScale-900">
|
||||||
|
{label || "Drag & Drop Video Here"}
|
||||||
|
</p>
|
||||||
|
<p className="mb-4 text-xs text-grayScale-500">
|
||||||
|
{description || "or click to browse files"}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
className="rounded-lg bg-brand-500 px-4 py-2 text-sm font-medium text-white hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
Browse Files
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
FileUpload.displayName = "FileUpload"
|
||||||
|
|
||||||
27
src/components/ui/select.tsx
Normal file
27
src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { ChevronDown } from "lucide-react"
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
|
||||||
|
|
||||||
|
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
||||||
|
({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full appearance-none rounded-lg border bg-white px-3 py-2 pr-8 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-grayScale-400" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Select.displayName = "Select"
|
||||||
|
|
||||||
58
src/components/ui/stepper.tsx
Normal file
58
src/components/ui/stepper.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Check } from "lucide-react"
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
export interface StepperProps {
|
||||||
|
steps: string[]
|
||||||
|
currentStep: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stepper({ steps, currentStep, className }: StepperProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center justify-between", className)}>
|
||||||
|
{steps.map((step, index) => {
|
||||||
|
const stepNumber = index + 1
|
||||||
|
const isCompleted = stepNumber < currentStep
|
||||||
|
const isCurrent = stepNumber === currentStep
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={step}>
|
||||||
|
<div className="flex flex-1 items-center">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid h-10 w-10 place-items-center rounded-full border-2 text-sm font-semibold transition-colors",
|
||||||
|
isCompleted && "border-brand-500 bg-brand-500 text-white",
|
||||||
|
isCurrent && "border-brand-500 bg-brand-50 text-brand-600",
|
||||||
|
!isCompleted && !isCurrent && "border-grayScale-300 bg-white text-grayScale-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isCompleted ? <Check className="h-5 w-5" /> : stepNumber}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"mt-2 text-xs font-medium",
|
||||||
|
isCurrent && "text-brand-600",
|
||||||
|
!isCurrent && "text-grayScale-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{step}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{index < steps.length - 1 && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mx-4 h-0.5 flex-1",
|
||||||
|
isCompleted ? "bg-brand-500" : "bg-grayScale-200",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
21
src/components/ui/textarea.tsx
Normal file
21
src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "../../lib/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-lg border bg-white px-3 py-2 text-sm ring-offset-background placeholder:text-grayScale-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { Topbar } from "../components/topbar/Topbar"
|
||||||
|
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full bg-grayScale-100">
|
<div className="flex min-h-screen bg-grayScale-100">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="ml-[264px] flex min-w-0 flex-1 flex-col">
|
||||||
<Topbar />
|
<Topbar />
|
||||||
<main className="min-w-0 flex-1 px-6 pb-8 pt-4">
|
<main className="min-w-0 flex-1 overflow-y-auto px-6 pb-8 pt-4">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
554
src/pages/content-management/AddPracticePage.tsx
Normal file
554
src/pages/content-management/AddPracticePage.tsx
Normal file
|
|
@ -0,0 +1,554 @@
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { X, Plus, Check, ArrowLeft } 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"
|
||||||
|
import { Select } from "../../components/ui/select"
|
||||||
|
import { Stepper } from "../../components/ui/stepper"
|
||||||
|
import { Badge } from "../../components/ui/badge"
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar"
|
||||||
|
|
||||||
|
type QuestionType = "multiple-choice" | "short-answer" | "true-false"
|
||||||
|
|
||||||
|
interface Question {
|
||||||
|
id: string
|
||||||
|
question: string
|
||||||
|
type: QuestionType
|
||||||
|
options: string[]
|
||||||
|
correctAnswer: string
|
||||||
|
points: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PracticeFormData {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
category: string
|
||||||
|
difficulty: string
|
||||||
|
duration: string
|
||||||
|
tags: string
|
||||||
|
participants: string[]
|
||||||
|
questions: Question[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const STEPS = ["Details", "Questions", "Review"]
|
||||||
|
|
||||||
|
export function AddPracticePage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [currentStep, setCurrentStep] = useState(1)
|
||||||
|
const [formData, setFormData] = useState<PracticeFormData>({
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
category: "",
|
||||||
|
difficulty: "",
|
||||||
|
duration: "",
|
||||||
|
tags: "",
|
||||||
|
participants: [],
|
||||||
|
questions: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const [currentQuestion, setCurrentQuestion] = useState<Partial<Question>>({
|
||||||
|
question: "",
|
||||||
|
type: "multiple-choice",
|
||||||
|
options: ["", "", "", ""],
|
||||||
|
correctAnswer: "",
|
||||||
|
points: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockParticipants = [
|
||||||
|
{ id: "1", name: "Sarah", avatar: "" },
|
||||||
|
{ id: "2", name: "Jon", avatar: "" },
|
||||||
|
{ id: "3", name: "Priya", avatar: "" },
|
||||||
|
{ id: "4", name: "Jake", avatar: "" },
|
||||||
|
{ id: "5", name: "Emma", avatar: "" },
|
||||||
|
{ id: "6", name: "Robert", avatar: "" },
|
||||||
|
{ id: "7", name: "Luke", avatar: "" },
|
||||||
|
{ id: "8", name: "Ethan", avatar: "" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const toggleParticipant = (id: string) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
participants: prev.participants.includes(id)
|
||||||
|
? prev.participants.filter((p) => p !== id)
|
||||||
|
: [...prev.participants, id],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addQuestion = () => {
|
||||||
|
if (!currentQuestion.question || !currentQuestion.correctAnswer) return
|
||||||
|
|
||||||
|
const newQuestion: Question = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
question: currentQuestion.question,
|
||||||
|
type: currentQuestion.type as QuestionType,
|
||||||
|
options: currentQuestion.options || [],
|
||||||
|
correctAnswer: currentQuestion.correctAnswer,
|
||||||
|
points: currentQuestion.points || 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
questions: [...prev.questions, newQuestion],
|
||||||
|
}))
|
||||||
|
|
||||||
|
setCurrentQuestion({
|
||||||
|
question: "",
|
||||||
|
type: "multiple-choice",
|
||||||
|
options: ["", "", "", ""],
|
||||||
|
correctAnswer: "",
|
||||||
|
points: 10,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addOption = () => {
|
||||||
|
setCurrentQuestion((prev) => ({
|
||||||
|
...prev,
|
||||||
|
options: [...(prev.options || []), ""],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOption = (index: number, value: string) => {
|
||||||
|
setCurrentQuestion((prev) => ({
|
||||||
|
...prev,
|
||||||
|
options: prev.options?.map((opt, i) => (i === index ? value : opt)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
console.log("Practice data:", formData)
|
||||||
|
// Handle form submission
|
||||||
|
}
|
||||||
|
|
||||||
|
const canProceedToStep2 = () => {
|
||||||
|
return (
|
||||||
|
formData.title &&
|
||||||
|
formData.description &&
|
||||||
|
formData.category &&
|
||||||
|
formData.difficulty &&
|
||||||
|
formData.duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canProceedToStep3 = () => {
|
||||||
|
return formData.questions.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => navigate("/content/speaking")}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Add New Practice</h1>
|
||||||
|
</div>
|
||||||
|
<Button className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<Stepper steps={STEPS} currentStep={currentStep} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Step 1: Details */}
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-6 text-lg font-semibold text-grayScale-900">Practice Details</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Practice Title
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
|
placeholder="Enter practice title"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
placeholder="Enter practice description"
|
||||||
|
rows={4}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Category
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select category</option>
|
||||||
|
<option value="grammar">Grammar</option>
|
||||||
|
<option value="vocabulary">Vocabulary</option>
|
||||||
|
<option value="speaking">Speaking</option>
|
||||||
|
<option value="listening">Listening</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Difficulty
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={formData.difficulty}
|
||||||
|
onChange={(e) => setFormData({ ...formData, difficulty: e.target.value })}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select difficulty</option>
|
||||||
|
<option value="beginner">Beginner</option>
|
||||||
|
<option value="intermediate">Intermediate</option>
|
||||||
|
<option value="advanced">Advanced</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Duration (minutes)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={formData.duration}
|
||||||
|
onChange={(e) => setFormData({ ...formData, duration: e.target.value })}
|
||||||
|
placeholder="30"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">Tags</label>
|
||||||
|
<Input
|
||||||
|
value={formData.tags}
|
||||||
|
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
|
||||||
|
placeholder="Enter tags separated by commas"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => setCurrentStep(2)}
|
||||||
|
disabled={!canProceedToStep2()}
|
||||||
|
className="bg-brand-500 hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Questions */}
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Select Participants Section */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-grayScale-900">Select Participants</h2>
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
{mockParticipants.map((participant) => {
|
||||||
|
const isSelected = formData.participants.includes(participant.id)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={participant.id}
|
||||||
|
className="relative flex flex-col items-center"
|
||||||
|
onClick={() => toggleParticipant(participant.id)}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Avatar className="h-16 w-16 cursor-pointer border-2 border-grayScale-200 transition-all hover:border-brand-500">
|
||||||
|
<AvatarImage src={participant.avatar} />
|
||||||
|
<AvatarFallback className="bg-brand-100 text-brand-600">
|
||||||
|
{participant.name[0]}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
{isSelected && (
|
||||||
|
<div className="absolute -right-1 -top-1 grid h-6 w-6 place-items-center rounded-full bg-brand-500 text-white">
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="mt-2 text-sm font-medium text-grayScale-700">
|
||||||
|
{participant.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Add Questions Section */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-grayScale-900">
|
||||||
|
General Practice Questions
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Existing Questions */}
|
||||||
|
{formData.questions.map((q) => (
|
||||||
|
<div key={q.id} className="mb-4 rounded-lg border bg-grayScale-50 p-4">
|
||||||
|
<div className="mb-2 flex items-start justify-between">
|
||||||
|
<p className="font-medium text-grayScale-900">{q.question}</p>
|
||||||
|
<Badge variant="secondary">{q.points} points</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-grayScale-600">
|
||||||
|
Type: {q.type} | Correct Answer: {q.correctAnswer}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Add New Question Form */}
|
||||||
|
<div className="space-y-4 rounded-lg border bg-white p-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Question
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={currentQuestion.question}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentQuestion({ ...currentQuestion, question: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Enter your question"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Question Type
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={currentQuestion.type}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentQuestion({
|
||||||
|
...currentQuestion,
|
||||||
|
type: e.target.value as QuestionType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="multiple-choice">Multiple Choice</option>
|
||||||
|
<option value="short-answer">Short Answer</option>
|
||||||
|
<option value="true-false">True/False</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">Points</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={currentQuestion.points}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentQuestion({
|
||||||
|
...currentQuestion,
|
||||||
|
points: parseInt(e.target.value) || 10,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentQuestion.type === "multiple-choice" && (
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Options
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{currentQuestion.options?.map((option, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={option}
|
||||||
|
onChange={(e) => updateOption(index, e.target.value)}
|
||||||
|
placeholder={`Option ${index + 1}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addOption}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add Option
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Correct Answer
|
||||||
|
</label>
|
||||||
|
{currentQuestion.type === "multiple-choice" ? (
|
||||||
|
<Select
|
||||||
|
value={currentQuestion.correctAnswer}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentQuestion({ ...currentQuestion, correctAnswer: e.target.value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Select correct option</option>
|
||||||
|
{currentQuestion.options?.map(
|
||||||
|
(opt, idx) =>
|
||||||
|
opt && (
|
||||||
|
<option key={idx} value={opt}>
|
||||||
|
{opt}
|
||||||
|
</option>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={currentQuestion.correctAnswer}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCurrentQuestion({ ...currentQuestion, correctAnswer: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Enter correct answer"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={addQuestion}
|
||||||
|
disabled={!currentQuestion.question || !currentQuestion.correctAnswer}
|
||||||
|
className="w-full bg-brand-500 hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add New Question
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setCurrentStep(1)}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setCurrentStep(3)}
|
||||||
|
disabled={!canProceedToStep3()}
|
||||||
|
className="bg-brand-500 hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: Review */}
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-grayScale-900">Practice Details</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-grayScale-600">Title:</span>
|
||||||
|
<span className="text-sm font-medium text-grayScale-900">{formData.title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-grayScale-600">Description:</span>
|
||||||
|
<span className="text-sm font-medium text-grayScale-900">
|
||||||
|
{formData.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-grayScale-600">Category:</span>
|
||||||
|
<span className="text-sm font-medium text-grayScale-900">{formData.category}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-grayScale-600">Difficulty:</span>
|
||||||
|
<span className="text-sm font-medium text-grayScale-900">
|
||||||
|
{formData.difficulty}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-grayScale-600">Duration:</span>
|
||||||
|
<span className="text-sm font-medium text-grayScale-900">
|
||||||
|
{formData.duration} minutes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-sm text-grayScale-600">Tags:</span>
|
||||||
|
<span className="text-sm font-medium text-grayScale-900">{formData.tags}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-grayScale-900">Questions</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{formData.questions.map((q, index) => (
|
||||||
|
<div key={q.id} className="rounded-lg border bg-grayScale-50 p-4">
|
||||||
|
<div className="mb-2 flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-grayScale-900">
|
||||||
|
{index + 1}. {q.question}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-grayScale-600">
|
||||||
|
Type: {q.type} | Points: {q.points}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{q.type === "multiple-choice" && q.options.length > 0 && (
|
||||||
|
<div className="mt-2 space-y-1">
|
||||||
|
{q.options.map((opt, optIdx) => (
|
||||||
|
<div
|
||||||
|
key={optIdx}
|
||||||
|
className={`rounded px-2 py-1 text-sm ${
|
||||||
|
opt === q.correctAnswer
|
||||||
|
? "bg-brand-100 text-brand-700 font-medium"
|
||||||
|
: "bg-white text-grayScale-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt}
|
||||||
|
{opt === q.correctAnswer && (
|
||||||
|
<Check className="ml-2 inline h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mt-2 text-sm text-grayScale-600">
|
||||||
|
Correct Answer: <span className="font-medium">{q.correctAnswer}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setCurrentStep(2)}>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
Create Practice
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
172
src/pages/content-management/AddVideoPage.tsx
Normal file
172
src/pages/content-management/AddVideoPage.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { Save, ArrowLeft } from "lucide-react"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
import { Card } from "../../components/ui/card"
|
||||||
|
import { FileUpload } from "../../components/ui/file-upload"
|
||||||
|
import { Input } from "../../components/ui/input"
|
||||||
|
import { Textarea } from "../../components/ui/textarea"
|
||||||
|
import { Select } from "../../components/ui/select"
|
||||||
|
|
||||||
|
export function AddVideoPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [videoFile, setVideoFile] = useState<File | null>(null)
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
tags: "",
|
||||||
|
category: "",
|
||||||
|
visibility: "",
|
||||||
|
thumbnail: null as File | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
console.log("Video data:", { videoFile, ...formData })
|
||||||
|
// Handle form submission
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => navigate("/content/courses")}
|
||||||
|
className="h-8 w-8"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Add New Video</h1>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleSubmit} className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-grayScale-900">Video Upload</h2>
|
||||||
|
<FileUpload
|
||||||
|
accept="video/*"
|
||||||
|
onFileSelect={setVideoFile}
|
||||||
|
label="Drag & Drop Video Here"
|
||||||
|
description="or click to browse files"
|
||||||
|
className="min-h-[200px]"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{videoFile && (
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-grayScale-900">Video Preview</h2>
|
||||||
|
<div className="aspect-video w-full overflow-hidden rounded-lg bg-grayScale-900">
|
||||||
|
<video
|
||||||
|
src={URL.createObjectURL(videoFile)}
|
||||||
|
controls
|
||||||
|
className="h-full w-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-grayScale-900">Video Details</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Video Title
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||||
|
placeholder="Enter video title"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
placeholder="Enter video description"
|
||||||
|
rows={4}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">Tags</label>
|
||||||
|
<Input
|
||||||
|
value={formData.tags}
|
||||||
|
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
|
||||||
|
placeholder="React, Hooks, JavaScript"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Category
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={formData.category}
|
||||||
|
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select category</option>
|
||||||
|
<option value="development">Development</option>
|
||||||
|
<option value="design">Design</option>
|
||||||
|
<option value="business">Business</option>
|
||||||
|
<option value="language">Language</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Visibility
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={formData.visibility}
|
||||||
|
onChange={(e) => setFormData({ ...formData, visibility: e.target.value })}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select visibility</option>
|
||||||
|
<option value="public">Public</option>
|
||||||
|
<option value="private">Private</option>
|
||||||
|
<option value="unlisted">Unlisted</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Thumbnail
|
||||||
|
</label>
|
||||||
|
<FileUpload
|
||||||
|
accept="image/*"
|
||||||
|
onFileSelect={(file) => setFormData({ ...formData, thumbnail: file })}
|
||||||
|
label="Upload Thumbnail"
|
||||||
|
description="or click to browse"
|
||||||
|
className="min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button type="submit" className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
Save Video
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -5,6 +5,7 @@ const tabs = [
|
||||||
{ label: "Overview", to: "/content" },
|
{ label: "Overview", to: "/content" },
|
||||||
{ label: "Courses", to: "/content/courses" },
|
{ label: "Courses", to: "/content/courses" },
|
||||||
{ label: "Speaking", to: "/content/speaking" },
|
{ label: "Speaking", to: "/content/speaking" },
|
||||||
|
{ label: "Practice", to: "/content/practices" },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function ContentManagementLayout() {
|
export function ContentManagementLayout() {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,58 @@
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
import { Link } from "react-router-dom"
|
||||||
|
import { BookOpen, Mic, Briefcase } from "lucide-react"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/ui/card"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
|
||||||
export function ContentOverviewPage() {
|
export function ContentOverviewPage() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="space-y-6">
|
||||||
<Card className="shadow-none">
|
<h1 className="text-xl font-semibold text-grayScale-900">Content Management</h1>
|
||||||
|
<div className="grid gap-6 md:grid-cols-3">
|
||||||
|
<Card className="shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Courses</CardTitle>
|
<div className="mb-4 grid h-12 w-12 place-items-center rounded-lg bg-brand-100 text-brand-600">
|
||||||
|
<BookOpen className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-lg">Courses</CardTitle>
|
||||||
|
<CardDescription>Manage course videos and educational content</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-sm text-muted-foreground">Manage courses content (scaffold).</CardContent>
|
<CardContent>
|
||||||
|
<Link to="/content/courses">
|
||||||
|
<Button className="w-full bg-brand-500 hover:bg-brand-600">Manage Courses</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="shadow-none">
|
|
||||||
|
<Card className="shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Speaking</CardTitle>
|
<div className="mb-4 grid h-12 w-12 place-items-center rounded-lg bg-brand-100 text-brand-600">
|
||||||
|
<Mic className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-lg">Speaking</CardTitle>
|
||||||
|
<CardDescription>Manage speaking practice sessions and exercises</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-sm text-muted-foreground">Manage speaking content (scaffold).</CardContent>
|
<CardContent>
|
||||||
|
<Link to="/content/speaking">
|
||||||
|
<Button className="w-full bg-brand-500 hover:bg-brand-600">Manage Speaking</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card className="shadow-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="mb-4 grid h-12 w-12 place-items-center rounded-lg bg-brand-100 text-brand-600">
|
||||||
|
<Briefcase className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-lg">Practice</CardTitle>
|
||||||
|
<CardDescription>Manage practice details, members, and leadership</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Link to="/content/practices">
|
||||||
|
<Button className="w-full bg-brand-500 hover:bg-brand-600">Manage Practice</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,29 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { Plus } from "lucide-react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
|
||||||
export function CoursesPage() {
|
export function CoursesPage() {
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Courses</h1>
|
||||||
|
<Link to="/content/courses/add-video">
|
||||||
|
<Button className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add New Video
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<Card className="shadow-none">
|
<Card className="shadow-none">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Courses</CardTitle>
|
<CardTitle>Course Management</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-sm text-muted-foreground">Courses module placeholder.</CardContent>
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Manage your course videos and content here.
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
286
src/pages/content-management/PracticeDetailsPage.tsx
Normal file
286
src/pages/content-management/PracticeDetailsPage.tsx
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Plus, Edit, Trash2 } 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"
|
||||||
|
import { Select } from "../../components/ui/select"
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "../../components/ui/dialog"
|
||||||
|
|
||||||
|
const mockLeaders = [
|
||||||
|
{ id: "1", name: "John Doe", role: "CEO" },
|
||||||
|
{ id: "2", name: "Jane Smith", role: "COO" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockMembers = [
|
||||||
|
{ id: "1", name: "John Doe", role: "Member" },
|
||||||
|
{ id: "2", name: "Jane Smith", role: "Member" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function PracticeDetailsPage() {
|
||||||
|
const [isMemberModalOpen, setIsMemberModalOpen] = useState(false)
|
||||||
|
const [isLeaderModalOpen, setIsLeaderModalOpen] = useState(false)
|
||||||
|
const [memberName, setMemberName] = useState("")
|
||||||
|
const [memberRole, setMemberRole] = useState("")
|
||||||
|
const [leaderName, setLeaderName] = useState("")
|
||||||
|
const [leaderRole, setLeaderRole] = useState("")
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
type: "",
|
||||||
|
street: "",
|
||||||
|
city: "",
|
||||||
|
state: "",
|
||||||
|
zipCode: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleAddMember = () => {
|
||||||
|
console.log("Add member:", { memberName, memberRole })
|
||||||
|
setIsMemberModalOpen(false)
|
||||||
|
setMemberName("")
|
||||||
|
setMemberRole("")
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddLeader = () => {
|
||||||
|
console.log("Add leader:", { leaderName, leaderRole })
|
||||||
|
setIsLeaderModalOpen(false)
|
||||||
|
setLeaderName("")
|
||||||
|
setLeaderRole("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Practice Management</h1>
|
||||||
|
|
||||||
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
|
{/* Practice Leadership */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold text-grayScale-900">Practice Leadership</h2>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsLeaderModalOpen(true)}
|
||||||
|
className="bg-brand-500 hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add New Leader
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{mockLeaders.map((leader) => (
|
||||||
|
<div
|
||||||
|
key={leader.id}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-grayScale-900">{leader.name}</p>
|
||||||
|
<p className="text-sm text-grayScale-600">{leader.role}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Practice Details */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-grayScale-900">Practice Details</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Practice Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="Enter practice name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Practice Description
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
placeholder="Enter practice description"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Practice Type
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={formData.type}
|
||||||
|
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">Select practice type</option>
|
||||||
|
<option value="online">Online</option>
|
||||||
|
<option value="offline">Offline</option>
|
||||||
|
<option value="hybrid">Hybrid</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Practice Address
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
value={formData.street}
|
||||||
|
onChange={(e) => setFormData({ ...formData, street: e.target.value })}
|
||||||
|
placeholder="Street"
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Input
|
||||||
|
value={formData.city}
|
||||||
|
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
|
||||||
|
placeholder="City"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={formData.state}
|
||||||
|
onChange={(e) => setFormData({ ...formData, state: e.target.value })}
|
||||||
|
placeholder="State"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={formData.zipCode}
|
||||||
|
onChange={(e) => setFormData({ ...formData, zipCode: e.target.value })}
|
||||||
|
placeholder="Zip Code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className="w-full bg-brand-500 hover:bg-brand-600">Save Changes</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Practice Members */}
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold text-grayScale-900">Practice Members</h2>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsMemberModalOpen(true)}
|
||||||
|
className="bg-brand-500 hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add New Member
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{mockMembers.map((member) => (
|
||||||
|
<div
|
||||||
|
key={member.id}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-grayScale-900">{member.name}</p>
|
||||||
|
<p className="text-sm text-grayScale-600">{member.role}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Add Member Modal */}
|
||||||
|
<Dialog open={isMemberModalOpen} onOpenChange={setIsMemberModalOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add New Member</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Member Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={memberName}
|
||||||
|
onChange={(e) => setMemberName(e.target.value)}
|
||||||
|
placeholder="Enter member name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Member Role
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={memberRole}
|
||||||
|
onChange={(e) => setMemberRole(e.target.value)}
|
||||||
|
placeholder="Enter member role"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsMemberModalOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleAddMember} className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Add Leader Modal */}
|
||||||
|
<Dialog open={isLeaderModalOpen} onOpenChange={setIsLeaderModalOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add New Leader</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Leader Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={leaderName}
|
||||||
|
onChange={(e) => setLeaderName(e.target.value)}
|
||||||
|
placeholder="Enter leader name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Leader Role
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={leaderRole}
|
||||||
|
onChange={(e) => setLeaderRole(e.target.value)}
|
||||||
|
placeholder="Enter leader role"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsLeaderModalOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleAddLeader} className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
Add Leader
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
100
src/pages/content-management/PracticeMembersPage.tsx
Normal file
100
src/pages/content-management/PracticeMembersPage.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Plus } from "lucide-react"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
import { Card } from "../../components/ui/card"
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar"
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "../../components/ui/dialog"
|
||||||
|
import { Input } from "../../components/ui/input"
|
||||||
|
|
||||||
|
const mockMembers = [
|
||||||
|
{ id: "1", name: "John", avatar: "" },
|
||||||
|
{ id: "2", name: "Jane", avatar: "" },
|
||||||
|
{ id: "3", name: "Mike", avatar: "" },
|
||||||
|
{ id: "4", name: "Sarah", avatar: "" },
|
||||||
|
{ id: "5", name: "David", avatar: "" },
|
||||||
|
{ id: "6", name: "Emily", avatar: "" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function PracticeMembersPage() {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [memberName, setMemberName] = useState("")
|
||||||
|
const [memberRole, setMemberRole] = useState("")
|
||||||
|
|
||||||
|
const handleAddMember = () => {
|
||||||
|
console.log("Add member:", { memberName, memberRole })
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setMemberName("")
|
||||||
|
setMemberRole("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Practice Management</h1>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold text-grayScale-900">Current Members</h2>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
className="bg-brand-500 hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add Members
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-4 md:grid-cols-6">
|
||||||
|
{mockMembers.map((member) => (
|
||||||
|
<div key={member.id} className="flex flex-col items-center">
|
||||||
|
<Avatar className="h-16 w-16 border-2 border-grayScale-200">
|
||||||
|
<AvatarImage src={member.avatar} />
|
||||||
|
<AvatarFallback className="bg-brand-100 text-brand-600">
|
||||||
|
{member.name[0]}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<span className="mt-2 text-sm font-medium text-grayScale-700">{member.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add New Member</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Member Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={memberName}
|
||||||
|
onChange={(e) => setMemberName(e.target.value)}
|
||||||
|
placeholder="Enter member name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Member Role
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={memberRole}
|
||||||
|
onChange={(e) => setMemberRole(e.target.value)}
|
||||||
|
placeholder="Enter member role"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsModalOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleAddMember} className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,13 +1,29 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { Plus } from "lucide-react"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
|
||||||
export function SpeakingPage() {
|
export function SpeakingPage() {
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Speaking</h1>
|
||||||
|
<Link to="/content/speaking/add-practice">
|
||||||
|
<Button className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add New Practice
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<Card className="shadow-none">
|
<Card className="shadow-none">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Speaking</CardTitle>
|
<CardTitle>Speaking Practice Management</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-sm text-muted-foreground">Speaking module placeholder.</CardContent>
|
<CardContent className="text-sm text-muted-foreground">
|
||||||
|
Manage speaking practice sessions and exercises here.
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
104
src/pages/role-management/AddRolePage.tsx
Normal file
104
src/pages/role-management/AddRolePage.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { useState } from "react"
|
||||||
|
import { ArrowLeft } from "lucide-react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
import { Card } from "../../components/ui/card"
|
||||||
|
import { Input } from "../../components/ui/input"
|
||||||
|
import { Textarea } from "../../components/ui/textarea"
|
||||||
|
|
||||||
|
const permissions = [
|
||||||
|
"View Dashboard",
|
||||||
|
"Manage Users",
|
||||||
|
"Manage Roles",
|
||||||
|
"Manage Practices",
|
||||||
|
"View Reports",
|
||||||
|
"Manage Content",
|
||||||
|
"Manage Settings",
|
||||||
|
]
|
||||||
|
|
||||||
|
export function AddRolePage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [roleName, setRoleName] = useState("")
|
||||||
|
const [roleDescription, setRoleDescription] = useState("")
|
||||||
|
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([])
|
||||||
|
|
||||||
|
const togglePermission = (permission: string) => {
|
||||||
|
setSelectedPermissions((prev) =>
|
||||||
|
prev.includes(permission)
|
||||||
|
? prev.filter((p) => p !== permission)
|
||||||
|
: [...prev, permission],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
console.log("Add role:", { roleName, roleDescription, selectedPermissions })
|
||||||
|
navigate("/roles")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => navigate("/roles")} className="h-8 w-8">
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Add New Role</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">Role Name</label>
|
||||||
|
<Input
|
||||||
|
value={roleName}
|
||||||
|
onChange={(e) => setRoleName(e.target.value)}
|
||||||
|
placeholder="Enter role name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Role Description
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={roleDescription}
|
||||||
|
onChange={(e) => setRoleDescription(e.target.value)}
|
||||||
|
placeholder="Enter role description"
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-4 block text-sm font-medium text-grayScale-700">
|
||||||
|
Permissions
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{permissions.map((permission) => (
|
||||||
|
<label
|
||||||
|
key={permission}
|
||||||
|
className="flex items-center gap-2 rounded-lg border p-3 hover:bg-grayScale-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedPermissions.includes(permission)}
|
||||||
|
onChange={() => togglePermission(permission)}
|
||||||
|
className="h-4 w-4 rounded border-grayScale-300 text-brand-500 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-grayScale-700">{permission}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={handleSubmit} className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
Add Role
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
11
src/pages/role-management/RoleManagementLayout.tsx
Normal file
11
src/pages/role-management/RoleManagementLayout.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Outlet } from "react-router-dom"
|
||||||
|
|
||||||
|
export function RoleManagementLayout() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-6xl">
|
||||||
|
<div className="mb-4 text-sm font-semibold text-grayScale-500">Role Management</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
44
src/pages/role-management/RolesListPage.tsx
Normal file
44
src/pages/role-management/RolesListPage.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { Plus, Edit } from "lucide-react"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
import { Card, CardContent } from "../../components/ui/card"
|
||||||
|
|
||||||
|
const mockRoles = [
|
||||||
|
{ id: "1", name: "Admin", userCount: 10 },
|
||||||
|
{ id: "2", name: "User", userCount: 5 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function RolesListPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Role Management</h1>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate("/roles/add")}
|
||||||
|
className="bg-brand-500 hover:bg-brand-600"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add New Role
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{mockRoles.map((role) => (
|
||||||
|
<Card key={role.id} className="overflow-hidden shadow-sm">
|
||||||
|
<div className="h-2 bg-gradient-to-r from-brand-500 to-brand-600" />
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h3 className="mb-2 text-lg font-semibold text-grayScale-900">{role.name}</h3>
|
||||||
|
<p className="mb-4 text-sm text-grayScale-600">{role.userCount} Users</p>
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
|
Edit Role
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
75
src/pages/user-management/RegisterUserPage.tsx
Normal file
75
src/pages/user-management/RegisterUserPage.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { ArrowLeft } from "lucide-react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
import { Card } from "../../components/ui/card"
|
||||||
|
import { Input } from "../../components/ui/input"
|
||||||
|
import { Textarea } from "../../components/ui/textarea"
|
||||||
|
import { Select } from "../../components/ui/select"
|
||||||
|
|
||||||
|
export function RegisterUserPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => navigate("/users")} className="h-8 w-8">
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">Register New User</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<form className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
First Name
|
||||||
|
</label>
|
||||||
|
<Input placeholder="Enter first name" required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Last Name
|
||||||
|
</label>
|
||||||
|
<Input placeholder="Enter last name" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">Email</label>
|
||||||
|
<Input type="email" placeholder="Enter email address" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">Phone</label>
|
||||||
|
<Input type="tel" placeholder="Enter phone number" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">Role</label>
|
||||||
|
<Select required>
|
||||||
|
<option value="">Select role</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="user">User</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">Notes</label>
|
||||||
|
<Textarea placeholder="Enter any additional notes" rows={3} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => navigate("/users")}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
Register User
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
94
src/pages/user-management/UserGroupsPage.tsx
Normal file
94
src/pages/user-management/UserGroupsPage.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Plus, Edit } from "lucide-react"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
import { Card, CardContent } from "../../components/ui/card"
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "../../components/ui/dialog"
|
||||||
|
import { Input } from "../../components/ui/input"
|
||||||
|
import { Textarea } from "../../components/ui/textarea"
|
||||||
|
|
||||||
|
const mockGroups = [
|
||||||
|
{ id: "1", name: "Big 10", userCount: 10 },
|
||||||
|
{ id: "2", name: "Small 5", userCount: 5 },
|
||||||
|
{ id: "3", name: "Team 8", userCount: 8 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function UserGroupsPage() {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
const [groupName, setGroupName] = useState("")
|
||||||
|
const [groupDescription, setGroupDescription] = useState("")
|
||||||
|
|
||||||
|
const handleAddGroup = () => {
|
||||||
|
console.log("Add group:", { groupName, groupDescription })
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setGroupName("")
|
||||||
|
setGroupDescription("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">User Groups</h1>
|
||||||
|
<Button onClick={() => setIsModalOpen(true)} className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add New Group
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
{mockGroups.map((group) => (
|
||||||
|
<Card key={group.id} className="overflow-hidden shadow-sm">
|
||||||
|
<div className="h-2 bg-gradient-to-r from-brand-500 to-brand-600" />
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h3 className="mb-2 text-lg font-semibold text-grayScale-900">{group.name}</h3>
|
||||||
|
<p className="mb-4 text-sm text-grayScale-600">{group.userCount} Users</p>
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
|
Edit Role
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add New Group</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Group Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={groupName}
|
||||||
|
onChange={(e) => setGroupName(e.target.value)}
|
||||||
|
placeholder="Enter group name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-grayScale-700">
|
||||||
|
Group Description
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
value={groupDescription}
|
||||||
|
onChange={(e) => setGroupDescription(e.target.value)}
|
||||||
|
placeholder="Enter group description"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsModalOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleAddGroup} className="bg-brand-500 hover:bg-brand-600">
|
||||||
|
Add Group
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
58
src/pages/user-management/UserManagementDashboard.tsx
Normal file
58
src/pages/user-management/UserManagementDashboard.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { UserPlus, Users } from "lucide-react"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/ui/card"
|
||||||
|
import { Button } from "../../components/ui/button"
|
||||||
|
|
||||||
|
export function UserManagementDashboard() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-xl font-semibold text-grayScale-900">User Management</h1>
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<Card className="shadow-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="mb-4 grid h-12 w-12 place-items-center rounded-lg bg-brand-100 text-brand-600">
|
||||||
|
<UserPlus className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-lg">User Register</CardTitle>
|
||||||
|
<CardDescription>Register new users to the system</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Link to="/users/register">
|
||||||
|
<Button className="w-full bg-brand-500 hover:bg-brand-600">Register User</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="shadow-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="mb-4 grid h-12 w-12 place-items-center rounded-lg bg-brand-100 text-brand-600">
|
||||||
|
<Users className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-lg">User Groups</CardTitle>
|
||||||
|
<CardDescription>Manage user groups and permissions</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Link to="/users/groups">
|
||||||
|
<Button className="w-full bg-brand-500 hover:bg-brand-600">View Groups</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="shadow-sm md:col-span-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">User List</CardTitle>
|
||||||
|
<CardDescription>View and manage all users in the system</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Link to="/users/list">
|
||||||
|
<Button variant="outline" className="w-full">
|
||||||
|
View All Users
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user