Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-11 12:12:40 +08:00
parent 01a2c63b06
commit 71f758fe0b
66 changed files with 3055 additions and 939 deletions
@@ -0,0 +1,133 @@
// modules/admin/components/courses/AchievementsBuilder.jsx
// The "Achievements" sub-section of the Rewards step: pick an existing
// achievement from the registry or define a new one — same New/Attach
// pattern as RoadmapBuilder's Units section. A course carries at most one
// achievement.
import { useState } from "react";
import * as LucideIcons from "lucide-react";
import { Plus, Link2, Trophy, BadgeCheck, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import AttachAchievementDialog from "./AttachAchievementDialog";
import CreateAchievementDialog from "./CreateAchievementDialog";
export default function AchievementsBuilder({ achievementKeys, onAchievementKeysChange, registry, onRegistryChange }) {
const [attachOpen, setAttachOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
// Gate the whole New/Attach builder behind an explicit yes/no — don't
// assume every course wants an achievement. Starts open if a course being
// edited already has one selected.
const [wantsAchievement, setWantsAchievement] = useState(achievementKeys.length > 0);
const selected = registry.find((a) => a.key === achievementKeys[0]) ?? null;
const handleAttach = (key) => onAchievementKeysChange([key]);
const handleCreated = (achievement) => {
onRegistryChange([...registry, achievement]);
onAchievementKeysChange([achievement.key]);
};
const declineAchievement = () => {
onAchievementKeysChange([]);
setWantsAchievement(false);
};
if (!wantsAchievement) {
return (
<div className="border-t pt-4">
<div className="rounded-md border border-dashed px-4 py-5 flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium flex items-center gap-1.5">
<Trophy className="h-3.5 w-3.5 text-muted-foreground" />
Award an achievement for completing this course?
</p>
<p className="text-xs text-muted-foreground mt-0.5">
Optional — learners can earn a badge or milestone for finishing this course.
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<Button type="button" variant="outline" size="sm" onClick={declineAchievement}>
No
</Button>
<Button type="button" size="sm" onClick={() => setWantsAchievement(true)}>
Yes, add one
</Button>
</div>
</div>
</div>
);
}
return (
<div className="border-t pt-4">
<div className="flex items-start justify-between gap-3 pb-3 mb-3 border-b">
<div className="space-y-0.5">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Achievements</p>
<p className="text-xs text-muted-foreground">
Attach an existing achievement from the registry, or define a new one from scratch.
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<Button type="button" variant="outline" size="sm" onClick={() => setCreateOpen(true)}>
<Plus className="h-3.5 w-3.5 mr-1.5" /> New Achievement
</Button>
<Button type="button" variant="outline" size="sm" onClick={() => setAttachOpen(true)}>
<Link2 className="h-3.5 w-3.5 mr-1.5" /> Attach
</Button>
</div>
</div>
{!selected ? (
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
<Trophy className="h-7 w-7 text-muted-foreground" />
<p className="text-sm font-medium">No achievement selected</p>
<Button type="button" variant="ghost" size="sm" className="text-muted-foreground" onClick={declineAchievement}>
Actually, skip achievements
</Button>
</div>
) : (
<div className="rounded-md border px-3 py-2.5 flex items-center gap-3">
{(() => {
const Icon = LucideIcons[selected.icon] ?? Trophy;
return <Icon className="h-4 w-4 text-muted-foreground shrink-0" />;
})()}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{selected.label}</p>
{selected.description && (
<p className="text-xs text-muted-foreground truncate">{selected.description}</p>
)}
</div>
<Badge variant="outline" className="text-[10px] shrink-0 capitalize gap-1">
{selected.type === "badge" ? <Trophy className="h-2.5 w-2.5" /> : <BadgeCheck className="h-2.5 w-2.5" />}
{selected.type}
</Badge>
<Button
type="button"
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-destructive shrink-0 h-7 w-7"
onClick={() => onAchievementKeysChange([])}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
<AttachAchievementDialog
open={attachOpen}
onOpenChange={setAttachOpen}
registry={registry}
selectedKey={achievementKeys[0] ?? null}
onAttach={handleAttach}
/>
<CreateAchievementDialog
open={createOpen}
onOpenChange={setCreateOpen}
onCreated={handleCreated}
/>
</div>
);
}