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

Forgot Password

+

+ Enter your email address and we'll send you a reset link. +

+
+ +
+
+ + setEmail(e.target.value)} + required + /> +
+ + +
+ +
+ + Back to Login + +
+
+
+
+ ) +} + diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx new file mode 100644 index 0000000..5d71557 --- /dev/null +++ b/src/pages/auth/LoginPage.tsx @@ -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 ( +
+
+
+
+ +
+ +
+

Admin Login

+

Please enter your details to continue

+
+ +
+
+ + setEmail(e.target.value)} + required + /> +
+ +
+ +
+ setPassword(e.target.value)} + required + className="pr-10" + /> + +
+
+ +
+ + Forgot Password? + +
+ + +
+
+
+
+ ) +} + diff --git a/src/pages/auth/VerificationPage.tsx b/src/pages/auth/VerificationPage.tsx new file mode 100644 index 0000000..5cce8b7 --- /dev/null +++ b/src/pages/auth/VerificationPage.tsx @@ -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(["", "", "", "", ""]) + 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) => { + 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 ( +
+
+
+
+ +
+ +
+

Verification Code

+

+ We have sent a verification code sent to your email address ******230@gmail.com +

+
+ +
+
+ {codes.map((code, index) => ( + { + 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" + : "" + }`} + /> + ))} +
+ +
+ Resend code in {formatTime(timeLeft)} +
+ + + +
+ +
+
+
+
+
+ ) +} + diff --git a/src/pages/content-management/AddQuestionPage.tsx b/src/pages/content-management/AddQuestionPage.tsx new file mode 100644 index 0000000..93a2a61 --- /dev/null +++ b/src/pages/content-management/AddQuestionPage.tsx @@ -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( + 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 ( +
+
+ +

+ {isEditing ? "Edit Question" : "Add New Question"} +

+
+ +
+ + + Question Details + + + {/* Question Type */} +
+ + +
+ + {/* Question Text */} +
+ +