questions-page
This commit is contained in:
parent
79e2ef6ce1
commit
ca72ca0a70
19
package-lock.json
generated
19
package-lock.json
generated
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />} />
|
||||||
|
|
|
||||||
64
src/pages/auth/ForgotPasswordPage.tsx
Normal file
64
src/pages/auth/ForgotPasswordPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
89
src/pages/auth/LoginPage.tsx
Normal file
89
src/pages/auth/LoginPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
151
src/pages/auth/VerificationPage.tsx
Normal file
151
src/pages/auth/VerificationPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
316
src/pages/content-management/AddQuestionPage.tsx
Normal file
316
src/pages/content-management/AddQuestionPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
275
src/pages/content-management/QuestionsPage.tsx
Normal file
275
src/pages/content-management/QuestionsPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user