Files
starr-philproperties/src/modules/admin/components/courses/AchievementsBuilder.jsx
T
2026-08-03 12:03:11 +08:00

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>
);
}