remove speaking tab and align human language filters
Hide Speaking from content tabs and update Human Language filters to subcategory and course-focused selection for the single descriptive language category flow. Made-with: Cursor
This commit is contained in:
parent
e477595578
commit
882db5444d
|
|
@ -6,7 +6,6 @@ const tabs = [
|
||||||
{ label: "Courses", to: "/content/courses" },
|
{ label: "Courses", to: "/content/courses" },
|
||||||
{ label: "Human Language", to: "/content/human-language" },
|
{ label: "Human Language", to: "/content/human-language" },
|
||||||
{ label: "Flows", to: "/content/flows" },
|
{ label: "Flows", to: "/content/flows" },
|
||||||
{ label: "Speaking", to: "/content/speaking" },
|
|
||||||
{ label: "Practice", to: "/content/practices" },
|
{ label: "Practice", to: "/content/practices" },
|
||||||
{ label: "Questions", to: "/content/questions" },
|
{ label: "Questions", to: "/content/questions" },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/ca
|
||||||
import { Button } from "../../components/ui/button"
|
import { Button } from "../../components/ui/button"
|
||||||
import { SpinnerIcon } from "../../components/ui/spinner-icon"
|
import { SpinnerIcon } from "../../components/ui/spinner-icon"
|
||||||
import { getCourseCategories, getCoursesByCategory, getHumanLanguageLessonsByCourse } from "../../api/courses.api"
|
import { getCourseCategories, getCoursesByCategory, getHumanLanguageLessonsByCourse } from "../../api/courses.api"
|
||||||
import type { Course, CourseCategory, HumanLanguageLesson } from "../../types/course.types"
|
import type { Course, HumanLanguageLesson } from "../../types/course.types"
|
||||||
|
|
||||||
const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const
|
const CEFR_LEVELS = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"] as const
|
||||||
type CefrLevel = (typeof CEFR_LEVELS)[number]
|
type CefrLevel = (typeof CEFR_LEVELS)[number]
|
||||||
|
|
||||||
export function HumanLanguagePage() {
|
export function HumanLanguagePage() {
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [categories, setCategories] = useState<CourseCategory[]>([])
|
|
||||||
const [courses, setCourses] = useState<Course[]>([])
|
const [courses, setCourses] = useState<Course[]>([])
|
||||||
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null)
|
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null)
|
||||||
const [selectedCourseId, setSelectedCourseId] = useState<number | null>(null)
|
const [selectedCourseId, setSelectedCourseId] = useState<number | null>(null)
|
||||||
|
const [selectedLessonId, setSelectedLessonId] = useState<number | "ALL">("ALL")
|
||||||
const [selectedLevel, setSelectedLevel] = useState<CefrLevel | "ALL">("ALL")
|
const [selectedLevel, setSelectedLevel] = useState<CefrLevel | "ALL">("ALL")
|
||||||
const [collapsedLevels, setCollapsedLevels] = useState<CefrLevel[]>([])
|
const [collapsedLevels, setCollapsedLevels] = useState<CefrLevel[]>([])
|
||||||
const [lessonsByLevel, setLessonsByLevel] = useState<Record<CefrLevel, HumanLanguageLesson[]>>(
|
const [lessonsByLevel, setLessonsByLevel] = useState<Record<CefrLevel, HumanLanguageLesson[]>>(
|
||||||
|
|
@ -28,7 +28,6 @@ export function HumanLanguagePage() {
|
||||||
try {
|
try {
|
||||||
const res = await getCourseCategories()
|
const res = await getCourseCategories()
|
||||||
const items = res.data?.data?.categories ?? []
|
const items = res.data?.data?.categories ?? []
|
||||||
setCategories(items)
|
|
||||||
const humanLanguageCategory =
|
const humanLanguageCategory =
|
||||||
items.find((c) => c.name.toLowerCase().includes("human language")) ??
|
items.find((c) => c.name.toLowerCase().includes("human language")) ??
|
||||||
items.find((c) => c.name.toLowerCase().includes("language")) ??
|
items.find((c) => c.name.toLowerCase().includes("language")) ??
|
||||||
|
|
@ -50,6 +49,7 @@ export function HumanLanguagePage() {
|
||||||
const items = res.data?.data?.courses ?? []
|
const items = res.data?.data?.courses ?? []
|
||||||
setCourses(items)
|
setCourses(items)
|
||||||
setSelectedCourseId(items[0]?.id ?? null)
|
setSelectedCourseId(items[0]?.id ?? null)
|
||||||
|
setSelectedLessonId("ALL")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +85,18 @@ export function HumanLanguagePage() {
|
||||||
() => (selectedLevel === "ALL" ? CEFR_LEVELS : [selectedLevel]).map((level) => ({ level, rows: lessonsByLevel[level] })),
|
() => (selectedLevel === "ALL" ? CEFR_LEVELS : [selectedLevel]).map((level) => ({ level, rows: lessonsByLevel[level] })),
|
||||||
[lessonsByLevel, selectedLevel],
|
[lessonsByLevel, selectedLevel],
|
||||||
)
|
)
|
||||||
|
const lessonOptions = useMemo(() => {
|
||||||
|
const seen = new Set<number>()
|
||||||
|
const options: { id: number; title: string }[] = []
|
||||||
|
for (const rows of Object.values(lessonsByLevel)) {
|
||||||
|
for (const lesson of rows) {
|
||||||
|
if (seen.has(lesson.id)) continue
|
||||||
|
seen.add(lesson.id)
|
||||||
|
options.push({ id: lesson.id, title: lesson.title })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options.sort((a, b) => a.title.localeCompare(b.title))
|
||||||
|
}, [lessonsByLevel])
|
||||||
|
|
||||||
const toggleLevel = (level: CefrLevel) => {
|
const toggleLevel = (level: CefrLevel) => {
|
||||||
setCollapsedLevels((prev) => (prev.includes(level) ? prev.filter((l) => l !== level) : [...prev, level]))
|
setCollapsedLevels((prev) => (prev.includes(level) ? prev.filter((l) => l !== level) : [...prev, level]))
|
||||||
|
|
@ -112,21 +124,7 @@ export function HumanLanguagePage() {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-medium uppercase tracking-wide text-grayScale-500">Category</label>
|
<label className="text-xs font-medium uppercase tracking-wide text-grayScale-500">Subcategory</label>
|
||||||
<select
|
|
||||||
className="h-10 w-full rounded-md border border-grayScale-200 bg-white px-3 text-sm"
|
|
||||||
value={selectedCategoryId ?? ""}
|
|
||||||
onChange={(e) => setSelectedCategoryId(Number(e.target.value))}
|
|
||||||
>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<option key={category.id} value={category.id}>
|
|
||||||
{category.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="text-xs font-medium uppercase tracking-wide text-grayScale-500">Course</label>
|
|
||||||
<select
|
<select
|
||||||
className="h-10 w-full rounded-md border border-grayScale-200 bg-white px-3 text-sm"
|
className="h-10 w-full rounded-md border border-grayScale-200 bg-white px-3 text-sm"
|
||||||
value={selectedCourseId ?? ""}
|
value={selectedCourseId ?? ""}
|
||||||
|
|
@ -139,6 +137,23 @@ export function HumanLanguagePage() {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-medium uppercase tracking-wide text-grayScale-500">Course</label>
|
||||||
|
<select
|
||||||
|
className="h-10 w-full rounded-md border border-grayScale-200 bg-white px-3 text-sm"
|
||||||
|
value={selectedLessonId}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedLessonId(e.target.value === "ALL" ? "ALL" : Number(e.target.value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="ALL">All courses</option>
|
||||||
|
{lessonOptions.map((lesson) => (
|
||||||
|
<option key={lesson.id} value={lesson.id}>
|
||||||
|
{lesson.title}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-medium uppercase tracking-wide text-grayScale-500">Fetch lessons by level</label>
|
<label className="text-xs font-medium uppercase tracking-wide text-grayScale-500">Fetch lessons by level</label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -172,7 +187,10 @@ export function HumanLanguagePage() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{levelRows.map(({ level, rows }) => (
|
{levelRows.map(({ level, rows }) => {
|
||||||
|
const filteredRows =
|
||||||
|
selectedLessonId === "ALL" ? rows : rows.filter((lesson) => lesson.id === selectedLessonId)
|
||||||
|
return (
|
||||||
<Card key={level} className="overflow-hidden border-grayScale-200/80 shadow-sm">
|
<Card key={level} className="overflow-hidden border-grayScale-200/80 shadow-sm">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -183,16 +201,16 @@ export function HumanLanguagePage() {
|
||||||
{collapsedLevels.includes(level) ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
{collapsedLevels.includes(level) ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
<span className="text-sm font-semibold text-grayScale-900">{level}</span>
|
<span className="text-sm font-semibold text-grayScale-900">{level}</span>
|
||||||
<span className="rounded-md bg-brand-100 px-2 py-0.5 text-xs font-medium text-brand-700">
|
<span className="rounded-md bg-brand-100 px-2 py-0.5 text-xs font-medium text-brand-700">
|
||||||
{rows.length} unit(s)
|
{filteredRows.length} unit(s)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{!collapsedLevels.includes(level) ? (
|
{!collapsedLevels.includes(level) ? (
|
||||||
<CardContent className="space-y-3 p-4">
|
<CardContent className="space-y-3 p-4">
|
||||||
{rows.length === 0 ? (
|
{filteredRows.length === 0 ? (
|
||||||
<p className="text-sm text-grayScale-500">No lessons found for this level.</p>
|
<p className="text-sm text-grayScale-500">No lessons found for this level.</p>
|
||||||
) : (
|
) : (
|
||||||
rows.map((lesson) => (
|
filteredRows.map((lesson) => (
|
||||||
<div key={lesson.id} className="rounded-xl border border-grayScale-200 bg-white p-3">
|
<div key={lesson.id} className="rounded-xl border border-grayScale-200 bg-white p-3">
|
||||||
<p className="text-sm font-semibold text-grayScale-900">{lesson.title}</p>
|
<p className="text-sm font-semibold text-grayScale-900">{lesson.title}</p>
|
||||||
<p className="mt-1 text-xs text-grayScale-500">
|
<p className="mt-1 text-xs text-grayScale-500">
|
||||||
|
|
@ -212,7 +230,8 @@ export function HumanLanguagePage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
) : null}
|
) : null}
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user