forked from syntaxbullet/aurorabot
feat: Add web admin page for quest management and refactor Discord bot's quest UI to use new components.
This commit is contained in:
@@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { DesignSystem } from "./pages/DesignSystem";
|
||||
import { AdminQuests } from "./pages/AdminQuests";
|
||||
import { Home } from "./pages/Home";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
@@ -12,6 +13,7 @@ export function App() {
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/design-system" element={<DesignSystem />} />
|
||||
<Route path="/admin/quests" element={<AdminQuests />} />
|
||||
<Route path="/" element={<Home />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
230
web/src/components/quest-form.tsx
Normal file
230
web/src/components/quest-form.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "./ui/card";
|
||||
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from "./ui/form";
|
||||
import { Input } from "./ui/input";
|
||||
import { Button } from "./ui/button";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
import { toast } from "sonner";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
|
||||
const questSchema = z.object({
|
||||
name: z.string().min(3, "Name must be at least 3 characters"),
|
||||
description: z.string().optional(),
|
||||
triggerEvent: z.string().min(1, "Trigger event is required"),
|
||||
target: z.number().min(1, "Target must be at least 1"),
|
||||
xpReward: z.number().min(0).optional(),
|
||||
balanceReward: z.number().min(0).optional(),
|
||||
});
|
||||
|
||||
type QuestFormValues = z.infer<typeof questSchema>;
|
||||
|
||||
const TRIGGER_EVENTS = [
|
||||
{ label: "XP Gain", value: "XP_GAIN" },
|
||||
{ label: "Item Collect", value: "ITEM_COLLECT" },
|
||||
{ label: "Item Use", value: "ITEM_USE" },
|
||||
{ label: "Daily Reward", value: "DAILY_REWARD" },
|
||||
{ label: "Lootbox Currency Reward", value: "LOOTBOX" },
|
||||
{ label: "Exam Reward", value: "EXAM_REWARD" },
|
||||
{ label: "Purchase", value: "PURCHASE" },
|
||||
{ label: "Transfer In", value: "TRANSFER_IN" },
|
||||
{ label: "Transfer Out", value: "TRANSFER_OUT" },
|
||||
{ label: "Trade In", value: "TRADE_IN" },
|
||||
{ label: "Trade Out", value: "TRADE_OUT" },
|
||||
{ label: "Quest Reward", value: "QUEST_REWARD" },
|
||||
{ label: "Trivia Entry", value: "TRIVIA_ENTRY" },
|
||||
{ label: "Trivia Win", value: "TRIVIA_WIN" },
|
||||
];
|
||||
|
||||
export function QuestForm() {
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const form = useForm<QuestFormValues>({
|
||||
resolver: zodResolver(questSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
triggerEvent: "XP_GAIN",
|
||||
target: 1,
|
||||
xpReward: 100,
|
||||
balanceReward: 500,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: QuestFormValues) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const response = await fetch("/api/quests", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || "Failed to create quest");
|
||||
}
|
||||
|
||||
toast.success("Quest created successfully!", {
|
||||
description: `${data.name} has been added to the database.`,
|
||||
});
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
console.error("Submission error:", error);
|
||||
toast.error("Failed to create quest", {
|
||||
description: error instanceof Error ? error.message : "An unknown error occurred",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="glass-card max-w-2xl mx-auto overflow-hidden">
|
||||
<div className="h-1.5 bg-primary w-full" />
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold text-primary">Create New Quest</CardTitle>
|
||||
<CardDescription>
|
||||
Configure a new quest for the Aurora RPG academy.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Quest Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Collector's Journey" {...field} className="bg-background/50" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="triggerEvent"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Trigger Event</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="bg-background/50">
|
||||
<SelectValue placeholder="Select an event" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent className="glass-card border-border/50">
|
||||
<ScrollArea className="h-48">
|
||||
{TRIGGER_EVENTS.map((event) => (
|
||||
<SelectItem key={event.value} value={event.value}>
|
||||
{event.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Assigns a task to the student..."
|
||||
{...field}
|
||||
className="min-h-[100px] bg-background/50"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="target"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Target Value</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={e => field.onChange(parseInt(e.target.value))}
|
||||
className="bg-background/50"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="xpReward"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>XP Reward</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={e => field.onChange(parseInt(e.target.value))}
|
||||
className="bg-background/50"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="balanceReward"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>AU Reward</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
{...field}
|
||||
onChange={e => field.onChange(parseInt(e.target.value))}
|
||||
className="bg-background/50"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-primary text-primary-foreground hover:glow-primary active-press py-6 text-lg font-bold"
|
||||
>
|
||||
{isSubmitting ? "Creating..." : "Create Quest"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
59
web/src/pages/AdminQuests.tsx
Normal file
59
web/src/pages/AdminQuests.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { QuestForm } from "../components/quest-form";
|
||||
import { Badge } from "../components/ui/badge";
|
||||
import { SectionHeader } from "../components/section-header";
|
||||
import { SettingsDrawer } from "../components/settings-drawer";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
|
||||
export function AdminQuests() {
|
||||
return (
|
||||
<div className="min-h-screen bg-aurora-page text-foreground font-outfit overflow-x-hidden">
|
||||
{/* Navigation */}
|
||||
<nav className="sticky top-0 z-50 glass-card border-b border-border/50 py-4 px-8 flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-aurora sun-flare shadow-sm" />
|
||||
<span className="text-xl font-bold tracking-tight text-primary">Aurora Admin</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<Link to="/" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<Link to="/dashboard" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link to="/design-system" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Design System
|
||||
</Link>
|
||||
<div className="h-4 w-px bg-border/50" />
|
||||
<SettingsDrawer />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main className="pt-12 px-8 pb-12 max-w-7xl mx-auto space-y-12">
|
||||
<div className="space-y-4">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-primary transition-colors w-fit"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">Back to Dashboard</span>
|
||||
</Link>
|
||||
|
||||
<SectionHeader
|
||||
badge="Quest Management"
|
||||
title="Administrative Tools"
|
||||
description="Create and manage quests for the Aurora RPG students."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="animate-in fade-in slide-up duration-700">
|
||||
<QuestForm />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminQuests;
|
||||
@@ -58,6 +58,9 @@ export function Dashboard() {
|
||||
<Link to="/design-system" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Design System
|
||||
</Link>
|
||||
<Link to="/admin/quests" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Admin
|
||||
</Link>
|
||||
<div className="h-4 w-px bg-border/50" />
|
||||
<SettingsDrawer />
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { StatCard } from "../components/stat-card";
|
||||
import { LootdropCard } from "../components/lootdrop-card";
|
||||
import { Activity, Coins, Flame, Trophy } from "lucide-react";
|
||||
import { SettingsDrawer } from "../components/settings-drawer";
|
||||
import { QuestForm } from "../components/quest-form";
|
||||
|
||||
import { RecentActivity } from "../components/recent-activity";
|
||||
import { type RecentEvent } from "@shared/modules/dashboard/dashboard.types";
|
||||
@@ -98,6 +99,9 @@ export function DesignSystem() {
|
||||
<Link to="/dashboard" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link to="/admin/quests" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Admin
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -464,6 +468,17 @@ export function DesignSystem() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Administrative Tools Showcase */}
|
||||
<section className="space-y-6 animate-in slide-up delay-700">
|
||||
<h2 className="text-3xl font-bold flex items-center justify-center md:justify-start gap-2">
|
||||
<span className="w-8 h-8 rounded-full bg-primary inline-block" />
|
||||
Administrative Tools
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-6 text-left">
|
||||
<QuestForm />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Typography */}
|
||||
<section className="space-y-8 pb-12">
|
||||
<h2 className="text-step-3 font-bold text-center">Fluid Typography</h2>
|
||||
|
||||
@@ -31,6 +31,9 @@ export function Home() {
|
||||
<Link to="/design-system" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Design System
|
||||
</Link>
|
||||
<Link to="/admin/quests" className="text-step--1 font-medium text-muted-foreground hover:text-primary transition-colors">
|
||||
Admin
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -169,6 +169,34 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
||||
}
|
||||
}
|
||||
|
||||
// Quest Management
|
||||
if (url.pathname === "/api/quests" && req.method === "POST") {
|
||||
try {
|
||||
const { questService } = await import("@shared/modules/quest/quest.service");
|
||||
const data = await req.json();
|
||||
|
||||
// Basic validation could be added here or rely on service/DB
|
||||
const result = await questService.createQuest({
|
||||
name: data.name,
|
||||
description: data.description || "",
|
||||
triggerEvent: data.triggerEvent,
|
||||
requirements: { target: Number(data.target) || 1 },
|
||||
rewards: {
|
||||
xp: Number(data.xpReward) || 0,
|
||||
balance: Number(data.balanceReward) || 0
|
||||
}
|
||||
});
|
||||
|
||||
return Response.json({ success: true, quest: result[0] });
|
||||
} catch (error) {
|
||||
logger.error("web", "Error creating quest", error);
|
||||
return Response.json(
|
||||
{ error: "Failed to create quest", details: error instanceof Error ? error.message : String(error) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Settings Management
|
||||
if (url.pathname === "/api/settings") {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user