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:
Yared Yemane 2026-04-07 05:55:21 -07:00
parent e477595578
commit 882db5444d
2 changed files with 42 additions and 24 deletions

View File

@ -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" },
] ]

View File

@ -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>