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": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@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-separator": "^1.1.8",
|
||||
"@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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@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-separator": "^1.1.8",
|
||||
"@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 { CoursesPage } from "../pages/content-management/CoursesPage"
|
||||
import { SpeakingPage } from "../pages/content-management/SpeakingPage"
|
||||
import { AddVideoPage } from "../pages/content-management/AddVideoPage"
|
||||
import { AddPracticePage } from "../pages/content-management/AddPracticePage"
|
||||
import { NotFoundPage } from "../pages/NotFoundPage"
|
||||
import { NotificationsPage } from "../pages/notifications/NotificationsPage"
|
||||
import { PlaceholderPage } from "../pages/PlaceholderPage"
|
||||
import { UserDetailPage } from "../pages/user-management/UserDetailPage"
|
||||
import { UserManagementLayout } from "../pages/user-management/UserManagementLayout"
|
||||
import { UsersListPage } from "../pages/user-management/UsersListPage"
|
||||
import { UserManagementDashboard } from "../pages/user-management/UserManagementDashboard"
|
||||
import { UserGroupsPage } from "../pages/user-management/UserGroupsPage"
|
||||
import { 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"
|
||||
|
||||
export function AppRoutes() {
|
||||
|
|
@ -21,14 +31,26 @@ export function AppRoutes() {
|
|||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="/dashboard" element={<DashboardPage />} />
|
||||
<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>
|
||||
|
||||
<Route path="/roles" element={<RoleManagementLayout />}>
|
||||
<Route index element={<RolesListPage />} />
|
||||
<Route path="add" element={<AddRolePage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="/content" element={<ContentManagementLayout />}>
|
||||
<Route index element={<ContentOverviewPage />} />
|
||||
<Route path="courses" element={<CoursesPage />} />
|
||||
<Route path="courses/add-video" element={<AddVideoPage />} />
|
||||
<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 path="/notifications" element={<NotificationsPage />} />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
ClipboardList,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Shield,
|
||||
UserCircle2,
|
||||
Users,
|
||||
Users2,
|
||||
|
|
@ -23,6 +24,7 @@ type NavItem = {
|
|||
const navItems: NavItem[] = [
|
||||
{ label: "Dashboard", to: "/dashboard", icon: LayoutDashboard },
|
||||
{ label: "User Management", to: "/users", icon: Users },
|
||||
{ label: "Role Management", to: "/roles", icon: Shield },
|
||||
{ label: "Content Management", to: "/content", icon: BookOpen },
|
||||
{ label: "Notifications", to: "/notifications", icon: Bell },
|
||||
{ label: "User Log", to: "/user-log", icon: ClipboardList },
|
||||
|
|
@ -33,12 +35,12 @@ const navItems: NavItem[] = [
|
|||
|
||||
export function Sidebar() {
|
||||
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">
|
||||
<BrandLogo />
|
||||
</div>
|
||||
|
||||
<nav className="mt-6 space-y-1">
|
||||
<nav className="mt-6 flex-1 space-y-1 overflow-y-auto">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
|
|
@ -75,7 +77,7 @@ export function Sidebar() {
|
|||
})}
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto px-2 pt-6">
|
||||
<div className="px-2 pt-6">
|
||||
<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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<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"
|
||||
>
|
||||
<Bell className="h-5 w-5" />
|
||||
|
|
@ -14,7 +14,7 @@ export function Topbar() {
|
|||
|
||||
<Avatar className="h-10 w-10 ring-2 ring-brand-100">
|
||||
<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>
|
||||
</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() {
|
||||
return (
|
||||
<div className="flex h-full bg-grayScale-100">
|
||||
<div className="flex min-h-screen bg-grayScale-100">
|
||||
<Sidebar />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="ml-[264px] flex min-w-0 flex-1 flex-col">
|
||||
<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 />
|
||||
</main>
|
||||
</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: "Courses", to: "/content/courses" },
|
||||
{ label: "Speaking", to: "/content/speaking" },
|
||||
{ label: "Practice", to: "/content/practices" },
|
||||
]
|
||||
|
||||
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() {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card className="shadow-none">
|
||||
<div className="space-y-6">
|
||||
<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>
|
||||
<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>
|
||||
<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 className="shadow-none">
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<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>
|
||||
<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 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { Button } from "../../components/ui/button"
|
||||
|
||||
export function CoursesPage() {
|
||||
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">
|
||||
<CardHeader>
|
||||
<CardTitle>Courses</CardTitle>
|
||||
<CardTitle>Course Management</CardTitle>
|
||||
</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>
|
||||
</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 { Button } from "../../components/ui/button"
|
||||
|
||||
export function SpeakingPage() {
|
||||
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">
|
||||
<CardHeader>
|
||||
<CardTitle>Speaking</CardTitle>
|
||||
<CardTitle>Speaking Practice Management</CardTitle>
|
||||
</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>
|
||||
</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