|
@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
|
|
|
import { Link } from "@/i18n/navigation";
|
|
import { Link } from "@/i18n/navigation";
|
|
|
import { CourseBuyButton } from "@/components/course-buy-button";
|
|
import { CourseBuyButton } from "@/components/course-buy-button";
|
|
|
import { InlineLoading, SkeletonBlock } from "@/components/ui/loading-state";
|
|
import { InlineLoading, SkeletonBlock } from "@/components/ui/loading-state";
|
|
|
-import { fetchCourses, type Course, type CourseCategory } from "@/data/courses";
|
|
|
|
|
|
|
+import { fetchCoursesPaged, type Course, type CourseCategory } from "@/data/courses";
|
|
|
|
|
|
|
|
type Props = {
|
|
type Props = {
|
|
|
active: CourseCategory | null;
|
|
active: CourseCategory | null;
|
|
@@ -16,21 +16,33 @@ function shouldShowBuyButton(course: Course): boolean {
|
|
|
return !(course.goodsType === 1 || course.customType === 2);
|
|
return !(course.goodsType === 1 || course.customType === 2);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function shouldShowFreeBadge(course: Course): boolean {
|
|
|
|
|
+ return course.category === "trial" && course.price === 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export function CoursesListClient({ active, initialCourses }: Props) {
|
|
export function CoursesListClient({ active, initialCourses }: Props) {
|
|
|
|
|
+ const pageSize = 10;
|
|
|
const [courses, setCourses] = useState(initialCourses);
|
|
const [courses, setCourses] = useState(initialCourses);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
+ const [page, setPage] = useState(1);
|
|
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
const t = useTranslations("courses");
|
|
const t = useTranslations("courses");
|
|
|
const tn = useTranslations("nav");
|
|
const tn = useTranslations("nav");
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
let cancelled = false;
|
|
let cancelled = false;
|
|
|
- void fetchCourses(active ?? undefined)
|
|
|
|
|
- .then((data) => {
|
|
|
|
|
|
|
+ void fetchCoursesPaged(active ?? undefined, { current: page, row: pageSize })
|
|
|
|
|
+ .then((result) => {
|
|
|
if (cancelled) return;
|
|
if (cancelled) return;
|
|
|
- setCourses(data);
|
|
|
|
|
|
|
+ setCourses(result.list);
|
|
|
|
|
+ setTotal(result.page.total);
|
|
|
})
|
|
})
|
|
|
.catch(() => {
|
|
.catch(() => {
|
|
|
// Keep server-rendered fallback data.
|
|
// Keep server-rendered fallback data.
|
|
|
|
|
+ if (!cancelled) {
|
|
|
|
|
+ setCourses([]);
|
|
|
|
|
+ setTotal(0);
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
.finally(() => {
|
|
.finally(() => {
|
|
|
if (!cancelled) setLoading(false);
|
|
if (!cancelled) setLoading(false);
|
|
@@ -38,7 +50,10 @@ export function CoursesListClient({ active, initialCourses }: Props) {
|
|
|
return () => {
|
|
return () => {
|
|
|
cancelled = true;
|
|
cancelled = true;
|
|
|
};
|
|
};
|
|
|
- }, [active]);
|
|
|
|
|
|
|
+ }, [active, page]);
|
|
|
|
|
+
|
|
|
|
|
+ const canPrev = page > 1 && !loading;
|
|
|
|
|
+ const canNext = !loading && page * pageSize < total;
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div>
|
|
<div>
|
|
@@ -73,7 +88,7 @@ export function CoursesListClient({ active, initialCourses }: Props) {
|
|
|
{c.title}
|
|
{c.title}
|
|
|
</h2>
|
|
</h2>
|
|
|
<div className="shrink-0 translate-y-px">
|
|
<div className="shrink-0 translate-y-px">
|
|
|
- {c.price === 0 ? (
|
|
|
|
|
|
|
+ {shouldShowFreeBadge(c) ? (
|
|
|
<span className="inline-flex w-fit items-center rounded-full bg-emerald-50 px-3 py-2 text-sm font-bold tracking-tight text-emerald-700 ring-1 ring-emerald-200/70">
|
|
<span className="inline-flex w-fit items-center rounded-full bg-emerald-50 px-3 py-2 text-sm font-bold tracking-tight text-emerald-700 ring-1 ring-emerald-200/70">
|
|
|
{t("free")}
|
|
{t("free")}
|
|
|
</span>
|
|
</span>
|
|
@@ -114,6 +129,33 @@ export function CoursesListClient({ active, initialCourses }: Props) {
|
|
|
</li>
|
|
</li>
|
|
|
))}
|
|
))}
|
|
|
</ul>
|
|
</ul>
|
|
|
|
|
+ <div className="mt-5 flex items-center justify-end gap-3">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ if (!canPrev) return;
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ setPage((p) => Math.max(1, p - 1));
|
|
|
|
|
+ }}
|
|
|
|
|
+ disabled={!canPrev}
|
|
|
|
|
+ className="rounded-full border border-slate-300 px-3 py-1.5 text-sm text-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
+ >
|
|
|
|
|
+ {t("videoPrevPage")}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <span className="text-sm text-slate-500">{t("videoCurrentPage", { page })}</span>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ if (!canNext) return;
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ setPage((p) => p + 1);
|
|
|
|
|
+ }}
|
|
|
|
|
+ disabled={!canNext}
|
|
|
|
|
+ className="rounded-full border border-slate-300 px-3 py-1.5 text-sm text-slate-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
+ >
|
|
|
|
|
+ {t("videoNextPage")}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|