If you would like to be participating in the beta program of void sentinel, please send a DM to
_void_x_ on discord.
diff --git a/web/app/docs/page.tsx b/web/app/docs/page.tsx
new file mode 100644
index 0000000..bc84b58
--- /dev/null
+++ b/web/app/docs/page.tsx
@@ -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:
,
+ items: [
+ { id: "introduction", title: "Introduction" },
+ { id: "inviting-bot", title: "Inviting the Bot" },
+ ],
+ },
+ {
+ id: "commands",
+ title: "Commands",
+ icon:
,
+ items: [
+ { id: "leveling-commands", title: "Leveling" },
+ { id: "utility-commands", title: "Utility" },
+ { id: "fun-commands", title: "Fun & AI" },
+ ],
+ },
+ {
+ id: "dashboard",
+ title: "Dashboard",
+ icon:
,
+ 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 (
+
+ {/* Mobile Header */}
+
+
+
+
Void Sentinel
+
+
setMobileMenuOpen(!mobileMenuOpen)}
+ className="p-2 hover:bg-white/5 rounded-lg transition-colors"
+ >
+ {mobileMenuOpen ? : }
+
+
+
+
+ {/* Sidebar Navigation */}
+
+
+ {/* Main Content */}
+
+
+ {/* Getting Started */}
+
+
+ Documentation
+
+
+ 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.
+
+
+
+
+
+ Inviting the Bot
+
+
+ Currently, Void Sentinel is in Beta . Access is restricted to approved servers.
+
+
+
+
+
+
+
Restricted Access
+
+ To invite Void Sentinel to your server, you must request access by sending a DM to _void_x_ on Discord. Once approved, you can use the invite link provided in the dashboard.
+
+
+
+
+
+ {/* Commands */}
+
+
+ Leveling Commands
+
+
+
+
+
+
+
+
+
+
+ Utility Commands
+
+
+
+
+
+
+
+
+
+
+
+
+ Fun & AI Commands
+
+
+
+
+ Note: AI-powered features are currently experimental and restricted.
+
+
+
+
+
+
+
+
+
+
+
+ {/* Dashboard Guide */}
+
+
+
+ Leaderboard
+
+
+ View your server's leveling leaderboard through the dashboard. This list displays all members ranked by their accumulated XP and current level.
+
+
+
+
+
+
+
+
+ Leveling System Setup
+
+
+ The dashboard allows you to configure multi-track leveling systems. Here's how to set it up:
+
+
+
+
+
+ 1
+ Create Leveling Roles
+
+
+ Create all the level roles you intend to use in your Discord server first.
+
+
+
+
+
+ 2
+ Add to Multi-Track Leveling System
+
+
+ Go to the dashboard and click on Leveling in the sidebar.
+ In the Level Tracks section, click on Add New Track .
+ Give your track a unique name (e.g., "Mage", "Warrior").
+ Select an Initial Role . This role identifies which track a user belongs to.
+ Add your level roles and assign the specific levels they unlock at.
+ Click Save Changes to apply your configuration.
+
+
+ Free servers can have up to 4 tracks. Premium servers support up to 10 tracks.
+
+
+
+
+
+ 3
+ Set Level Bridgers
+
+
+ Level Bridgers help restrict users to their initial track and integrate seamlessly with Discord onboarding or reaction roles.
+
+
+ Select a Recruit Role (the role given by onboarding/reaction).
+ Select the corresponding Initial Role defined in your Level Track.
+ Click on Add Bridger .
+
+
+
+
+
+
+
+ Changing Tracks
+
+
+ If a user wants to switch to a different track, they must rest their progress. Use the command /level reset.
+ This will reset their level and XP to 0 and remove all roles associated with their current level track.
+
+
+
+
+
+
+
+
+ );
+}
+
+function CommandCard({ command, description, args, permission, tag }: { command: string, description: string, args?: string[], permission?: string, tag?: string }) {
+ return (
+
+
+
+
+ {command}
+
+ {permission && (
+
+ {permission}
+
+ )}
+
+ {tag && (
+
+ {tag}
+
+ )}
+
+
+
{description}
+ {args && args.length > 0 && (
+
+
Parameters
+
+ {args.map((arg, i) => (
+
+ {arg}
+
+ ))}
+
+
+ )}
+
+
+ )
+}
diff --git a/web/app/favicon.ico b/web/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
Binary files a/web/app/favicon.ico and /dev/null differ
diff --git a/web/components/DashboardSidebar.tsx b/web/components/DashboardSidebar.tsx
index e34cab6..a2be18a 100644
--- a/web/components/DashboardSidebar.tsx
+++ b/web/components/DashboardSidebar.tsx
@@ -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 */}
-
+
+
+
+
+
{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"
}`}
>
@@ -70,7 +75,19 @@ export default function DashboardSidebar({ guildId }: DashboardSidebarProps) {
);
})}
+
+ {/* Docs Link at Bottom */}
+
+
+
+ Documentation
+
+
+
>
);
}
diff --git a/web/components/Footer.tsx b/web/components/Footer.tsx
index 27b7859..2be7793 100644
--- a/web/components/Footer.tsx
+++ b/web/components/Footer.tsx
@@ -5,6 +5,11 @@ const Footer = () => {
© {new Date().getFullYear()} Void Sentinel. All rights reserved.
+
);
diff --git a/web/components/LevelingEditor.tsx b/web/components/LevelingEditor.tsx
index aa965d0..1a60755 100644
--- a/web/components/LevelingEditor.tsx
+++ b/web/components/LevelingEditor.tsx
@@ -72,7 +72,7 @@ export default function LevelingEditor({ guildId }: LevelingEditorProps) {
// Level Bridger State
const [bridges, setBridges] = useState
([]);
- const [deletedBridges, setDeletedBridges] = useState([]); // in_role_ids
+
const [newBridgeIn, setNewBridgeIn] = useState(null);
const [newBridgeOut, setNewBridgeOut] = useState(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) {