mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user