289 lines
14 KiB
TypeScript
289 lines
14 KiB
TypeScript
import React from "react";
|
|
import { toast } from "sonner";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
|
import { Badge } from "./ui/badge";
|
|
import { Skeleton } from "./ui/skeleton";
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
import { cn } from "../lib/utils";
|
|
import { FileText, RefreshCw, Trash2, Pencil, Star, Coins } from "lucide-react";
|
|
|
|
interface QuestListItem {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
triggerEvent: string;
|
|
requirements: { target?: number };
|
|
rewards: { xp?: number; balance?: number };
|
|
}
|
|
|
|
interface QuestTableProps {
|
|
quests: QuestListItem[];
|
|
isInitialLoading: boolean;
|
|
isRefreshing: boolean;
|
|
onRefresh?: () => void;
|
|
onDelete?: (id: number) => void;
|
|
onEdit?: (id: number) => void;
|
|
}
|
|
|
|
const TRIGGER_EVENT_LABELS: Record<string, string> = {
|
|
XP_GAIN: "XP Gain",
|
|
ITEM_COLLECT: "Item Collect",
|
|
ITEM_USE: "Item Use",
|
|
DAILY_REWARD: "Daily Reward",
|
|
LOOTBOX: "Lootbox Currency Reward",
|
|
EXAM_REWARD: "Exam Reward",
|
|
PURCHASE: "Purchase",
|
|
TRANSFER_IN: "Transfer In",
|
|
TRANSFER_OUT: "Transfer Out",
|
|
TRADE_IN: "Trade In",
|
|
TRADE_OUT: "Trade Out",
|
|
QUEST_REWARD: "Quest Reward",
|
|
TRIVIA_ENTRY: "Trivia Entry",
|
|
TRIVIA_WIN: "Trivia Win",
|
|
};
|
|
|
|
function getTriggerEventLabel(triggerEvent: string): string {
|
|
return TRIGGER_EVENT_LABELS[triggerEvent] || triggerEvent;
|
|
}
|
|
|
|
function TruncatedText({ text, maxLength = 100 }: { text: string; maxLength?: number }) {
|
|
if (!text || text.length <= maxLength) {
|
|
return <span>{text || "-"}</span>;
|
|
}
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="cursor-help border-b border-dashed border-muted-foreground/50">
|
|
{text.slice(0, maxLength)}...
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent className="max-w-md">
|
|
<p>{text}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
function QuestTableSkeleton() {
|
|
return (
|
|
<div className="space-y-3 animate-pulse">
|
|
<div className="grid grid-cols-8 gap-4 px-4 py-2 text-sm font-medium text-muted-foreground">
|
|
<Skeleton className="h-4 w-8" />
|
|
<Skeleton className="h-4 w-32" />
|
|
<Skeleton className="h-4 w-48" />
|
|
<Skeleton className="h-4 w-24" />
|
|
<Skeleton className="h-4 w-16" />
|
|
<Skeleton className="h-4 w-24" />
|
|
<Skeleton className="h-4 w-24" />
|
|
</div>
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="grid grid-cols-8 gap-4 px-4 py-3 border-t border-border/50">
|
|
<Skeleton className="h-5 w-8" />
|
|
<Skeleton className="h-5 w-32" />
|
|
<Skeleton className="h-5 w-48" />
|
|
<Skeleton className="h-5 w-24" />
|
|
<Skeleton className="h-5 w-16" />
|
|
<Skeleton className="h-5 w-24" />
|
|
<Skeleton className="h-5 w-24" />
|
|
<Skeleton className="h-5 w-16" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EmptyQuestState() {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center animate-in fade-in duration-500">
|
|
<div className="rounded-full bg-muted/50 p-4 mb-4">
|
|
<FileText className="w-8 h-8 text-muted-foreground" />
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-foreground">No quests available</h3>
|
|
<p className="text-muted-foreground mt-1 max-w-sm">
|
|
There are no quests in the database yet. Create your first quest using the form below.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function QuestTableContent({ quests, onDelete, onEdit }: { quests: QuestListItem[]; onDelete?: (id: number) => void; onEdit?: (id: number) => void }) {
|
|
if (quests.length === 0) {
|
|
return <EmptyQuestState />;
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-x-auto animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="border-b border-border/50">
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground w-16">
|
|
ID
|
|
</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground w-40">
|
|
Name
|
|
</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground w-64">
|
|
Description
|
|
</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground w-36">
|
|
Trigger Event
|
|
</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground w-20">
|
|
Target
|
|
</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground w-32">
|
|
XP Reward
|
|
</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground w-32">
|
|
AU Reward
|
|
</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-muted-foreground w-24">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{quests.map((quest) => {
|
|
const requirements = quest.requirements as { target?: number };
|
|
const rewards = quest.rewards as { xp?: number; balance?: number };
|
|
const target = requirements?.target || 1;
|
|
|
|
return (
|
|
<tr
|
|
key={quest.id}
|
|
id={`quest-row-${quest.id}`}
|
|
className="border-b border-border/30 hover:bg-muted/20 transition-colors animate-in fade-in slide-in-from-left-2 duration-300"
|
|
>
|
|
<td className="py-3 px-4 text-sm text-muted-foreground font-mono">
|
|
#{quest.id}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm font-medium text-foreground">
|
|
{quest.name}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-muted-foreground">
|
|
<TruncatedText text={quest.description || ""} maxLength={50} />
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
<Badge variant="outline" className="text-xs border-border/50">
|
|
{getTriggerEventLabel(quest.triggerEvent)}
|
|
</Badge>
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-foreground font-mono">
|
|
{target}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-foreground">
|
|
{rewards?.xp ? (
|
|
<span className="flex items-center gap-1">
|
|
<Star className="w-4 h-4 text-amber-400" />
|
|
<span className="font-mono">{rewards.xp}</span>
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground">-</span>
|
|
)}
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-foreground">
|
|
{rewards?.balance ? (
|
|
<span className="flex items-center gap-1">
|
|
<Coins className="w-4 h-4 text-amber-500" />
|
|
<span className="font-mono">{rewards.balance}</span>
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground">-</span>
|
|
)}
|
|
</td>
|
|
<td className="py-3 px-4">
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => onEdit?.(quest.id)}
|
|
className="p-1.5 rounded-md hover:bg-muted/50 transition-colors text-muted-foreground hover:text-foreground"
|
|
title="Edit quest"
|
|
>
|
|
<Pencil className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
toast("Delete this quest?", {
|
|
description: "This action cannot be undone.",
|
|
action: {
|
|
label: "Delete",
|
|
onClick: () => onDelete?.(quest.id)
|
|
},
|
|
cancel: {
|
|
label: "Cancel",
|
|
onClick: () => {}
|
|
},
|
|
style: {
|
|
background: "var(--destructive)",
|
|
color: "var(--destructive-foreground)"
|
|
},
|
|
actionButtonStyle: {
|
|
background: "var(--destructive)",
|
|
color: "var(--destructive-foreground)"
|
|
}
|
|
});
|
|
}}
|
|
className="p-1.5 rounded-md hover:bg-muted/50 transition-colors text-muted-foreground hover:text-destructive"
|
|
title="Delete quest"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function QuestTable({ quests, isInitialLoading, isRefreshing, onRefresh, onDelete, onEdit }: QuestTableProps) {
|
|
const showSkeleton = isInitialLoading && quests.length === 0;
|
|
|
|
return (
|
|
<Card className="glass-card overflow-hidden">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-xl font-bold text-primary">Quest Inventory</CardTitle>
|
|
<div className="flex items-center gap-3">
|
|
{showSkeleton ? (
|
|
<Badge variant="secondary" className="animate-pulse">
|
|
Loading...
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="outline" className="border-border/50">
|
|
{quests.length} quest{quests.length !== 1 ? "s" : ""}
|
|
</Badge>
|
|
)}
|
|
<button
|
|
onClick={onRefresh}
|
|
disabled={isRefreshing}
|
|
className={cn(
|
|
"p-2 rounded-md hover:bg-muted/50 transition-colors",
|
|
isRefreshing && "cursor-wait"
|
|
)}
|
|
title="Refresh quests"
|
|
>
|
|
<RefreshCw className={cn(
|
|
"w-[18px] h-[18px] text-muted-foreground transition-transform",
|
|
isRefreshing && "animate-spin"
|
|
)} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{showSkeleton ? (
|
|
<QuestTableSkeleton />
|
|
) : (
|
|
<QuestTableContent quests={quests} onDelete={onDelete} onEdit={onEdit} />
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|