mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
104 lines
3.9 KiB
React
104 lines
3.9 KiB
React
// 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);
|
|
|
|
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([]);
|
|
};
|
|
|
|
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">
|
|
<h2 className="text-sm font-semibold">Achievements</h2>
|
|
<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={() => setAttachOpen(true)}>
|
|
<Link2 /> Select
|
|
</Button>
|
|
<Button type="button" variant="outline" size="sm" onClick={() => setCreateOpen(true)}>
|
|
<Plus /> Create
|
|
</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>
|
|
);
|
|
}
|