added docs and fixed level bridger
This commit is contained in:
@@ -3,6 +3,8 @@ import { redirect, notFound } from "next/navigation";
|
||||
import { getUserGuilds } from "@/lib/discord";
|
||||
import ServerSwitcher from "@/components/ServerSwitcher";
|
||||
import DashboardSidebar from "@/components/DashboardSidebar";
|
||||
import UserProfile from "@/components/UserProfile";
|
||||
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
@@ -35,18 +37,8 @@ export default async function DashboardLayout({
|
||||
<ServerSwitcher currentGuild={currentGuild} guilds={guilds} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 sm:gap-3 bg-white/5 px-3 sm:px-4 py-2 rounded-full border border-white/10 backdrop-blur-md pointer-events-auto">
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-sm font-semibold text-white">{session.user?.name}</p>
|
||||
</div>
|
||||
{session.user?.image && (
|
||||
<img
|
||||
src={session.user.image}
|
||||
alt="User"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 rounded-full border border-blue-500/50"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<UserProfile user={session.user} />
|
||||
|
||||
</header>
|
||||
|
||||
{/* Bot Not In Guild Content */}
|
||||
@@ -104,18 +96,8 @@ export default async function DashboardLayout({
|
||||
<ServerSwitcher currentGuild={currentGuild} guilds={guilds} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 sm:gap-3 bg-white/5 px-3 sm:px-4 py-2 rounded-full border border-white/10 backdrop-blur-md pointer-events-auto">
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-sm font-semibold text-white">{session.user?.name}</p>
|
||||
</div>
|
||||
{session.user?.image && (
|
||||
<img
|
||||
src={session.user.image}
|
||||
alt="User"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 rounded-full border border-blue-500/50"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<UserProfile user={session.user} />
|
||||
|
||||
</header>
|
||||
|
||||
{/* Dashboard Content with Sidebar */}
|
||||
@@ -123,7 +105,8 @@ export default async function DashboardLayout({
|
||||
<DashboardSidebar guildId={guildId} />
|
||||
|
||||
{/* Main Area */}
|
||||
<div className="flex-1 overflow-y-auto bg-transparent p-4 sm:p-6 lg:p-8">
|
||||
<div className="flex-1 overflow-y-auto bg-transparent p-4 sm:p-6 lg:p-8 lg:ml-64">
|
||||
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { redirect } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { getUserGuilds } from "@/lib/discord"
|
||||
import Toast from "@/components/Toast"
|
||||
import UserProfile from "@/components/UserProfile"
|
||||
|
||||
|
||||
export default async function Dashboard() {
|
||||
const session = await auth()
|
||||
@@ -21,19 +23,11 @@ export default async function Dashboard() {
|
||||
<div className="min-h-screen bg-black/50 relative">
|
||||
<Toast />
|
||||
{/* Header / User Corner */}
|
||||
<div className="absolute top-4 right-4 flex items-center gap-3 bg-white/5 px-4 py-2 rounded-full border border-white/10 backdrop-blur-md z-10">
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-sm font-semibold text-white">{session.user?.name}</p>
|
||||
</div>
|
||||
{session.user?.image && (
|
||||
<img
|
||||
src={session.user.image}
|
||||
alt="User"
|
||||
className="w-10 h-10 rounded-full border border-blue-500/50"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute top-4 right-4 z-50">
|
||||
<UserProfile user={session.user} />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="container mx-auto px-4 py-20 flex flex-col items-center">
|
||||
<div className="w-full max-w-4xl mb-8 p-4 bg-blue-500/10 border border-blue-500/20 rounded-lg text-center text-blue-200 text-sm">
|
||||
If you would like to be participating in the beta program of void sentinel, please send a DM to <span className="font-mono bg-blue-500/20 px-1 rounded">_void_x_</span> on discord.
|
||||
|
||||
421
web/app/docs/page.tsx
Normal file
421
web/app/docs/page.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Book,
|
||||
Bot,
|
||||
LayoutDashboard,
|
||||
MessageSquare,
|
||||
Shield,
|
||||
Terminal,
|
||||
Zap,
|
||||
Menu,
|
||||
X,
|
||||
ChevronRight,
|
||||
Sparkles
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: "getting-started",
|
||||
title: "Getting Started",
|
||||
icon: <Zap className="w-5 h-5" />,
|
||||
items: [
|
||||
{ id: "introduction", title: "Introduction" },
|
||||
{ id: "inviting-bot", title: "Inviting the Bot" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "commands",
|
||||
title: "Commands",
|
||||
icon: <Terminal className="w-5 h-5" />,
|
||||
items: [
|
||||
{ id: "leveling-commands", title: "Leveling" },
|
||||
{ id: "utility-commands", title: "Utility" },
|
||||
{ id: "fun-commands", title: "Fun & AI" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "dashboard",
|
||||
title: "Dashboard",
|
||||
icon: <LayoutDashboard className="w-5 h-5" />,
|
||||
items: [
|
||||
{ id: "leaderboard", title: "Leaderboard" },
|
||||
{ id: "leveling-system", title: "Leveling System" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function DocsPage() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState("introduction");
|
||||
|
||||
React.useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveSection(entry.target.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: "-100px 0px -80% 0px",
|
||||
}
|
||||
);
|
||||
|
||||
const ids = sections.flatMap((section) => section.items.map((item) => item.id));
|
||||
ids.forEach((id) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
observer.observe(element);
|
||||
}
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
setActiveSection(id);
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white selection:bg-blue-500/30">
|
||||
{/* Mobile Header */}
|
||||
<div className="lg:hidden sticky top-0 z-50 border-b border-white/10 bg-black/80 backdrop-blur-md px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 font-bold text-xl">
|
||||
<img src="/void_sentinel.png" alt="Void Sentinel Logo" className="w-6 h-6 rounded-full" />
|
||||
<span>Void Sentinel</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
className="p-2 hover:bg-white/5 rounded-lg transition-colors"
|
||||
>
|
||||
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 flex flex-col lg:flex-row gap-8 py-8">
|
||||
{/* Sidebar Navigation */}
|
||||
<aside className={cn(
|
||||
"lg:w-64 flex-shrink-0 fixed lg:sticky top-[4rem] lg:top-8 h-[calc(100vh-6rem)] lg:h-[calc(100vh-4rem)] overflow-y-auto z-40 bg-black lg:bg-transparent transition-transform duration-300 left-0 w-full lg:translate-x-0 border-r lg:border-r-0 border-white/10 lg:block p-4 lg:p-0",
|
||||
mobileMenuOpen ? "translate-x-0" : "-translate-x-full"
|
||||
)}>
|
||||
<div className="space-y-6">
|
||||
<div className="hidden lg:flex items-center gap-2 font-bold text-2xl mb-8 px-2">
|
||||
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
|
||||
<img src="/void_sentinel.png" alt="Void Sentinel Logo" className="w-8 h-8 rounded-full" />
|
||||
<span>Void Sentinel</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{sections.map((section) => (
|
||||
<div key={section.id} className="space-y-2">
|
||||
<div className="flex items-center gap-2 px-2 text-sm font-semibold text-gray-400 uppercase tracking-wider">
|
||||
{section.icon}
|
||||
{section.title}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{section.items.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => scrollToSection(item.id)}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2 rounded-lg text-sm transition-all duration-200 border border-transparent",
|
||||
activeSection === item.id
|
||||
? "bg-blue-500/10 text-blue-400 border-blue-500/20 shadow-[0_0_15px_rgba(59,130,246,0.1)]"
|
||||
: "text-gray-400 hover:text-white hover:bg-white/5"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="pt-6 mt-6 border-t border-white/10">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 min-w-0 lg:pl-8">
|
||||
<div className="max-w-3xl mx-auto space-y-16 pb-20">
|
||||
{/* Getting Started */}
|
||||
<section id="introduction" className="space-y-6 scroll-mt-24">
|
||||
<h1 className="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white via-blue-200 to-slate-400">
|
||||
Documentation
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 leading-relaxed">
|
||||
Welcome to the official documentation for Void Sentinel. A powerful, modern Discord bot designed to elevate your server's engagement through advanced leveling, utility tools, and AI-powered interactions.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="inviting-bot" className="space-y-4 scroll-mt-24 border-t border-white/10 pt-10">
|
||||
<h2 className="text-3xl font-bold flex items-center gap-3">
|
||||
Inviting the Bot
|
||||
</h2>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
Currently, Void Sentinel is in <span className="text-yellow-400 font-semibold">Beta</span>. Access is restricted to approved servers.
|
||||
</p>
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-xl p-4 flex gap-3">
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<Shield className="w-5 h-5 text-yellow-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-yellow-400">Restricted Access</h4>
|
||||
<p className="text-sm text-yellow-200/80 mt-1">
|
||||
To invite Void Sentinel to your server, you must request access by sending a DM to <span className="font-mono bg-black/30 px-1 rounded">_void_x_</span> on Discord. Once approved, you can use the invite link provided in the dashboard.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Commands */}
|
||||
<section id="leveling-commands" className="space-y-4 scroll-mt-24 border-t border-white/10 pt-10">
|
||||
<h2 className="text-3xl font-bold flex items-center gap-3">
|
||||
Leveling Commands
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<CommandCard
|
||||
command="/leaderboard"
|
||||
description="Displays the top 10 members in the server leaderboard. Generates a custom image with ranks, avatars, and XP progress."
|
||||
/>
|
||||
<CommandCard
|
||||
command="/set_levelup_message_channel"
|
||||
description="Sets the channel where level-up notifications will be sent."
|
||||
args={["channel: The target channel (optional, defaults to current)"]}
|
||||
permission="MANAGE_GUILD"
|
||||
/>
|
||||
<CommandCard
|
||||
command="/set_levelup_message"
|
||||
description="Sets the custom message template for level-up notifications."
|
||||
args={["message: Use {user.mention} and {user.level} as placeholders."]}
|
||||
permission="MANAGE_GUILD"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="utility-commands" className="space-y-4 scroll-mt-24 border-t border-white/10 pt-10">
|
||||
<h2 className="text-3xl font-bold flex items-center gap-3">
|
||||
Utility Commands
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
<CommandCard
|
||||
command="/auto_response"
|
||||
description="Create an automatic response trigger."
|
||||
args={[
|
||||
"msg: The trigger message",
|
||||
"response: The bot's response",
|
||||
"mention_reply: Whether to reply with a ping (default: false)"
|
||||
]}
|
||||
permission="MANAGE_MESSAGES"
|
||||
/>
|
||||
<CommandCard
|
||||
command="/view_auto_responses"
|
||||
description="List all configured auto-responses for the server."
|
||||
permission="MANAGE_MESSAGES"
|
||||
/>
|
||||
<CommandCard
|
||||
command="/delete_auto_response"
|
||||
description="Delete an existing auto-response."
|
||||
args={["msg: The trigger message to delete"]}
|
||||
permission="MANAGE_MESSAGES"
|
||||
/>
|
||||
<CommandCard
|
||||
command="/edit_auto_response"
|
||||
description="Edit an existing auto-response."
|
||||
args={[
|
||||
"msg: The trigger message",
|
||||
"new_response: New response text (optional)",
|
||||
"new_mention_reply: New reply setting (optional)"
|
||||
]}
|
||||
permission="MANAGE_MESSAGES"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="fun-commands" className="space-y-4 scroll-mt-24 border-t border-white/10 pt-10">
|
||||
<h2 className="text-3xl font-bold flex items-center gap-3">
|
||||
Fun & AI Commands
|
||||
</h2>
|
||||
|
||||
<div className="bg-blue-500/10 border border-blue-500/20 rounded-xl p-4 mb-6">
|
||||
<p className="text-sm text-blue-200">
|
||||
<strong className="text-blue-400">Note:</strong> AI-powered features are currently experimental and restricted.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<CommandCard
|
||||
command="/ai_chat"
|
||||
description="Toggle AI chat mode for the current channel. The bot will respond to messages in this channel."
|
||||
permission="BOT_OWNER"
|
||||
tag="Restricted"
|
||||
/>
|
||||
<CommandCard
|
||||
command="/urban"
|
||||
description="Search for a term on Urban Dictionary."
|
||||
args={["term: The word to search for"]}
|
||||
/>
|
||||
<CommandCard
|
||||
command="/say"
|
||||
description="Make the bot say something, optionally impersonating another user via webhook."
|
||||
args={[
|
||||
"msg: The message content",
|
||||
"user: User to impersonate (optional)"
|
||||
]}
|
||||
/>
|
||||
<CommandCard
|
||||
command="/summary"
|
||||
description="Summarizes a referenced message or the replied message using AI."
|
||||
args={["message: Link to message (or reply to one)"]}
|
||||
tag="Restricted"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Dashboard Guide */}
|
||||
|
||||
<section id="leaderboard" className="space-y-6 scroll-mt-24 border-t border-white/10 pt-10">
|
||||
<h2 className="text-3xl font-bold flex items-center gap-3">
|
||||
Leaderboard
|
||||
</h2>
|
||||
<p className="text-gray-300">
|
||||
View your server's leveling leaderboard through the dashboard. This list displays all members ranked by their accumulated XP and current level.
|
||||
</p>
|
||||
<div className="rounded-xl overflow-hidden border border-white/10 bg-black/20">
|
||||
<img
|
||||
src="/leaderboard_example.png"
|
||||
alt="Leaderboard Example"
|
||||
className="w-full h-auto opacity-90 hover:opacity-100 transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="leveling-system" className="space-y-6 scroll-mt-24 border-t border-white/10 pt-10">
|
||||
<h2 className="text-3xl font-bold flex items-center gap-3">
|
||||
Leveling System Setup
|
||||
</h2>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
The dashboard allows you to configure multi-track leveling systems. Here's how to set it up:
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6 space-y-3">
|
||||
<h3 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-500/20 text-blue-400 text-xs font-bold border border-blue-500/30">1</span>
|
||||
Create Leveling Roles
|
||||
</h3>
|
||||
<ul className="list-disc list-inside space-y-1 ml-1 text-gray-300">
|
||||
<li>Create all the level roles you intend to use in your Discord server first.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6 space-y-4">
|
||||
<h3 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-500/20 text-blue-400 text-xs font-bold border border-blue-500/30">2</span>
|
||||
Add to Multi-Track Leveling System
|
||||
</h3>
|
||||
<ul className="list-disc list-inside space-y-2 ml-1 text-gray-300">
|
||||
<li>Go to the dashboard and click on <strong>Leveling</strong> in the sidebar.</li>
|
||||
<li>In the <strong>Level Tracks</strong> section, click on <strong>Add New Track</strong>.</li>
|
||||
<li>Give your track a unique name (e.g., "Mage", "Warrior").</li>
|
||||
<li>Select an <strong>Initial Role</strong>. <span className="text-sm text-gray-400 block ml-6 mt-1 mb-2 border-l-2 border-white/10 pl-3 italic">This role identifies which track a user belongs to.</span></li>
|
||||
<li>Add your level roles and assign the specific levels they unlock at.</li>
|
||||
<li>Click <strong>Save Changes</strong> to apply your configuration.</li>
|
||||
</ul>
|
||||
<div className="text-sm text-blue-300/80 bg-blue-500/10 border border-blue-500/20 p-3 rounded-lg flex items-center gap-2">
|
||||
Free servers can have up to 4 tracks. Premium servers support up to 10 tracks.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-6 space-y-4">
|
||||
<h3 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-blue-500/20 text-blue-400 text-xs font-bold border border-blue-500/30">3</span>
|
||||
Set Level Bridgers
|
||||
</h3>
|
||||
<p className="text-gray-300 text-sm">
|
||||
Level Bridgers help restrict users to their initial track and integrate seamlessly with Discord onboarding or reaction roles.
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 ml-1 text-gray-300">
|
||||
<li>Select a <strong>Recruit Role</strong> (the role given by onboarding/reaction).</li>
|
||||
<li>Select the corresponding <strong>Initial Role</strong> defined in your Level Track.</li>
|
||||
<li>Click on <strong>Add Bridger</strong>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-xl p-4">
|
||||
<h4 className="flex items-center gap-2 font-semibold text-yellow-400 mb-2">
|
||||
<Shield className="w-4 h-4" />
|
||||
Changing Tracks
|
||||
</h4>
|
||||
<p className="text-sm text-yellow-200/80">
|
||||
If a user wants to switch to a different track, they must rest their progress. Use the command <code className="bg-black/30 px-1.5 py-0.5 rounded font-mono text-yellow-100">/level reset</code>.
|
||||
This will reset their level and XP to 0 and remove all roles associated with their current level track.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandCard({ command, description, args, permission, tag }: { command: string, description: string, args?: string[], permission?: string, tag?: string }) {
|
||||
return (
|
||||
<div className="group rounded-xl bg-white/5 border border-white/5 overflow-hidden hover:bg-white/[0.07] transition-all duration-300">
|
||||
<div className="p-4 border-b border-white/5 flex flex-wrap items-center justify-between gap-2 bg-black/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<code className="px-2.5 py-1 rounded-md bg-blue-500/20 text-blue-400 font-mono text-sm font-semibold">
|
||||
{command}
|
||||
</code>
|
||||
{permission && (
|
||||
<span className="px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider bg-red-500/10 text-red-400 border border-red-500/20">
|
||||
{permission}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tag && (
|
||||
<span className="px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider bg-yellow-500/10 text-yellow-400 border border-yellow-500/20">
|
||||
{tag}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
<p className="text-gray-300 text-sm leading-relaxed">{description}</p>
|
||||
{args && args.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Parameters</p>
|
||||
<ul className="space-y-1">
|
||||
{args.map((arg, i) => (
|
||||
<li key={i} className="text-xs text-gray-400 font-mono pl-2 border-l-2 border-white/10">
|
||||
{arg}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -3,7 +3,8 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Menu, X, Trophy, Sparkles } from "lucide-react";
|
||||
import { Menu, X, Trophy, Sparkles, BookMarked } from "lucide-react";
|
||||
|
||||
|
||||
interface DashboardSidebarProps {
|
||||
guildId: string;
|
||||
@@ -45,10 +46,14 @@ export default function DashboardSidebar({ guildId }: DashboardSidebarProps) {
|
||||
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={`fixed lg:relative inset-y-0 left-0 z-40 w-64 flex-shrink-0 bg-black/90 lg:bg-black/20 border-r border-white/10 overflow-y-auto backdrop-blur-md lg:backdrop-blur-sm transform transition-transform duration-300 ease-in-out ${sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
|
||||
className={`fixed inset-y-0 left-0 z-40 w-64 flex-shrink-0 bg-black/90 lg:bg-black/20 border-r border-white/10 backdrop-blur-md lg:backdrop-blur-sm transform transition-transform duration-300 ease-in-out flex flex-col ${sidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
|
||||
}`}
|
||||
>
|
||||
<div className="p-4 space-y-2 pt-20 lg:pt-4">
|
||||
|
||||
<div className="p-4 space-y-2 pt-20 lg:pt-24 flex-1 overflow-y-auto">
|
||||
|
||||
|
||||
|
||||
{menuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
@@ -60,8 +65,8 @@ export default function DashboardSidebar({ guildId }: DashboardSidebarProps) {
|
||||
href={href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg font-medium transition-all flex items-center gap-3 ${isActive
|
||||
? "bg-blue-600/20 text-blue-400 border border-blue-600/30"
|
||||
: "text-gray-400 hover:text-white hover:bg-white/5 border border-transparent"
|
||||
? "bg-blue-600/20 text-blue-400 border border-blue-600/30"
|
||||
: "text-gray-400 hover:text-white hover:bg-white/5 border border-transparent"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
@@ -70,7 +75,19 @@ export default function DashboardSidebar({ guildId }: DashboardSidebarProps) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Docs Link at Bottom */}
|
||||
<div className="p-4 mt-auto border-t border-white/10">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="w-full text-left px-4 py-3 rounded-lg font-medium transition-all flex items-center gap-3 text-gray-400 hover:text-white hover:bg-white/5 border border-transparent"
|
||||
>
|
||||
<BookMarked className="w-5 h-5" />
|
||||
Documentation
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ const Footer = () => {
|
||||
<div className="text-gray-400 text-sm">
|
||||
© {new Date().getFullYear()} Void Sentinel. All rights reserved.
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<a href="/docs" className="text-gray-400 hover:text-white transition-colors text-sm font-medium">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function LevelingEditor({ guildId }: LevelingEditorProps) {
|
||||
|
||||
// Level Bridger State
|
||||
const [bridges, setBridges] = useState<LevelBridge[]>([]);
|
||||
const [deletedBridges, setDeletedBridges] = useState<string[]>([]); // in_role_ids
|
||||
|
||||
const [newBridgeIn, setNewBridgeIn] = useState<string | null>(null);
|
||||
const [newBridgeOut, setNewBridgeOut] = useState<string | null>(null);
|
||||
const [showAddBridge, setShowAddBridge] = useState(false);
|
||||
@@ -118,6 +118,68 @@ export default function LevelingEditor({ guildId }: LevelingEditorProps) {
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Add bridge
|
||||
const handleAddBridge = async () => {
|
||||
if (!newBridgeIn || !newBridgeOut) return;
|
||||
if (newBridgeIn === newBridgeOut) {
|
||||
setError("Recruit Role and Initial Role cannot be the same");
|
||||
return;
|
||||
}
|
||||
|
||||
const bridge = {
|
||||
in_role_id: String(newBridgeIn),
|
||||
out_role_id: String(newBridgeOut)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/guilds/${guildId}/level_bridger`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(bridge),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || "Failed to add bridge");
|
||||
}
|
||||
|
||||
setBridges([...bridges, bridge]);
|
||||
setNewBridgeIn(null);
|
||||
setNewBridgeOut(null);
|
||||
setError(null);
|
||||
setSuccess("Bridge added successfully");
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to add bridge");
|
||||
}
|
||||
};
|
||||
|
||||
// Delete bridge
|
||||
const handleDeleteBridge = async (index: number) => {
|
||||
const bridge = bridges[index];
|
||||
const inRoleId = String(bridge.in_role_id);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/guilds/${guildId}/level_bridger`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ in_role_id: inRoleId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete bridge");
|
||||
}
|
||||
|
||||
const newBridges = [...bridges];
|
||||
newBridges.splice(index, 1);
|
||||
setBridges(newBridges);
|
||||
setSuccess("Bridge deleted successfully");
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to delete bridge");
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch tracks and roles
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -202,7 +264,7 @@ export default function LevelingEditor({ guildId }: LevelingEditorProps) {
|
||||
setDeletedTracks([]);
|
||||
|
||||
setBridges(Array.isArray(bridgesData) ? bridgesData : []);
|
||||
setDeletedBridges([]);
|
||||
|
||||
|
||||
// All tracks start collapsed
|
||||
} catch (err) {
|
||||
@@ -239,19 +301,6 @@ export default function LevelingEditor({ guildId }: LevelingEditorProps) {
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
// Level Bridger Deletions
|
||||
const deleteBridgePromises = deletedBridges.map(async (inRoleId) => {
|
||||
const response = await fetch(`/api/guilds/${guildId}/level_bridger`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ in_role_id: inRoleId }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to delete bridge for ${inRoleId}`);
|
||||
}
|
||||
});
|
||||
await Promise.all(deleteBridgePromises);
|
||||
|
||||
// Then, update/create each track with the new format:
|
||||
// { track_name: string, roles: [{ role_id: number, level: number }, ...] }
|
||||
const updatePromises = tracks.map(async (track) => {
|
||||
@@ -277,23 +326,9 @@ export default function LevelingEditor({ guildId }: LevelingEditorProps) {
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Level Bridger Updates (PUT)
|
||||
const updateBridgePromises = bridges.map(async (bridge) => {
|
||||
const response = await fetch(`/api/guilds/${guildId}/level_bridger`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(bridge),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to save bridge ${bridge.in_role_id} -> ${bridge.out_role_id}`);
|
||||
}
|
||||
});
|
||||
await Promise.all(updateBridgePromises);
|
||||
|
||||
// Update tracking state
|
||||
setOriginalTrackNames(new Set(tracks.map(t => t.name)));
|
||||
setDeletedTracks([]);
|
||||
setDeletedBridges([]);
|
||||
setSuccess("Changes saved successfully!");
|
||||
setHasChanges(false);
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
@@ -1054,13 +1089,7 @@ export default function LevelingEditor({ guildId }: LevelingEditorProps) {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const newBridges = [...bridges];
|
||||
newBridges.splice(index, 1);
|
||||
setBridges(newBridges);
|
||||
setDeletedBridges(prev => [...prev, (bridge as any).in_role_id || bridge.in_role_id]);
|
||||
setHasChanges(true);
|
||||
}}
|
||||
onClick={() => handleDeleteBridge(index)}
|
||||
className="p-2 text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded-lg transition-colors ml-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -1241,19 +1270,7 @@ export default function LevelingEditor({ guildId }: LevelingEditorProps) {
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (newBridgeIn && newBridgeOut) {
|
||||
if (newBridgeIn === newBridgeOut) {
|
||||
setError("Recruit Role and Initial Role cannot be the same");
|
||||
return;
|
||||
}
|
||||
setBridges([...bridges, { in_role_id: newBridgeIn, out_role_id: newBridgeOut }]);
|
||||
setNewBridgeIn(null);
|
||||
setNewBridgeOut(null);
|
||||
setHasChanges(true);
|
||||
setError(null);
|
||||
}
|
||||
}}
|
||||
onClick={handleAddBridge}
|
||||
disabled={!newBridgeIn || !newBridgeOut}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${newBridgeIn && newBridgeOut
|
||||
? "bg-blue-600 hover:bg-blue-700 text-white"
|
||||
|
||||
72
web/components/UserProfile.tsx
Normal file
72
web/components/UserProfile.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { LogOut, User, ChevronDown } from "lucide-react";
|
||||
import { signOut } from "next-auth/react";
|
||||
|
||||
interface UserProfileProps {
|
||||
user?: {
|
||||
name?: string | null;
|
||||
email?: string | null;
|
||||
image?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export default function UserProfile({ user }: UserProfileProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown on click outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative pointer-events-auto" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 sm:gap-3 bg-white/5 px-2 sm:px-4 py-2 rounded-full border border-white/10 backdrop-blur-md hover:bg-white/10 transition-colors group"
|
||||
>
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-sm font-semibold text-white group-hover:text-blue-200 transition-colors">
|
||||
{user?.name || "User"}
|
||||
</p>
|
||||
</div>
|
||||
{user?.image ? (
|
||||
<img
|
||||
src={user.image}
|
||||
alt={user.name || "User"}
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 rounded-full border border-blue-500/50 group-hover:border-blue-400 transition-colors"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full border border-blue-500/50 bg-gray-700 flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
<ChevronDown className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-48 bg-zinc-900 border border-white/10 rounded-xl shadow-xl overflow-hidden z-50 py-1 animate-in fade-in zoom-in-95 duration-100 ease-out">
|
||||
<div className="px-4 py-3 border-b border-white/10 sm:hidden">
|
||||
<p className="text-sm font-semibold text-white truncate">{user?.name || "User"}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: "/" })}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-red-400 hover:bg-white/5 hover:text-red-300 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
Log Out
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -269,9 +269,9 @@ export async function createLevelTrack(
|
||||
try {
|
||||
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
||||
|
||||
// WORKAROUND: Unquote role_id for backend (expects u64)
|
||||
// We kept it as string in frontend/NextJS to preserve precision.
|
||||
const bodyJson = JSON.stringify(data).replace(/"role_id":\s*"(\d+)"/g, '"role_id": $1');
|
||||
// REMOVED WORKAROUND: Backend now expects strings for u64 IDs
|
||||
const bodyJson = JSON.stringify(data);
|
||||
|
||||
|
||||
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, {
|
||||
method: "POST",
|
||||
@@ -303,8 +303,9 @@ export async function updateLevelTrack(
|
||||
try {
|
||||
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
||||
|
||||
// WORKAROUND: Unquote role_id for backend (expects u64)
|
||||
const bodyJson = JSON.stringify(data).replace(/"role_id":\s*"(\d+)"/g, '"role_id": $1');
|
||||
// REMOVED WORKAROUND: Backend now expects strings for u64 IDs
|
||||
const bodyJson = JSON.stringify(data);
|
||||
|
||||
|
||||
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level/track`, {
|
||||
method: "PATCH",
|
||||
@@ -400,8 +401,9 @@ export async function createLevelBridge(
|
||||
try {
|
||||
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
||||
|
||||
// WORKAROUND: Unquote role_id for backend (expects u64)
|
||||
const bodyJson = JSON.stringify(bridge).replace(/("in_role_id"|"out_role_id"):\s*"(\d+)"/g, '$1: $2');
|
||||
// REMOVED WORKAROUND: Backend now expects strings for u64 IDs
|
||||
const bodyJson = JSON.stringify(bridge);
|
||||
|
||||
|
||||
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, {
|
||||
method: "POST",
|
||||
@@ -432,7 +434,8 @@ export async function updateLevelBridge(
|
||||
try {
|
||||
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
||||
|
||||
const bodyJson = JSON.stringify(bridge).replace(/("in_role_id"|"out_role_id"):\s*"(\d+)"/g, '$1: $2');
|
||||
const bodyJson = JSON.stringify(bridge);
|
||||
|
||||
|
||||
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, {
|
||||
method: "PUT",
|
||||
@@ -463,8 +466,8 @@ export async function deleteLevelBridge(
|
||||
try {
|
||||
if (!process.env.BOT_API_URL) return { success: false, error: "Bot API URL not configured" };
|
||||
|
||||
// Even for delete, we might need to handle the u64 issue if the ID is passed in body
|
||||
const bodyJson = JSON.stringify({ in_role_id: inRoleId }).replace(/("in_role_id"):\s*"(\d+)"/g, '$1: $2');
|
||||
const bodyJson = JSON.stringify({ in_role_id: inRoleId });
|
||||
|
||||
|
||||
const response = await fetch(`${process.env.BOT_API_URL}/api/${guildId}/level_bridger`, {
|
||||
method: "DELETE",
|
||||
|
||||
BIN
web/public/leaderboard_example.png
Normal file
BIN
web/public/leaderboard_example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
Reference in New Issue
Block a user