This commit is contained in:
kirukib 2025-12-19 18:31:48 +03:00
parent afccaf9892
commit 79e2ef6ce1
25 changed files with 2030 additions and 35 deletions

93
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 />} />

View File

@ -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"

View File

@ -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>
)

View 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,
}

View 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"

View 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"

View 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>
)
}

View 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"

View File

@ -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>

View 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>
)
}

View 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>
)
}

View File

@ -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() {

View File

@ -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">
<CardHeader>
<CardTitle>Courses</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">Manage courses content (scaffold).</CardContent>
</Card>
<Card className="shadow-none">
<CardHeader>
<CardTitle>Speaking</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">Manage speaking content (scaffold).</CardContent>
</Card>
<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>
<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>
<Link to="/content/courses">
<Button className="w-full bg-brand-500 hover:bg-brand-600">Manage Courses</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">
<Mic className="h-6 w-6" />
</div>
<CardTitle className="text-lg">Speaking</CardTitle>
<CardDescription>Manage speaking practice sessions and exercises</CardDescription>
</CardHeader>
<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>
)
}

View File

@ -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 (
<Card className="shadow-none">
<CardHeader>
<CardTitle>Courses</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">Courses module placeholder.</CardContent>
</Card>
<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>Course Management</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Manage your course videos and content here.
</CardContent>
</Card>
</div>
)
}

View 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>
)
}

View 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>
)
}

View File

@ -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 (
<Card className="shadow-none">
<CardHeader>
<CardTitle>Speaking</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">Speaking module placeholder.</CardContent>
</Card>
<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 Practice Management</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
Manage speaking practice sessions and exercises here.
</CardContent>
</Card>
</div>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}