feat: Add web admin page for quest management and refactor Discord bot's quest UI to use new components.

This commit is contained in:
syntaxbullet
2026-01-15 17:21:49 +01:00
parent 9e5c6b5ac3
commit 2f73f38877
12 changed files with 552 additions and 94 deletions

View File

@@ -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>

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

View 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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {