mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -1,15 +1,18 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
ArrowLeft, House, Pencil, Clock, BookOpen, Layers,
|
||||
ArrowLeft, Pencil, Clock, BookOpen, Layers,
|
||||
BadgeCheck, Tag, Star, Lock, ListChecks, BarChart2,
|
||||
Trophy, Users, Award,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import CourseReadingProgressList from "../../components/courses/CourseReadingProgressList";
|
||||
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||||
import CourseBadge from "@/modules/admin/components/courses/CourseBadge";
|
||||
import { ACHIEVEMENT_REGISTRY } from "@/utils/achievements.data";
|
||||
import api from "@/utils/api.util";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -71,10 +74,43 @@ export default function ViewCourse() {
|
||||
const { fetchCourse, course, loading } = useCourses();
|
||||
const { fmtDateTime } = useDateFormat();
|
||||
|
||||
const [instructors, setInstructors] = useState([]);
|
||||
const [achievementKeys, setAchievementKeys] = useState([]);
|
||||
const [badgeImageUrl, setBadgeImageUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCourse(courseId);
|
||||
|
||||
// Instructors
|
||||
api.get(`/admin/courses/${courseId}/instructors`)
|
||||
.then(({ data }) => setInstructors(data.data?.data ?? data.data ?? []))
|
||||
.catch(() => {});
|
||||
|
||||
// Achievements
|
||||
api.get(`/admin/courses/${courseId}/achievements`)
|
||||
.then(({ data }) => {
|
||||
const rows = data?.data?.data ?? [];
|
||||
setAchievementKeys(rows.map((r) => r.achievement_key));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [courseId]);
|
||||
|
||||
// Fresh stream token for the badge image once course loads
|
||||
useEffect(() => {
|
||||
if (!course?.badge_asset_id) {
|
||||
setBadgeImageUrl(course?.badge_image_url ?? null);
|
||||
return;
|
||||
}
|
||||
const STREAM_BASE = `${import.meta.env.VITE_API_URL}/client/media/stream`;
|
||||
api.post("/admin/media/token", { asset_id: course.badge_asset_id })
|
||||
.then(({ data }) => {
|
||||
const thumbUrl = data.data?.thumbnail_url;
|
||||
const token = data.data?.token;
|
||||
setBadgeImageUrl(thumbUrl ?? (token ? `${STREAM_BASE}/${token}` : null));
|
||||
})
|
||||
.catch(() => setBadgeImageUrl(course.badge_image_url ?? null));
|
||||
}, [course?.badge_asset_id, course?.badge_image_url]);
|
||||
|
||||
return (
|
||||
<section className="bg-muted/60 min-h-full">
|
||||
<PageMeta title={course ? `${course.title} - STARR` : undefined} description={course?.description} />
|
||||
@@ -155,9 +191,7 @@ export default function ViewCourse() {
|
||||
<SectionCard icon={Clock} title="Duration & Stats">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<InfoRow label="Duration">
|
||||
{course.duration_formatted ?? course.duration_seconds
|
||||
? `${course.duration_seconds}s`
|
||||
: "—"}
|
||||
{course.duration_formatted ?? (course.duration_seconds ? `${course.duration_seconds}s` : "—")}
|
||||
</InfoRow>
|
||||
<InfoRow label="Units">
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
@@ -188,6 +222,102 @@ export default function ViewCourse() {
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* ── Instructors ── */}
|
||||
<SectionCard icon={Users} title="Course Instructors">
|
||||
{instructors.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground italic">No instructors assigned.</p>
|
||||
) : (
|
||||
<ul className="space-y-3">
|
||||
{instructors.map((inst, i) => {
|
||||
const fullName = inst.user?.personal_info?.name?.full_name ?? null;
|
||||
const email = inst.user?.email ?? null;
|
||||
return (
|
||||
<li key={inst.id ?? i} className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center shrink-0">
|
||||
<Users className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium leading-tight">{inst.display_name}</p>
|
||||
{(fullName || email) && (
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{fullName ?? email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="ml-auto text-[10px] shrink-0">
|
||||
#{i + 1}
|
||||
</Badge>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
{/* ── Rewards ── */}
|
||||
<SectionCard icon={Award} title="Rewards">
|
||||
<div className="space-y-5">
|
||||
|
||||
{/* Completion badge */}
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-3">Completion Badge</p>
|
||||
<div className="flex items-start gap-4">
|
||||
<CourseBadge
|
||||
title={course.title}
|
||||
level={course.level}
|
||||
color={course.badge_color ?? "purple"}
|
||||
imageUrl={badgeImageUrl}
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5 text-xs text-muted-foreground pt-1">
|
||||
<div><span className="font-medium text-foreground">Label:</span> Course Completion</div>
|
||||
<div><span className="font-medium text-foreground">Trigger:</span> Pass course assessment</div>
|
||||
<div><span className="font-medium text-foreground">Type:</span> Milestone achievement</div>
|
||||
<div><span className="font-medium text-foreground">Color:</span> <span className="capitalize">{course.badge_color ?? "purple"}</span></div>
|
||||
<Badge className="self-start mt-1 bg-green-100 text-green-700 border-green-400 dark:bg-green-900/40 dark:text-green-400 dark:border-green-700 gap-1 text-xs">
|
||||
<BadgeCheck className="size-3" /> Mandatory
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Achievements */}
|
||||
<div className="border-t pt-4">
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-3">
|
||||
Achievements
|
||||
<span className="ml-2 normal-case font-normal">({achievementKeys.length}/3)</span>
|
||||
</p>
|
||||
{achievementKeys.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground italic">No achievements assigned.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{achievementKeys.map((key) => {
|
||||
const ach = ACHIEVEMENT_REGISTRY.find((a) => a.key === key);
|
||||
return (
|
||||
<li key={key} className="flex items-start gap-2.5 text-sm">
|
||||
<div className="mt-0.5 shrink-0">
|
||||
{ach?.type === "badge"
|
||||
? <Trophy className="h-4 w-4 text-amber-500" />
|
||||
: <BadgeCheck className="h-4 w-4 text-primary" />
|
||||
}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<span className="font-medium">{ach?.label ?? key}</span>
|
||||
{ach?.description && (
|
||||
<p className="text-xs text-muted-foreground leading-snug mt-0.5">{ach.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="ml-auto text-[9px] capitalize shrink-0 mt-0.5">
|
||||
{ach?.type ?? "badge"}
|
||||
</Badge>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{/* ── Prerequisites ── */}
|
||||
{course.prerequisites?.length > 0 && (
|
||||
<SectionCard icon={Star} title="Prerequisites">
|
||||
@@ -207,11 +337,11 @@ export default function ViewCourse() {
|
||||
<SectionCard icon={Lock} title="Final Assessment">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InfoRow label="Title">{course.assessment.title ?? "Untitled Assessment"}</InfoRow>
|
||||
<InfoRow label="Required">
|
||||
{/* <InfoRow label="Required">
|
||||
<Badge variant={course.assessment.is_required ? "default" : "secondary"}>
|
||||
{course.assessment.is_required ? "Required" : "Optional"}
|
||||
</Badge>
|
||||
</InfoRow>
|
||||
</InfoRow> */}
|
||||
<InfoRow label="Passing Score">{course.assessment.passing_score ?? 70}%</InfoRow>
|
||||
<InfoRow label="Time Limit">
|
||||
{course.assessment.time_limit_minutes
|
||||
@@ -247,4 +377,4 @@ export default function ViewCourse() {
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user