questions-page

This commit is contained in:
Kirubel-Kibru-Yaltopia 2025-12-29 16:22:40 +03:00
parent 79e2ef6ce1
commit ca72ca0a70
9 changed files with 925 additions and 20 deletions

19
package-lock.json generated
View File

@ -86,7 +86,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -2752,7 +2751,6 @@
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@ -2763,7 +2761,6 @@
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -2774,7 +2771,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@ -2830,7 +2826,6 @@
"integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/scope-manager": "8.50.0",
"@typescript-eslint/types": "8.50.0", "@typescript-eslint/types": "8.50.0",
@ -3082,7 +3077,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3304,7 +3298,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -3788,7 +3781,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -4763,7 +4755,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -4811,7 +4802,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@ -4994,7 +4984,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -5004,7 +4993,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -5024,7 +5012,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@ -5230,8 +5217,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@ -5625,7 +5611,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -5793,7 +5778,6 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -5931,7 +5915,6 @@
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -22,11 +22,19 @@ import { RolesListPage } from "../pages/role-management/RolesListPage"
import { AddRolePage } from "../pages/role-management/AddRolePage" import { AddRolePage } from "../pages/role-management/AddRolePage"
import { PracticeDetailsPage } from "../pages/content-management/PracticeDetailsPage" import { PracticeDetailsPage } from "../pages/content-management/PracticeDetailsPage"
import { PracticeMembersPage } from "../pages/content-management/PracticeMembersPage" import { PracticeMembersPage } from "../pages/content-management/PracticeMembersPage"
import { QuestionsPage } from "../pages/content-management/QuestionsPage"
import { AddQuestionPage } from "../pages/content-management/AddQuestionPage"
import { UserLogPage } from "../pages/user-log/UserLogPage" import { UserLogPage } from "../pages/user-log/UserLogPage"
import { LoginPage } from "../pages/auth/LoginPage"
import { ForgotPasswordPage } from "../pages/auth/ForgotPasswordPage"
import { VerificationPage } from "../pages/auth/VerificationPage"
export function AppRoutes() { export function AppRoutes() {
return ( return (
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/verification" element={<VerificationPage />} />
<Route element={<AppLayout />}> <Route element={<AppLayout />}>
<Route path="/" element={<Navigate to="/dashboard" replace />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<DashboardPage />} /> <Route path="/dashboard" element={<DashboardPage />} />
@ -51,6 +59,9 @@ export function AppRoutes() {
<Route path="speaking/add-practice" element={<AddPracticePage />} /> <Route path="speaking/add-practice" element={<AddPracticePage />} />
<Route path="practices" element={<PracticeDetailsPage />} /> <Route path="practices" element={<PracticeDetailsPage />} />
<Route path="practices/members" element={<PracticeMembersPage />} /> <Route path="practices/members" element={<PracticeMembersPage />} />
<Route path="questions" element={<QuestionsPage />} />
<Route path="questions/add" element={<AddQuestionPage />} />
<Route path="questions/edit/:id" element={<AddQuestionPage />} />
</Route> </Route>
<Route path="/notifications" element={<NotificationsPage />} /> <Route path="/notifications" element={<NotificationsPage />} />

View File

@ -0,0 +1,64 @@
import { useState } from "react"
import { Link } from "react-router-dom"
import { BrandLogo } from "../../components/brand/BrandLogo"
import { Button } from "../../components/ui/button"
import { Input } from "../../components/ui/input"
export function ForgotPasswordPage() {
const [email, setEmail] = useState("")
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Handle forgot password logic here
console.log("Forgot password:", { email })
}
return (
<div className="flex min-h-screen items-center justify-center bg-grayScale-100 px-4 py-12">
<div className="w-full max-w-md">
<div className="rounded-2xl bg-white p-8 shadow-soft">
<div className="mb-8">
<BrandLogo />
</div>
<div className="mb-8">
<h1 className="mb-2 text-2xl font-semibold text-grayScale-600">Forgot Password</h1>
<p className="text-sm text-grayScale-400">
Enter your email address and we'll send you a reset link.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="mb-2 block text-sm font-medium text-grayScale-600">
Email Address
</label>
<Input
id="email"
type="email"
placeholder="admin@yimaruacademy.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<Button type="submit" className="w-full">
Send Reset Link
</Button>
</form>
<div className="mt-6 text-center">
<Link
to="/login"
className="text-sm font-medium text-brand-500 hover:text-brand-600"
>
Back to Login
</Link>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,89 @@
import { useState } from "react"
import { Link } from "react-router-dom"
import { Eye, EyeOff } from "lucide-react"
import { BrandLogo } from "../../components/brand/BrandLogo"
import { Button } from "../../components/ui/button"
import { Input } from "../../components/ui/input"
export function LoginPage() {
const [showPassword, setShowPassword] = useState(false)
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Handle login logic here
console.log("Login:", { email, password })
}
return (
<div className="flex min-h-screen items-center justify-center bg-grayScale-100 px-4 py-12">
<div className="w-full max-w-md">
<div className="rounded-2xl bg-white p-8 shadow-soft">
<div className="mb-8 flex justify-center">
<BrandLogo />
</div>
<div className="mb-8 text-center">
<h1 className="mb-2 text-2xl font-semibold text-grayScale-600">Admin Login</h1>
<p className="text-sm text-grayScale-400">Please enter your details to continue</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="mb-2 block text-sm font-medium text-grayScale-600">
Email Address
</label>
<Input
id="email"
type="email"
placeholder="admin@yimaruacademy.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password" className="mb-2 block text-sm font-medium text-grayScale-600">
Password
</label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-grayScale-400 hover:text-grayScale-600"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
</div>
<div className="flex justify-end">
<Link
to="/forgot-password"
className="text-sm font-medium text-brand-500 hover:text-brand-600"
>
Forgot Password?
</Link>
</div>
<Button type="submit" className="w-full">
Login
</Button>
</form>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,151 @@
import { useState, useEffect, useRef } from "react"
import { BrandLogo } from "../../components/brand/BrandLogo"
import { Button } from "../../components/ui/button"
import { Input } from "../../components/ui/input"
export function VerificationPage() {
const [codes, setCodes] = useState<string[]>(["", "", "", "", ""])
const [activeIndex, setActiveIndex] = useState(0)
const [timeLeft, setTimeLeft] = useState(30)
const inputRefs = useRef<(HTMLInputElement | null)[]>([])
useEffect(() => {
if (timeLeft > 0) {
const timer = setTimeout(() => setTimeLeft(timeLeft - 1), 1000)
return () => clearTimeout(timer)
}
}, [timeLeft])
const handleCodeChange = (index: number, value: string) => {
if (value.length > 1) {
// Handle paste
const pastedCodes = value.slice(0, 5).split("")
const newCodes = [...codes]
pastedCodes.forEach((code, i) => {
if (index + i < 5) {
newCodes[index + i] = code
}
})
setCodes(newCodes)
const nextIndex = Math.min(index + pastedCodes.length, 4)
setActiveIndex(nextIndex)
inputRefs.current[nextIndex]?.focus()
return
}
if (!/^\d*$/.test(value)) return // Only allow digits
const newCodes = [...codes]
newCodes[index] = value
setCodes(newCodes)
if (value && index < 4) {
setActiveIndex(index + 1)
inputRefs.current[index + 1]?.focus()
} else if (!value && index > 0) {
setActiveIndex(index - 1)
inputRefs.current[index - 1]?.focus()
}
}
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Backspace" && !codes[index] && index > 0) {
setActiveIndex(index - 1)
inputRefs.current[index - 1]?.focus()
}
}
const handleVerify = (e: React.FormEvent) => {
e.preventDefault()
const code = codes.join("")
if (code.length === 5) {
// Handle verification logic here
console.log("Verification code:", code)
}
}
const handleResend = () => {
if (timeLeft === 0) {
setTimeLeft(30)
setCodes(["", "", "", "", ""])
setActiveIndex(0)
inputRefs.current[0]?.focus()
// Handle resend logic here
console.log("Resending code...")
}
}
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`
}
return (
<div className="flex min-h-screen items-center justify-center bg-grayScale-100 px-4 py-12">
<div className="w-full max-w-md">
<div className="rounded-2xl bg-white p-8 shadow-soft">
<div className="mb-8">
<BrandLogo />
</div>
<div className="mb-8 text-center">
<h1 className="mb-2 text-2xl font-semibold text-grayScale-600">Verification Code</h1>
<p className="text-sm text-grayScale-400">
We have sent a verification code sent to your email address ******230@gmail.com
</p>
</div>
<form onSubmit={handleVerify} className="space-y-6">
<div className="flex justify-center gap-3">
{codes.map((code, index) => (
<Input
key={index}
ref={(el) => {
inputRefs.current[index] = el
}}
type="text"
inputMode="numeric"
maxLength={1}
value={code}
onChange={(e) => handleCodeChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onFocus={() => setActiveIndex(index)}
className={`h-14 w-14 text-center text-xl font-semibold ${
activeIndex === index
? "border-2 border-brand-500 ring-2 ring-brand-500/20"
: ""
}`}
/>
))}
</div>
<div className="text-center text-sm text-grayScale-400">
Resend code in {formatTime(timeLeft)}
</div>
<Button type="submit" className="w-full" disabled={codes.join("").length !== 5}>
Verify
</Button>
<div className="text-center">
<button
type="button"
onClick={handleResend}
disabled={timeLeft > 0}
className={`text-sm font-medium ${
timeLeft > 0
? "text-grayScale-400 cursor-not-allowed"
: "text-brand-500 hover:text-brand-600"
}`}
>
Send Again
</button>
</div>
</form>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,316 @@
import { useState } from "react"
import { useNavigate, useParams } from "react-router-dom"
import { ArrowLeft, Plus, X } from "lucide-react"
import { Button } from "../../components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
import { Input } from "../../components/ui/input"
import { Textarea } from "../../components/ui/textarea"
import { Select } from "../../components/ui/select"
type QuestionType = "multiple-choice" | "short-answer" | "true-false"
interface Question {
id: string
question: string
type: QuestionType
options: string[]
correctAnswer: string
points: number
category?: string
difficulty?: string
}
// Mock data for editing
const mockQuestion: Question = {
id: "1",
question: "",
type: "multiple-choice",
options: ["", "", "", ""],
correctAnswer: "",
points: 10,
category: "",
difficulty: "",
}
export function AddQuestionPage() {
const navigate = useNavigate()
const { id } = useParams<{ id?: string }>()
const isEditing = !!id
const [formData, setFormData] = useState<Question>(
isEditing
? mockQuestion // In a real app, fetch the question by id
: {
id: Date.now().toString(),
question: "",
type: "multiple-choice",
options: ["", "", "", ""],
correctAnswer: "",
points: 10,
category: "",
difficulty: "",
},
)
const handleTypeChange = (type: QuestionType) => {
setFormData((prev) => {
if (type === "true-false") {
return {
...prev,
type,
options: ["True", "False"],
correctAnswer: prev.correctAnswer === "True" || prev.correctAnswer === "False" ? prev.correctAnswer : "",
}
} else if (type === "short-answer") {
return {
...prev,
type,
options: [],
correctAnswer: "",
}
} else {
return {
...prev,
type,
options: prev.options.length > 0 ? prev.options : ["", "", "", ""],
}
}
})
}
const handleOptionChange = (index: number, value: string) => {
setFormData((prev) => {
const newOptions = [...prev.options]
newOptions[index] = value
return { ...prev, options: newOptions }
})
}
const addOption = () => {
setFormData((prev) => ({
...prev,
options: [...prev.options, ""],
}))
}
const removeOption = (index: number) => {
setFormData((prev) => ({
...prev,
options: prev.options.filter((_, i) => i !== index),
}))
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Validation
if (!formData.question.trim()) {
alert("Please enter a question")
return
}
if (formData.type === "multiple-choice" || formData.type === "true-false") {
if (!formData.correctAnswer) {
alert("Please select a correct answer")
return
}
if (formData.type === "multiple-choice") {
const hasEmptyOptions = formData.options.some((opt) => !opt.trim())
if (hasEmptyOptions) {
alert("Please fill in all options")
return
}
}
} else if (formData.type === "short-answer") {
if (!formData.correctAnswer.trim()) {
alert("Please enter a correct answer")
return
}
}
// In a real app, save the question here
console.log("Saving question:", formData)
alert(isEditing ? "Question updated successfully!" : "Question created successfully!")
navigate("/content/questions")
}
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => navigate("/content/questions")}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-xl font-semibold text-grayScale-900">
{isEditing ? "Edit Question" : "Add New Question"}
</h1>
</div>
<form onSubmit={handleSubmit}>
<Card className="shadow-none">
<CardHeader>
<CardTitle>Question Details</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Question Type */}
<div>
<label className="mb-2 block text-sm font-medium text-grayScale-600">
Question Type
</label>
<Select
value={formData.type}
onChange={(e) => handleTypeChange(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>
{/* Question Text */}
<div>
<label htmlFor="question" className="mb-2 block text-sm font-medium text-grayScale-600">
Question
</label>
<Textarea
id="question"
placeholder="Enter your question here..."
value={formData.question}
onChange={(e) => setFormData((prev) => ({ ...prev, question: e.target.value }))}
rows={3}
required
/>
</div>
{/* Options for Multiple Choice */}
{(formData.type === "multiple-choice" || formData.type === "true-false") && (
<div>
<label className="mb-2 block text-sm font-medium text-grayScale-600">
Options
</label>
<div className="space-y-2">
{formData.options.map((option, index) => (
<div key={index} className="flex items-center gap-2">
<Input
value={option}
onChange={(e) => handleOptionChange(index, e.target.value)}
placeholder={`Option ${index + 1}`}
disabled={formData.type === "true-false"}
required
/>
{formData.type === "multiple-choice" && formData.options.length > 2 && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => removeOption(index)}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
))}
{formData.type === "multiple-choice" && (
<Button type="button" variant="outline" onClick={addOption} className="w-full">
<Plus className="h-4 w-4" />
Add Option
</Button>
)}
</div>
</div>
)}
{/* Correct Answer */}
<div>
<label className="mb-2 block text-sm font-medium text-grayScale-600">
Correct Answer
</label>
{formData.type === "multiple-choice" || formData.type === "true-false" ? (
<Select
value={formData.correctAnswer}
onChange={(e) =>
setFormData((prev) => ({ ...prev, correctAnswer: e.target.value }))
}
required
>
<option value="">Select correct answer</option>
{formData.options.map((option, index) => (
<option key={index} value={option}>
{option}
</option>
))}
</Select>
) : (
<Textarea
placeholder="Enter the correct answer..."
value={formData.correctAnswer}
onChange={(e) =>
setFormData((prev) => ({ ...prev, correctAnswer: e.target.value }))
}
rows={2}
required
/>
)}
</div>
{/* Points */}
<div>
<label htmlFor="points" className="mb-2 block text-sm font-medium text-grayScale-600">
Points
</label>
<Input
id="points"
type="number"
min="1"
value={formData.points}
onChange={(e) =>
setFormData((prev) => ({ ...prev, points: parseInt(e.target.value) || 0 }))
}
required
/>
</div>
{/* Category */}
<div>
<label htmlFor="category" className="mb-2 block text-sm font-medium text-grayScale-600">
Category (Optional)
</label>
<Input
id="category"
placeholder="e.g., Programming, Geography"
value={formData.category || ""}
onChange={(e) => setFormData((prev) => ({ ...prev, category: e.target.value }))}
/>
</div>
{/* Difficulty */}
<div>
<label className="mb-2 block text-sm font-medium text-grayScale-600">
Difficulty (Optional)
</label>
<Select
value={formData.difficulty || ""}
onChange={(e) => setFormData((prev) => ({ ...prev, difficulty: e.target.value }))}
>
<option value="">Select difficulty</option>
<option value="Easy">Easy</option>
<option value="Medium">Medium</option>
<option value="Hard">Hard</option>
</Select>
</div>
{/* Actions */}
<div className="flex justify-end gap-2 pt-4">
<Button type="button" variant="outline" onClick={() => navigate("/content/questions")}>
Cancel
</Button>
<Button type="submit" className="bg-brand-500 hover:bg-brand-600">
{isEditing ? "Update Question" : "Create Question"}
</Button>
</div>
</CardContent>
</Card>
</form>
</div>
)
}

View File

@ -6,6 +6,7 @@ const tabs = [
{ label: "Courses", to: "/content/courses" }, { label: "Courses", to: "/content/courses" },
{ label: "Speaking", to: "/content/speaking" }, { label: "Speaking", to: "/content/speaking" },
{ label: "Practice", to: "/content/practices" }, { label: "Practice", to: "/content/practices" },
{ label: "Questions", to: "/content/questions" },
] ]
export function ContentManagementLayout() { export function ContentManagementLayout() {

View File

@ -1,5 +1,5 @@
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { BookOpen, Mic, Briefcase } from "lucide-react" import { BookOpen, Mic, Briefcase, HelpCircle } from "lucide-react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/ui/card"
import { Button } from "../../components/ui/button" import { Button } from "../../components/ui/button"
@ -7,7 +7,7 @@ export function ContentOverviewPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h1 className="text-xl font-semibold text-grayScale-900">Content Management</h1> <h1 className="text-xl font-semibold text-grayScale-900">Content Management</h1>
<div className="grid gap-6 md:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<Card className="shadow-sm"> <Card className="shadow-sm">
<CardHeader> <CardHeader>
<div className="mb-4 grid h-12 w-12 place-items-center rounded-lg bg-brand-100 text-brand-600"> <div className="mb-4 grid h-12 w-12 place-items-center rounded-lg bg-brand-100 text-brand-600">
@ -52,6 +52,21 @@ export function ContentOverviewPage() {
</Link> </Link>
</CardContent> </CardContent>
</Card> </Card>
<Card className="shadow-sm">
<CardHeader>
<div className="mb-4 grid h-12 w-12 place-items-center rounded-lg bg-brand-100 text-brand-600">
<HelpCircle className="h-6 w-6" />
</div>
<CardTitle className="text-lg">Questions</CardTitle>
<CardDescription>Manage questions, quizzes, and assessments</CardDescription>
</CardHeader>
<CardContent>
<Link to="/content/questions">
<Button className="w-full bg-brand-500 hover:bg-brand-600">Manage Questions</Button>
</Link>
</CardContent>
</Card>
</div> </div>
</div> </div>
) )

View File

@ -0,0 +1,275 @@
import { useState } from "react"
import { Link } from "react-router-dom"
import { Plus, Search, Edit, Trash2 } from "lucide-react"
import { Button } from "../../components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"
import { Input } from "../../components/ui/input"
import { Select } from "../../components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../components/ui/table"
import { Badge } from "../../components/ui/badge"
type QuestionType = "multiple-choice" | "short-answer" | "true-false"
interface Question {
id: string
question: string
type: QuestionType
options: string[]
correctAnswer: string
points: number
category?: string
difficulty?: string
createdAt?: string
}
// Mock data
const mockQuestions: Question[] = [
{
id: "1",
question: "What is the capital of France?",
type: "multiple-choice",
options: ["London", "Berlin", "Paris", "Madrid"],
correctAnswer: "Paris",
points: 10,
category: "Geography",
difficulty: "Easy",
createdAt: "2024-01-15",
},
{
id: "2",
question: "Explain the concept of React hooks in your own words.",
type: "short-answer",
options: [],
correctAnswer: "React hooks are functions that let you use state and other React features in functional components.",
points: 20,
category: "Programming",
difficulty: "Medium",
createdAt: "2024-01-16",
},
{
id: "3",
question: "JavaScript is a compiled language.",
type: "true-false",
options: ["True", "False"],
correctAnswer: "False",
points: 5,
category: "Programming",
difficulty: "Easy",
createdAt: "2024-01-17",
},
{
id: "4",
question: "Which of the following is a CSS preprocessor?",
type: "multiple-choice",
options: ["SASS", "HTML", "JavaScript", "Python"],
correctAnswer: "SASS",
points: 15,
category: "Web Development",
difficulty: "Medium",
createdAt: "2024-01-18",
},
{
id: "5",
question: "TypeScript is a superset of JavaScript.",
type: "true-false",
options: ["True", "False"],
correctAnswer: "True",
points: 10,
category: "Programming",
difficulty: "Easy",
createdAt: "2024-01-19",
},
]
const typeLabels: Record<QuestionType, string> = {
"multiple-choice": "Multiple Choice",
"short-answer": "Short Answer",
"true-false": "True/False",
}
const typeColors: Record<QuestionType, string> = {
"multiple-choice": "bg-blue-100 text-blue-700",
"short-answer": "bg-green-100 text-green-700",
"true-false": "bg-purple-100 text-purple-700",
}
export function QuestionsPage() {
const [questions, setQuestions] = useState<Question[]>(mockQuestions)
const [searchQuery, setSearchQuery] = useState("")
const [typeFilter, setTypeFilter] = useState<string>("all")
const [categoryFilter, setCategoryFilter] = useState<string>("all")
const [difficultyFilter, setDifficultyFilter] = useState<string>("all")
const filteredQuestions = questions.filter((q) => {
const matchesSearch = q.question.toLowerCase().includes(searchQuery.toLowerCase())
const matchesType = typeFilter === "all" || q.type === typeFilter
const matchesCategory = categoryFilter === "all" || q.category === categoryFilter
const matchesDifficulty = difficultyFilter === "all" || q.difficulty === difficultyFilter
return matchesSearch && matchesType && matchesCategory && matchesDifficulty
})
const categories = Array.from(new Set(questions.map((q) => q.category).filter(Boolean)))
const difficulties = Array.from(new Set(questions.map((q) => q.difficulty).filter(Boolean)))
const handleDelete = (id: string) => {
if (window.confirm("Are you sure you want to delete this question?")) {
setQuestions(questions.filter((q) => q.id !== id))
}
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-grayScale-900">Questions</h1>
<Link to="/content/questions/add">
<Button className="bg-brand-500 hover:bg-brand-600">
<Plus className="h-4 w-4" />
Add New Question
</Button>
</Link>
</div>
<Card className="shadow-none">
<CardHeader>
<CardTitle>Question Management</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Search and Filters */}
<div className="flex flex-col gap-4 md:flex-row">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-grayScale-400" />
<Input
placeholder="Search questions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div className="flex gap-2">
<Select value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)}>
<option value="all">All Types</option>
<option value="multiple-choice">Multiple Choice</option>
<option value="short-answer">Short Answer</option>
<option value="true-false">True/False</option>
</Select>
{categories.length > 0 && (
<Select value={categoryFilter} onChange={(e) => setCategoryFilter(e.target.value)}>
<option value="all">All Categories</option>
{categories.map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</Select>
)}
{difficulties.length > 0 && (
<Select
value={difficultyFilter}
onChange={(e) => setDifficultyFilter(e.target.value)}
>
<option value="all">All Difficulties</option>
{difficulties.map((diff) => (
<option key={diff} value={diff}>
{diff}
</option>
))}
</Select>
)}
</div>
</div>
{/* Results count */}
<div className="text-sm text-grayScale-500">
Showing {filteredQuestions.length} of {questions.length} questions
</div>
{/* Questions Table */}
{filteredQuestions.length > 0 ? (
<div className="rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Question</TableHead>
<TableHead>Type</TableHead>
<TableHead>Category</TableHead>
<TableHead>Difficulty</TableHead>
<TableHead>Points</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredQuestions.map((question) => (
<TableRow key={question.id}>
<TableCell className="max-w-md">
<div className="truncate font-medium">{question.question}</div>
{question.type === "multiple-choice" && question.options.length > 0 && (
<div className="mt-1 text-xs text-grayScale-400">
Options: {question.options.join(", ")}
</div>
)}
</TableCell>
<TableCell>
<Badge className={typeColors[question.type]}>
{typeLabels[question.type]}
</Badge>
</TableCell>
<TableCell>{question.category || "-"}</TableCell>
<TableCell>
{question.difficulty && (
<Badge
variant={
question.difficulty === "Easy"
? "default"
: question.difficulty === "Medium"
? "secondary"
: "destructive"
}
>
{question.difficulty}
</Badge>
)}
</TableCell>
<TableCell>{question.points}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Link to={`/content/questions/edit/${question.id}`}>
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
</Link>
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(question.id)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="py-12 text-center text-grayScale-400">
<p>No questions found matching your criteria.</p>
</div>
)}
</CardContent>
</Card>
</div>
)
}