mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
173 lines
7.9 KiB
React
173 lines
7.9 KiB
React
import { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import * as LucideIcons from "lucide-react";
|
|
import { House, Plus, Pencil, Trash2, Trophy, Lock } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
|
import { PageMeta } from "@/contexts/MetadataContext";
|
|
import {
|
|
AdminAchievementsProvider,
|
|
useAdminAchievements,
|
|
} from "@/contexts/AdminAchievementsContext";
|
|
|
|
function AchievementCard({ item, onEdit, onDelete }) {
|
|
const Icon = LucideIcons[item.icon] ?? Trophy;
|
|
return (
|
|
<div className="rounded-lg border bg-card p-5 flex items-start justify-between gap-4">
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-12 h-12 rounded-lg border bg-muted flex items-center justify-center shrink-0">
|
|
<Icon className="h-5 w-5 text-muted-foreground" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<p className="text-sm font-semibold">{item.label}</p>
|
|
<code className="text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground">{item.key}</code>
|
|
<Badge variant="outline" className="text-[10px] capitalize">{item.type}</Badge>
|
|
{!item.is_active && <Badge variant="secondary">Inactive</Badge>}
|
|
{item.is_system && (
|
|
<Badge variant="secondary" className="gap-1">
|
|
<Lock className="h-2.5 w-2.5" /> System
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
{item.description && (
|
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">{item.description}</p>
|
|
)}
|
|
{item.trigger && (
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
Trigger: <span className="font-medium text-foreground capitalize">{item.trigger}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
<Button type="button" variant="ghost" size="icon" onClick={() => onEdit(item)}>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
{!item.is_system && (
|
|
<Button type="button" variant="ghost" size="icon" onClick={() => onDelete(item)}>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AchievementsInner() {
|
|
const navigate = useNavigate();
|
|
const { achievements, loading, fetchAchievements, deleteAchievement } = useAdminAchievements();
|
|
|
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
const [deleting, setDeleting] = useState(false);
|
|
|
|
useEffect(() => { fetchAchievements(); }, []);
|
|
|
|
const confirmDelete = async () => {
|
|
if (!deleteTarget) return;
|
|
setDeleting(true);
|
|
await deleteAchievement(deleteTarget.achievement_definition_id);
|
|
setDeleting(false);
|
|
setDeleteTarget(null);
|
|
};
|
|
|
|
return (
|
|
<section className="bg-muted/60 min-h-full">
|
|
<PageMeta title="Achievements - STARR" />
|
|
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
|
<div className="w-full max-w-2xl mx-auto">
|
|
|
|
<div className="flex flex-col gap-2 mb-6">
|
|
<AppBreadcrumb items={[
|
|
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
|
|
{ label: "Achievements" },
|
|
]} />
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-xl font-semibold">Achievements</h1>
|
|
<p className="text-sm text-muted-foreground mt-0.5">
|
|
Badges and milestones learners can earn across the platform.
|
|
</p>
|
|
</div>
|
|
<Button size="sm" onClick={() => navigate("/admin/achievements/add")}>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add Achievement
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="rounded-lg border bg-card p-4 flex items-start gap-3 mb-5">
|
|
<Lock className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
<p className="text-xs text-muted-foreground">
|
|
<strong>System</strong> achievements are auto-granted by platform events (registration, course completion, etc.)
|
|
and cannot be deleted or have their key/type changed — everything else stays editable.
|
|
</p>
|
|
</div>
|
|
|
|
<Separator className="mb-5" />
|
|
|
|
{loading && !achievements.length ? (
|
|
<div className="flex justify-center py-12"><Spinner className="h-5 w-5" /></div>
|
|
) : !achievements.length ? (
|
|
<p className="text-sm text-muted-foreground text-center py-12">No achievements found.</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{achievements.map((item) => (
|
|
<AchievementCard
|
|
key={item.achievement_definition_id}
|
|
item={item}
|
|
onEdit={(a) => navigate(`/admin/achievements/${a.achievement_definition_id}/edit`)}
|
|
onDelete={(a) => setDeleteTarget(a)}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delete confirmation dialog */}
|
|
<Dialog open={!!deleteTarget} onOpenChange={(open) => { if (!open) setDeleteTarget(null); }}>
|
|
<DialogContent className="sm:max-w-sm">
|
|
<DialogHeader>
|
|
<DialogTitle>Delete Achievement</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete{" "}
|
|
<span className="font-semibold text-foreground">{deleteTarget?.label}</span>?
|
|
This action cannot be undone. Any courses referencing this achievement must be unassigned first.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setDeleteTarget(null)} disabled={deleting}>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="destructive" onClick={confirmDelete} disabled={deleting}>
|
|
{deleting && <Spinner className="h-4 w-4 mr-2" />}
|
|
Delete
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export default function Achievements() {
|
|
return (
|
|
<AdminAchievementsProvider>
|
|
<AchievementsInner />
|
|
</AdminAchievementsProvider>
|
|
);
|
|
}
|