ui
This commit is contained in:
parent
d4d61bfed2
commit
7308d9bbcd
|
|
@ -29,6 +29,8 @@ import { ProgramTypeSelectionPage } from "../pages/content-management/ProgramTyp
|
|||
import { ProgramDetailPage } from "../pages/content-management/ProgramDetailPage";
|
||||
import { CourseManagementPage } from "../pages/content-management/CourseManagementPage";
|
||||
import { UnitManagementPage } from "../pages/content-management/UnitManagementPage";
|
||||
import { QuestionTypeLibraryPage } from "../pages/content-management/QuestionTypeLibraryPage";
|
||||
import { CreateQuestionTypeFlow } from "../pages/content-management/CreateQuestionTypeFlow";
|
||||
import { NotFoundPage } from "../pages/NotFoundPage";
|
||||
import { NotificationsPage } from "../pages/notifications/NotificationsPage";
|
||||
import { CreateNotificationPage } from "../pages/notifications/CreateNotificationPage";
|
||||
|
|
@ -165,6 +167,14 @@ export function AppRoutes() {
|
|||
path="/new-content/courses"
|
||||
element={<ProgramTypeSelectionPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/new-content/question-types"
|
||||
element={<QuestionTypeLibraryPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/new-content/question-types/create"
|
||||
element={<CreateQuestionTypeFlow />}
|
||||
/>
|
||||
<Route
|
||||
path="/new-content/courses/:programType"
|
||||
element={<ProgramDetailPage />}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function Stepper({ steps, currentStep, className }: StepperProps) {
|
|||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className="absolute top-4 h-[1.5px] bg-grayScale-200 z-0"
|
||||
style={{ left: "calc(50% + 24px)", right: "calc(-50% + 24px)" }}
|
||||
style={{ left: "calc(50% + 50px)", right: "calc(-50% + 50px)" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,19 +86,7 @@ export function CourseDetailPage() {
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-grayScale-200" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<div
|
||||
className="h-[0.5px] w-full opacity-20 rounded-full"
|
||||
style={{
|
||||
background: "gray",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<AddModuleModal
|
||||
isOpen={isAddModuleOpen}
|
||||
|
|
|
|||
87
src/pages/content-management/CreateQuestionTypeFlow.tsx
Normal file
87
src/pages/content-management/CreateQuestionTypeFlow.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Stepper } from "../../components/ui/stepper";
|
||||
import { QuestionTypeBasicInfoStep } from "./components/question-type-steps/QuestionTypeBasicInfoStep";
|
||||
import { QuestionTypeConfigStep } from "./components/question-type-steps/QuestionTypeConfigStep";
|
||||
|
||||
export function CreateQuestionTypeFlow() {
|
||||
const navigate = useNavigate();
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
|
||||
const steps = [
|
||||
"Basic Info",
|
||||
"Input & Answer Configuration",
|
||||
"Versions",
|
||||
"Review & Publish",
|
||||
];
|
||||
|
||||
const handleNext = () =>
|
||||
setCurrentStep((prev) => Math.min(prev + 1, steps.length));
|
||||
const handleBack = () => setCurrentStep((prev) => Math.max(prev - 1, 1));
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-20 overflow-x-hidden">
|
||||
{/* Header */}
|
||||
<div className=" border-b border-grayScale-100 sticky top-0 z-50">
|
||||
<div className="max-w-[1440px] mx-auto py-6">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<Link
|
||||
to="/new-content/question-types"
|
||||
className="flex items-center gap-2 text-[15px] font-medium text-grayScale-600 transition-colors hover:text-brand-500 group"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
|
||||
Back to Question Type Library
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-[28px] font-bold text-grayScale-900 tracking-tight">
|
||||
Create Question Type
|
||||
</h1>
|
||||
<p className="text-grayScale-500 text-[14px] font-medium">
|
||||
Create a new immersive practice session for students.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-10 px-8 rounded-[6px] border-grayScale-200 text-grayScale-900 font-medium hover:bg-grayScale-50"
|
||||
onClick={() => navigate("/new-content/question-types")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="h-10 px-8 rounded-[6px] bg-[#9E2891] font-medium text-white shadow-lg shadow-brand-500/10 hover:bg-[#8A237E] transition-all">
|
||||
Save as Draft
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 mx-auto">
|
||||
<Stepper steps={steps} currentStep={currentStep} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-[1440px] mx-auto px-10 mt-12 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
{currentStep === 1 && <QuestionTypeBasicInfoStep onNext={handleNext} />}
|
||||
{currentStep === 2 && (
|
||||
<QuestionTypeConfigStep onNext={handleNext} onBack={handleBack} />
|
||||
)}
|
||||
{currentStep > 2 && (
|
||||
<div className="bg-white rounded-2xl p-12 text-center border border-grayScale-100 shadow-sm">
|
||||
<p className="text-grayScale-400 font-medium">
|
||||
Step {currentStep} implementation in progress...
|
||||
</p>
|
||||
<Button onClick={handleBack} variant="outline" className="mt-4">
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,9 +16,11 @@ export function ProgramTypeSelectionPage() {
|
|||
exams. Select a program type to manage curriculum and modules.
|
||||
</p>
|
||||
</div>
|
||||
<Button className="h-10 px-6 rounded-[6px] bg-brand-500 font-bold text-white shadow-sm hover:bg-brand-600 transition-all flex items-center gap-2 mt-4">
|
||||
Manage Question Types
|
||||
</Button>
|
||||
<Link to="/new-content/question-types">
|
||||
<Button className="h-10 px-6 rounded-[6px] bg-brand-500 font-bold text-white shadow-sm hover:bg-brand-600 transition-all flex items-center gap-2 mt-4">
|
||||
Manage Question Types
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Gradient Divider */}
|
||||
|
|
|
|||
127
src/pages/content-management/QuestionTypeLibraryPage.tsx
Normal file
127
src/pages/content-management/QuestionTypeLibraryPage.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ArrowLeft, Plus, Search } from "lucide-react";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Select } from "../../components/ui/select";
|
||||
import { Card } from "../../components/ui/card";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { QuestionTypeCard } from "./components/QuestionTypeCard";
|
||||
|
||||
export function QuestionTypeLibraryPage() {
|
||||
const [activeTab, setActiveTab] = useState("All");
|
||||
|
||||
const questionTypes = [
|
||||
{
|
||||
title: "Describe a Photo",
|
||||
exam: "DUOLINGO" as const,
|
||||
skill: "Speaking" as const,
|
||||
variations: 12,
|
||||
status: "Published" as const,
|
||||
},
|
||||
{
|
||||
title: "Write About the Topic",
|
||||
exam: "DUOLINGO" as const,
|
||||
skill: "Writing" as const,
|
||||
variations: 12,
|
||||
status: "Published" as const,
|
||||
},
|
||||
{
|
||||
title: "Fill in the Blanks",
|
||||
exam: "IELTS" as const,
|
||||
skill: "Writing" as const,
|
||||
variations: 12,
|
||||
status: "Published" as const,
|
||||
},
|
||||
{
|
||||
title: "Describe a Photo",
|
||||
exam: "DUOLINGO" as const,
|
||||
skill: "Speaking" as const,
|
||||
variations: 12,
|
||||
status: "Published" as const,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in duration-500 pb-20">
|
||||
{/* Navigation & Header */}
|
||||
<div className="space-y-6">
|
||||
<Link
|
||||
to="/new-content/courses"
|
||||
className="flex items-center gap-2 text-[15px] font-bold text-grayScale-600 transition-colors hover:text-brand-500 group w-fit"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5 transition-transform group-hover:-translate-x-1" />
|
||||
Back to Courses
|
||||
</Link>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-[32px] font-medium text-grayScale-900 tracking-tight">
|
||||
Question Type Library
|
||||
</h1>
|
||||
<p className="text-grayScale-500 text-[16px] font-medium">
|
||||
Create and manage reusable question structures for practices and
|
||||
assessments.
|
||||
</p>
|
||||
</div>
|
||||
<Link to="/new-content/question-types/create">
|
||||
<Button className="h-12 px-8 rounded-[10px] bg-[#9E2891] font-bold text-white shadow-lg shadow-brand-500/10 hover:bg-[#8A237E] transition-all flex items-center gap-3">
|
||||
<Plus className="h-5 w-5" />
|
||||
Create Question Type
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control Bar */}
|
||||
<Card className="p-6 border-grayScale-200 rounded-2xl bg-white space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-grayScale-600" />
|
||||
<Input
|
||||
className="h-10 pl-12 rounded-[6px] border-grayScale-200 placeholder:text-grayScale-600 bg-[#F8FAFC] transition-all text-sm"
|
||||
placeholder="Search by practice name, ID, or keywords..."
|
||||
/>
|
||||
</div>
|
||||
<Select className="h-10 w-[180px] rounded-[6px] border-grayScale-200 placeholder:text-grayScale-600 text-grayScale-700 bg-[#F8FAFC] transition-all text-sm">
|
||||
<option>All Exams</option>
|
||||
<option>IELTS</option>
|
||||
<option>Duolingo</option>
|
||||
</Select>
|
||||
<Select className="h-10 w-[180px] rounded-[6px] border-grayScale-200 placeholder:text-grayScale-600 text-grayScale-700 bg-[#F8FAFC] transition-all text-sm">
|
||||
<option>All Skills</option>
|
||||
<option>Speaking</option>
|
||||
<option>Writing</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-[12px] font-medium text-grayScale-400 uppercase tracking-widest mr-2">
|
||||
STATUS:
|
||||
</span>
|
||||
{["All", "Published", "Drafts", "Archived"].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={cn(
|
||||
"h-10 px-4 rounded-full text-[13px] font-medium transition-all",
|
||||
activeTab === tab
|
||||
? "bg-[#9E2891] text-white shadow-md shadow-brand-500/20"
|
||||
: "bg-grayScale-100 text-grayScale-400 hover:bg-grayScale-100",
|
||||
)}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Grid of Cards */}
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
{questionTypes.map((qt, index) => (
|
||||
<QuestionTypeCard key={index} {...qt} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
src/pages/content-management/components/QuestionTypeCard.tsx
Normal file
83
src/pages/content-management/components/QuestionTypeCard.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { Edit2, Trash2, Mic2, Keyboard, Layers, MicIcon } from "lucide-react";
|
||||
import { Badge } from "../../../components/ui/badge";
|
||||
import { Card } from "../../../components/ui/card";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
interface QuestionTypeCardProps {
|
||||
title: string;
|
||||
exam: "DUOLINGO" | "IELTS" | "TOEFL";
|
||||
skill: "Speaking" | "Writing" | "Listening" | "Reading";
|
||||
variations: number;
|
||||
status: "Published" | "Draft" | "Archived";
|
||||
}
|
||||
|
||||
export function QuestionTypeCard({
|
||||
title,
|
||||
exam,
|
||||
skill,
|
||||
variations,
|
||||
status,
|
||||
}: QuestionTypeCardProps) {
|
||||
const SkillIcon = skill === "Speaking" ? MicIcon : Keyboard;
|
||||
|
||||
const examColors = {
|
||||
DUOLINGO: "bg-[#22C55EE5] text-[#fff] border-transparent",
|
||||
IELTS: "bg-[#EF4444E5] text-[#fff] border-transparent",
|
||||
TOEFL: "bg-[#DBEAFE] text-[#fff] border-transparent",
|
||||
};
|
||||
|
||||
const statusColors = {
|
||||
Published: "bg-[#F0FDF4] text-[#16A34A]",
|
||||
Draft: "bg-grayScale-50 text-grayScale-500",
|
||||
Archived: "bg-red-50 text-red-500",
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="group overflow-hidden border-grayScale-200 rounded-[12px] bg-white transition-all duration-300">
|
||||
<div className="px-4 py-6 space-y-8">
|
||||
<h3 className="text-[20px] font-bold text-grayScale-900 leading-[1.2]">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge
|
||||
className={cn(
|
||||
"px-3 py-1 rounded-[4px] text-[11px] font-bold tracking-wider shadow-none border-none",
|
||||
examColors[exam],
|
||||
)}
|
||||
>
|
||||
{exam}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-1 text-grayScale-900 font-bold text-[13px]">
|
||||
<SkillIcon className="h-4 w-4" />
|
||||
{skill}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 text-[#9E2891] font-medium text-[15px]">
|
||||
<Layers className="h-[16px] w-[16px]" />
|
||||
{variations} Variations
|
||||
</div>
|
||||
|
||||
<div className="pt-4 flex items-center justify-between border-t border-grayScale-200">
|
||||
<Badge
|
||||
className={cn(
|
||||
"px-3 py-1 rounded-[4px] text-[12px] font-bold shadow-none border-none",
|
||||
statusColors[status],
|
||||
)}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-5 transition-opacity">
|
||||
<button className="text-grayScale-500/70 transition-all">
|
||||
<Edit2 className="h-5 w-5" />
|
||||
</button>
|
||||
<button className="text-grayScale-500/70 transition-all">
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { useState } from "react";
|
||||
import { X, ArrowRight } from "lucide-react";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { Card } from "../../../../components/ui/card";
|
||||
import { Select } from "../../../../components/ui/select";
|
||||
import { Badge } from "../../../../components/ui/badge";
|
||||
|
||||
interface QuestionTypeBasicInfoStepProps {
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
export function QuestionTypeBasicInfoStep({
|
||||
onNext,
|
||||
}: QuestionTypeBasicInfoStepProps) {
|
||||
const [selectedChips, setSelectedChips] = useState([
|
||||
"Multiple Choice",
|
||||
"Sentence Completion",
|
||||
]);
|
||||
const suggestions = ["Matching Headings", "True/False/NG"];
|
||||
|
||||
const removeChip = (chip: string) => {
|
||||
setSelectedChips(selectedChips.filter((c) => c !== chip));
|
||||
};
|
||||
|
||||
const addChip = (chip: string) => {
|
||||
if (!selectedChips.includes(chip)) {
|
||||
setSelectedChips([...selectedChips, chip]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 pb-32">
|
||||
<Card className="max-w-4xl mx-auto overflow-hidden border-grayScale-100 shadow-sm rounded-2xl bg-white">
|
||||
<div className="p-10 border-b border-grayScale-200">
|
||||
<h2 className="text-[20px] font-medium text-grayScale-900">
|
||||
STEP 1: Basic Info
|
||||
</h2>
|
||||
<p className="text-grayScale-500 font-medium mt-1">
|
||||
Define what this question type is and where it applies.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-10 space-y-10">
|
||||
{/* Top Row: Course Type & Skill Category */}
|
||||
<div className="grid grid-cols-2 gap-10">
|
||||
<div className="space-y-3">
|
||||
<label className="text-[14px] font-medium text-grayScale-700 flex items-center gap-1">
|
||||
Course Type <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select className="h-12 rounded-[12px] border-grayScale-300 bg-[#F8FAFC] font-medium text-grayScale-900 transition-all ">
|
||||
<option>Select an exam type</option>
|
||||
<option>IELTS</option>
|
||||
<option>Duolingo</option>
|
||||
<option>TOEFL</option>
|
||||
</Select>
|
||||
<p className="text-grayScale-400 text-[13px] font-medium leading-relaxed">
|
||||
The core framework for the practice test.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[14px] font-medium text-grayScale-700 flex items-center gap-1">
|
||||
Skill Category <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select className="h-12 rounded-[12px] border-grayScale-300 bg-[#F8FAFC] font-medium text-grayScale-900 transition-all ">
|
||||
<option>Select a skill</option>
|
||||
<option>Speaking</option>
|
||||
<option>Writing</option>
|
||||
<option>Listening</option>
|
||||
<option>Reading</option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Question Type */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[14px] font-bold text-grayScale-700 flex items-center gap-1">
|
||||
Question Type <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Select className="h-12 rounded-[12px] border-grayScale-300 bg-[#F8FAFC] font-medium text-grayScale-900 transition-all ">
|
||||
<option>Single Format</option>
|
||||
<option>Mixed Format</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Question Types Chip Input */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-[14px] font-bold text-grayScale-700 flex items-center gap-1">
|
||||
Question Types <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="min-h-[56px] p-3 flex flex-wrap gap-2.5 rounded-[12px] border border-grayScale-300 bg-[#F8FAFC]">
|
||||
{selectedChips.map((chip) => (
|
||||
<Badge
|
||||
key={chip}
|
||||
className="bg-[#9E28911A] text-[#9E2891] border-[#9E289133] px-2 py-0 rounded-full text-[13px] font-medium flex items-center gap-2"
|
||||
>
|
||||
{chip}
|
||||
<button
|
||||
onClick={() => removeChip(chip)}
|
||||
className="hover:text-red-500 transition-colors"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
<input
|
||||
className="flex-1 min-w-[150px] bg-transparent border-none focus:ring-0 text-[14px] font-medium text-grayScale-900 px-3 placeholder:text-grayScale-400"
|
||||
placeholder="Add question types..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<span className="text-[13px] font-medium text-grayScale-500">
|
||||
Suggestions:
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{suggestions.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => addChip(s)}
|
||||
className="px-3 py-1.5 rounded-[6px] border border-grayScale-300 text-[13px] font-medium text-grayScale-600 hover:bg-grayScale-50 hover:text-[#9E2891] hover:border-[#9E2891]/20 transition-all"
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-4 py-4 border border-grayScale-200 flex items-center justify-between bg-[#F8FAFC]">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-10 px-6 rounded-[6px] border-none shadow-none text-grayScale-600 font-bold hover:bg-grayScale-100"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onNext}
|
||||
className="h-10 px-10 rounded-[6px] bg-[#9E2891] font-medium text-white shadow-lg shadow-brand-500/10 hover:bg-[#8A237E] transition-all flex items-center gap-3"
|
||||
>
|
||||
Next: Structure
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user