Yimaru-Admin/src/pages/content-management/AddPracticePage.tsx

583 lines
23 KiB
TypeScript

import { useState } from "react"
import { useNavigate } from "react-router-dom"
import { X, Plus, Check, ArrowLeft } from "lucide-react"
import { Button } from "../../components/ui/button"
import { Card } from "../../components/ui/card"
import { Input } from "../../components/ui/input"
import { Textarea } from "../../components/ui/textarea"
import { Select } from "../../components/ui/select"
import { Stepper } from "../../components/ui/stepper"
import { Badge } from "../../components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "../../components/ui/avatar"
type QuestionType = "multiple-choice" | "short-answer" | "true-false"
interface Question {
id: string
question: string
type: QuestionType
options: string[]
correctAnswer: string
points: number
}
interface PracticeFormData {
title: string
description: string
category: string
difficulty: string
duration: string
tags: string
participants: string[]
questions: Question[]
}
const STEPS = ["Details", "Questions", "Review"]
export function AddPracticePage() {
const navigate = useNavigate()
const [currentStep, setCurrentStep] = useState(1)
const [formData, setFormData] = useState<PracticeFormData>({
title: "",
description: "",
category: "",
difficulty: "",
duration: "",
tags: "",
participants: [],
questions: [],
})
const [currentQuestion, setCurrentQuestion] = useState<Partial<Question>>({
question: "",
type: "multiple-choice",
options: ["", "", "", ""],
correctAnswer: "",
points: 10,
})
const mockParticipants = [
{ id: "1", name: "Sarah", avatar: "" },
{ id: "2", name: "Jon", avatar: "" },
{ id: "3", name: "Priya", avatar: "" },
{ id: "4", name: "Jake", avatar: "" },
{ id: "5", name: "Emma", avatar: "" },
{ id: "6", name: "Robert", avatar: "" },
{ id: "7", name: "Luke", avatar: "" },
{ id: "8", name: "Ethan", avatar: "" },
]
const toggleParticipant = (id: string) => {
setFormData((prev) => ({
...prev,
participants: prev.participants.includes(id)
? prev.participants.filter((p) => p !== id)
: [...prev.participants, id],
}))
}
const addQuestion = () => {
if (!currentQuestion.question || !currentQuestion.correctAnswer) return
const newQuestion: Question = {
id: Date.now().toString(),
question: currentQuestion.question,
type: currentQuestion.type as QuestionType,
options: currentQuestion.options || [],
correctAnswer: currentQuestion.correctAnswer,
points: currentQuestion.points || 10,
}
setFormData((prev) => ({
...prev,
questions: [...prev.questions, newQuestion],
}))
setCurrentQuestion({
question: "",
type: "multiple-choice",
options: ["", "", "", ""],
correctAnswer: "",
points: 10,
})
}
const addOption = () => {
setCurrentQuestion((prev) => ({
...prev,
options: [...(prev.options || []), ""],
}))
}
const updateOption = (index: number, value: string) => {
setCurrentQuestion((prev) => ({
...prev,
options: prev.options?.map((opt, i) => (i === index ? value : opt)),
}))
}
const handleSubmit = () => {
console.log("Practice data:", formData)
// Handle form submission
}
const canProceedToStep2 = () => {
return (
formData.title &&
formData.description &&
formData.category &&
formData.difficulty &&
formData.duration
)
}
const canProceedToStep3 = () => {
return formData.questions.length > 0
}
return (
<div className="mx-auto w-full max-w-7xl space-y-6 pb-10 sm:space-y-8 sm:pb-12">
{/* Header */}
<div className="flex flex-col gap-4 border-b border-grayScale-100 pb-6 sm:flex-row sm:items-end sm:justify-between sm:pb-8">
<div className="flex items-start gap-3 sm:items-center">
<Button
variant="ghost"
size="icon"
onClick={() => navigate("/content/speaking")}
className="h-10 w-10 shrink-0 rounded-xl border border-grayScale-200 bg-white shadow-sm transition-colors hover:bg-grayScale-50 hover:border-grayScale-300"
>
<ArrowLeft className="h-4 w-4 text-grayScale-600" />
</Button>
<div>
<h1 className="text-2xl font-bold tracking-tight text-grayScale-900 sm:text-3xl">Add practice</h1>
<p className="mt-2 max-w-2xl text-sm leading-relaxed text-grayScale-500 sm:text-[15px]">
Draft a practice session with questions (demo flow wire to API when ready).
</p>
</div>
</div>
<Button className="h-11 w-full shrink-0 bg-brand-500 px-5 shadow-sm hover:bg-brand-600 sm:w-auto">
<Check className="h-4 w-4" />
Save
</Button>
</div>
{/* Stepper */}
<Card className="overflow-hidden border-grayScale-200/80 shadow-sm">
<div className="rounded-2xl border border-grayScale-100 bg-grayScale-50/40 px-4 py-5 sm:px-6">
<Stepper steps={STEPS} currentStep={currentStep} />
</div>
</Card>
{/* Step 1: Details */}
{currentStep === 1 && (
<Card className="w-full overflow-hidden border-grayScale-200/80 shadow-sm">
<div className="border-b border-grayScale-100 bg-gradient-to-r from-grayScale-50/80 to-white px-5 py-5 sm:px-8 sm:py-6">
<h2 className="text-lg font-semibold tracking-tight text-grayScale-900 sm:text-xl">Step 1: Details</h2>
<p className="mt-1.5 text-sm text-grayScale-500">Basics for this practice session.</p>
</div>
<div className="space-y-5 p-5 sm:p-8 lg:p-10">
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Practice Title
</label>
<Input
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="Enter practice title"
required
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Description
</label>
<Textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Enter practice description"
rows={4}
required
/>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Category
</label>
<Select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
required
>
<option value="">Select category</option>
<option value="grammar">Grammar</option>
<option value="vocabulary">Vocabulary</option>
<option value="speaking">Speaking</option>
<option value="listening">Listening</option>
</Select>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Difficulty
</label>
<Select
value={formData.difficulty}
onChange={(e) => setFormData({ ...formData, difficulty: e.target.value })}
required
>
<option value="">Select difficulty</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</Select>
</div>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Duration (minutes)
</label>
<Input
type="number"
value={formData.duration}
onChange={(e) => setFormData({ ...formData, duration: e.target.value })}
placeholder="30"
required
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">Tags</label>
<Input
value={formData.tags}
onChange={(e) => setFormData({ ...formData, tags: e.target.value })}
placeholder="Enter tags separated by commas"
/>
</div>
</div>
</div>
<div className="flex justify-end border-t border-grayScale-100 bg-grayScale-50/30 px-5 py-4 sm:px-8 sm:py-5">
<Button
onClick={() => setCurrentStep(2)}
disabled={!canProceedToStep2()}
className="min-w-[140px] bg-brand-500 shadow-sm hover:bg-brand-600"
>
Next
</Button>
</div>
</Card>
)}
{/* Step 2: Questions */}
{currentStep === 2 && (
<div className="space-y-6">
{/* Select Participants Section */}
<Card className="overflow-hidden border-grayScale-200/80 shadow-sm">
<div className="border-b border-grayScale-100 bg-gradient-to-r from-grayScale-50/80 to-white px-5 py-4 sm:px-8 sm:py-5">
<h2 className="text-lg font-semibold tracking-tight text-grayScale-900 sm:text-xl">Step 2: Participants</h2>
<p className="mt-1 text-sm text-grayScale-500">Optional select who appears in this practice.</p>
</div>
<div className="p-5 sm:p-8">
<div className="grid grid-cols-2 gap-5 sm:grid-cols-4 lg:grid-cols-8">
{mockParticipants.map((participant) => {
const isSelected = formData.participants.includes(participant.id)
return (
<div
key={participant.id}
className="group relative flex cursor-pointer flex-col items-center"
onClick={() => toggleParticipant(participant.id)}
>
<div className="relative">
<Avatar
className={`h-16 w-16 border-2 transition-all duration-200 ${
isSelected
? "border-brand-500 ring-2 ring-brand-500/20 scale-105"
: "border-grayScale-200 group-hover:border-brand-400 group-hover:shadow-md group-hover:scale-105"
}`}
>
<AvatarImage src={participant.avatar} />
<AvatarFallback className="bg-brand-100 text-brand-600 font-medium">
{participant.name[0]}
</AvatarFallback>
</Avatar>
{isSelected && (
<div className="absolute -right-1 -top-1 grid h-5 w-5 place-items-center rounded-full bg-brand-500 text-white shadow-sm ring-2 ring-white">
<X className="h-3 w-3" />
</div>
)}
</div>
<span
className={`mt-2 text-xs font-medium transition-colors ${
isSelected ? "text-brand-600" : "text-grayScale-500 group-hover:text-grayScale-600"
}`}
>
{participant.name}
</span>
</div>
)
})}
</div>
</div>
</Card>
{/* Add Questions Section */}
<Card className="overflow-hidden border-grayScale-200/80 shadow-sm">
<div className="border-b border-grayScale-100 bg-gradient-to-r from-grayScale-50/80 to-white px-5 py-4 sm:px-8 sm:py-5">
<h2 className="text-lg font-semibold tracking-tight text-grayScale-900 sm:text-xl">Questions</h2>
<p className="mt-1 text-sm text-grayScale-500">Build your question bank, then add to the practice.</p>
</div>
<div className="p-5 sm:p-8">
{/* Existing Questions */}
{formData.questions.map((q) => (
<div key={q.id} className="mb-4 rounded-xl border border-grayScale-200 bg-grayScale-50/50 p-4 transition-colors hover:bg-grayScale-50">
<div className="mb-2 flex items-start justify-between">
<p className="font-medium text-grayScale-600">{q.question}</p>
<Badge variant="secondary">{q.points} points</Badge>
</div>
<p className="text-sm text-grayScale-400">
Type: {q.type} | Correct Answer: {q.correctAnswer}
</p>
</div>
))}
{/* Add New Question Form */}
<div className="space-y-5 rounded-xl border border-dashed border-grayScale-300 bg-white p-5">
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Question
</label>
<Textarea
value={currentQuestion.question}
onChange={(e) =>
setCurrentQuestion({ ...currentQuestion, question: e.target.value })
}
placeholder="Enter your question"
rows={2}
/>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Question Type
</label>
<Select
value={currentQuestion.type}
onChange={(e) =>
setCurrentQuestion({
...currentQuestion,
type: e.target.value as QuestionType,
})
}
>
<option value="multiple-choice">Multiple Choice</option>
<option value="short-answer">Short Answer</option>
<option value="true-false">True/False</option>
</Select>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">Points</label>
<Input
type="number"
value={currentQuestion.points}
onChange={(e) =>
setCurrentQuestion({
...currentQuestion,
points: parseInt(e.target.value) || 10,
})
}
min="1"
/>
</div>
</div>
{currentQuestion.type === "multiple-choice" && (
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Options
</label>
<div className="space-y-2">
{currentQuestion.options?.map((option, index) => (
<div key={index} className="flex items-center gap-2">
<Input
value={option}
onChange={(e) => updateOption(index, e.target.value)}
placeholder={`Option ${index + 1}`}
/>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
onClick={addOption}
className="mt-1 w-full border-dashed"
>
<Plus className="h-4 w-4" />
Add Option
</Button>
</div>
</div>
)}
<div>
<label className="mb-1.5 block text-sm font-medium text-grayScale-500">
Correct Answer
</label>
{currentQuestion.type === "multiple-choice" ? (
<Select
value={currentQuestion.correctAnswer}
onChange={(e) =>
setCurrentQuestion({ ...currentQuestion, correctAnswer: e.target.value })
}
>
<option value="">Select correct option</option>
{currentQuestion.options?.map(
(opt, idx) =>
opt && (
<option key={idx} value={opt}>
{opt}
</option>
),
)}
</Select>
) : (
<Input
value={currentQuestion.correctAnswer}
onChange={(e) =>
setCurrentQuestion({ ...currentQuestion, correctAnswer: e.target.value })
}
placeholder="Enter correct answer"
/>
)}
</div>
<Button
type="button"
onClick={addQuestion}
disabled={!currentQuestion.question || !currentQuestion.correctAnswer}
className="w-full bg-brand-500 shadow-sm hover:bg-brand-600 transition-colors"
>
<Plus className="h-4 w-4" />
Add New Question
</Button>
</div>
</div>
</Card>
<div className="flex flex-col-reverse gap-3 rounded-2xl border border-grayScale-200/80 bg-grayScale-50/30 px-4 py-4 sm:flex-row sm:justify-end sm:px-6 sm:py-5">
<Button variant="outline" onClick={() => setCurrentStep(1)} className="px-6 sm:w-auto">
Back
</Button>
<Button
onClick={() => setCurrentStep(3)}
disabled={!canProceedToStep3()}
className="min-w-[140px] bg-brand-500 px-6 shadow-sm hover:bg-brand-600"
>
Next
</Button>
</div>
</div>
)}
{/* Step 3: Review */}
{currentStep === 3 && (
<div className="space-y-6">
<Card className="overflow-hidden border-grayScale-200/80 shadow-sm">
<div className="border-b border-grayScale-100 bg-gradient-to-r from-grayScale-50/80 to-white px-5 py-4 sm:px-8 sm:py-5">
<h2 className="text-lg font-semibold tracking-tight text-grayScale-900 sm:text-xl">Step 3: Review</h2>
<p className="mt-1 text-sm text-grayScale-500">Confirm details before creating the practice.</p>
</div>
<div className="p-5 sm:p-8">
<div className="divide-y divide-grayScale-100 overflow-hidden rounded-lg border border-grayScale-200">
{[
{ label: "Title", value: formData.title },
{ label: "Description", value: formData.description },
{ label: "Category", value: formData.category },
{ label: "Difficulty", value: formData.difficulty },
{ label: "Duration", value: `${formData.duration} minutes` },
{ label: "Tags", value: formData.tags },
].map((row, idx) => (
<div
key={row.label}
className={`flex items-baseline justify-between px-4 py-3 ${
idx % 2 === 0 ? "bg-grayScale-50/50" : "bg-white"
}`}
>
<span className="text-sm font-medium text-grayScale-400">{row.label}</span>
<span className="text-right text-sm font-medium text-grayScale-600">
{row.value}
</span>
</div>
))}
</div>
</div>
</Card>
<Card className="overflow-hidden border-grayScale-200/80 shadow-sm">
<div className="border-b border-grayScale-100 bg-gradient-to-r from-grayScale-50/80 to-white px-5 py-4 sm:px-8 sm:py-5">
<h2 className="text-lg font-semibold tracking-tight text-grayScale-900 sm:text-xl">Questions</h2>
</div>
<div className="p-5 sm:p-8">
<div className="space-y-4">
{formData.questions.map((q, index) => (
<div key={q.id} className="rounded-xl border border-grayScale-200 bg-grayScale-50/50 p-5 transition-colors hover:bg-grayScale-50">
<div className="mb-2 flex items-start justify-between gap-3">
<div>
<p className="font-medium text-grayScale-600">
{index + 1}. {q.question}
</p>
<p className="mt-1.5 text-sm text-grayScale-400">
Type: {q.type} | Points: {q.points}
</p>
</div>
</div>
{q.type === "multiple-choice" && q.options.length > 0 && (
<div className="mt-3 space-y-1.5">
{q.options.map((opt, optIdx) => (
<div
key={optIdx}
className={`rounded-lg px-3 py-1.5 text-sm ${
opt === q.correctAnswer
? "bg-brand-100 text-brand-700 font-medium ring-1 ring-brand-200"
: "bg-white text-grayScale-500 ring-1 ring-grayScale-100"
}`}
>
{opt}
{opt === q.correctAnswer && (
<Check className="ml-2 inline h-3.5 w-3.5" />
)}
</div>
))}
</div>
)}
<div className="mt-3 border-t border-grayScale-100 pt-2 text-sm text-grayScale-400">
Correct Answer: <span className="font-medium text-grayScale-600">{q.correctAnswer}</span>
</div>
</div>
))}
</div>
</div>
</Card>
<div className="flex flex-col-reverse gap-3 rounded-2xl border border-grayScale-200/80 bg-grayScale-50/30 px-4 py-4 sm:flex-row sm:justify-end sm:px-6 sm:py-5">
<Button variant="outline" onClick={() => setCurrentStep(2)} className="px-6 sm:w-auto">
Back
</Button>
<Button onClick={handleSubmit} className="min-w-[160px] bg-brand-500 px-6 shadow-sm hover:bg-brand-600">
Create practice
</Button>
</div>
</div>
)}
</div>
)
}