diff --git a/pyproject.toml b/pyproject.toml index 2a2109e..2283e44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,29 +24,29 @@ classifiers = [ # Korrigierte Syntax: Direktes Array dependencies = [ - "ezcord==0.7.4", - "py-cord==2.7.1", - "aiosqlite==0.22.1", - "aiohttp==3.13.3", - "aiocache==0.12.3", - "propcache==0.4.1", - "requests==2.32.5", - "wikipedia==1.4.0", - "beautifulsoup4==4.14.3", - "soupsieve==2.8.3", - "yarl==1.23.0", - "frozenlist==1.8.0", - "h11==0.16.0", - "multidict==6.7.1", - "SimpleColoredLogs==1.16.1.2026", - "FastAPI==0.135.1", - "uvicorn==0.41.0", - "Jinja2==3.1.6", - "MarkupSafe==3.0.3", - "starlette==0.52.1", - "timedelta==2020.12.3", - "ManagerX-Handler==1.2026.2.9.4", - "ManagerX-DevTools==1.2026.2.26" + "ezcord>=0.7.4", + "py-cord>=2.7.1", + "aiosqlite>=0.22.1", + "aiohttp>=3.13.3", + "aiocache>=0.12.3", + "propcache>=0.4.1", + "requests>=2.32.5", + "wikipedia>=1.4.0", + "beautifulsoup4>=4.14.3", + "soupsieve>=2.8.3", + "yarl>=1.23.0", + "frozenlist>=1.8.0", + "h11>=0.16.0", + "multidict>=6.7.1", + "SimpleColoredLogs>=1.16.1.2026", + "FastAPI>=0.135.1", + "uvicorn>=0.41.0", + "Jinja2>=3.1.6", + "MarkupSafe>=3.0.3", + "starlette>=0.52.1", + "timedelta>=2020.12.3", + "ManagerX-Handler>=1.2026.2.9.4", + "ManagerX-DevTools>=1.2026.3.24" ] [project.urls] diff --git a/requirements/req.txt b/requirements/req.txt index 766cc5a..4bc0048 100644 --- a/requirements/req.txt +++ b/requirements/req.txt @@ -1,86 +1,86 @@ # --- Deine Online Packages (ManagerX Ecosystem) --- -ManagerX-DevTools==1.2026.3.15 -ManagerX-Handler==1.2026.2.9.4 +ManagerX-DevTools>=1.2026.3.15 +ManagerX-Handler>=1.2026.2.9.4 # --- Core Frameworks & Discord --- -ezcord==0.7.4 -py-cord==2.7.1 -better-ipc==2.0.3 -fastapi==0.135.1 -uvicorn==0.41.0 -starlette==0.52.1 -PyJWT==2.10.1 +ezcord>=0.7.4 +py-cord>=2.7.1 +better-ipc>=2.0.3 +fastapi>=0.135.1 +uvicorn>=0.41.0 +starlette>=0.52.1 +PyJWT>=2.10.1 # --- Async & Performance --- -aiocache==0.12.3 -aiohappyeyeballs==2.6.1 -aiohttp==3.13.3 -aiosignal==1.4.0 -aiosqlite==0.22.1 -anyio==4.12.1 -frozenlist==1.8.0 -multidict==6.7.1 -propcache==0.4.1 -sniffio==1.3.1 -websockets==16.0 +aiocache>=0.12.3 +aiohappyeyeballs>=2.6.1 +aiohttp>=3.13.3 +aiosignal>=1.4.0 +aiosqlite>=0.22.1 +anyio>=4.12.1 +frozenlist>=1.8.0 +multidict>=6.7.1 +propcache>=0.4.1 +sniffio>=1.3.1 +websockets>=16.0 # --- Data & Validation --- -annotated-types==0.7.0 -pydantic==2.12.5 -pydantic_core==2.41.5 -PyYAML==6.0.1 -typing_extensions==4.15.0 -typing-inspection==0.4.2 +annotated-types>=0.7.0 +pydantic>=2.12.5 +pydantic_core>=2.41.5 +PyYAML>=6.0.1 +typing_extensions>=4.15.0 +typing-inspection>=0.4.2 # --- HTTP & Web --- -requests==2.32.5 -requests-toolbelt==1.0.0 -httpx==0.24.1 -httpcore==0.17.3 -h11==0.14.0 -urllib3==2.1.0 -yarl==1.23.0 -certifi==2026.2.25 -charset-normalizer==3.4.5 -idna==3.11 -rfc3986==1.5.0 +requests>=2.32.5 +requests-toolbelt>=1.0.0 +httpx>=0.24.1 +httpcore>=0.17.3 +h11>=0.14.0 +urllib3>=2.1.0 +yarl>=1.23.0 +certifi>=2026.2.25 +charset-normalizer>=3.4.5 +idna>=3.11 +rfc3986>=1.5.0 # --- UI, Imaging & Logging --- -beautifulsoup4==4.14.3 -soupsieve==2.8.3 -easy-pil==0.4.0 -pillow==10.4.0 -SimpleColoredLogs==1.16.1.2026 -colorama==0.4.6 -rich==13.5.2 -Pygments==2.19.2 -markdown-it-py==3.0.0 -mdurl==0.1.2 +beautifulsoup4>=4.14.3 +soupsieve>=2.8.3 +easy-pil>=0.4.0 +pillow>=10.4.0 +SimpleColoredLogs>=1.16.1.2026 +colorama>=0.4.6 +rich>=13.5.2 +Pygments>=2.19.2 +markdown-it-py>=3.0.0 +mdurl>=0.1.2 # --- System & Utilities --- -click==8.3.1 -psutil==5.9.5 -python-dotenv==1.2.2 -timedelta==2020.12.3 -pytz==2026.1.post1 -datetime==5.1 -six==1.17.0 -cffi==2.0.0 -pycparser==3.0 -PyNaCl==1.6.2 -zope.interface==8.2 +click>=8.3.1 +psutil>=5.9.5 +python-dotenv>=1.2.2 +timedelta>=2020.12.3 +pytz>=2026.1.post1 +datetime>=5.1 +six>=1.17.0 +cffi>=2.0.0 +pycparser>=3.0 +PyNaCl>=1.6.2 +zope.interface>=8.2 # --- Template Engines --- -Jinja2==3.1.6 -MarkupSafe==3.0.3 +Jinja2>=3.1.6 +MarkupSafe>=3.0.3 # --- Helpers & Docs --- -wikipedia==1.4.0 -packaging==26.0 -pathlib==1.0.1 -more-itertools==10.8.0 -jaraco.classes==3.4.0 -jaraco.context==6.1.1 -jaraco.functools==4.4.0 -docutils==0.22.4 -nh3==0.3.2 \ No newline at end of file +wikipedia>=1.4.0 +packaging>=26.0 +pathlib>=1.0.1 +more-itertools>=10.8.0 +jaraco.classes>=3.4.0 +jaraco.context>=6.1.1 +jaraco.functools>=4.4.0 +docutils>=0.22.4 +nh3>=0.3.2 \ No newline at end of file diff --git a/src/api/dashboard/routes.py b/src/api/dashboard/routes.py index 7bd343d..ab5d40a 100644 --- a/src/api/dashboard/routes.py +++ b/src/api/dashboard/routes.py @@ -68,6 +68,46 @@ async def get_stats(request: Request): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) +@router_public.get("/leaderboard") +async def get_leaderboard(limit: int = 50): + """ + Fetches the global leaderboard from StatsDB and enriches it with Discord data. + """ + from mx_devtools import StatsDB + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + try: + stats_db = StatsDB() + # get_leaderboard returns user_id, global_level, global_xp, total_messages, total_voice_minutes + rows = await stats_db.get_leaderboard(limit=limit) + + leaderboard = [] + for row in rows: + uid = row[0] + # Try to get user from cache first + user = bot_instance.get_user(uid) + + # If not in cache, we don't fetch_user here to avoid hitting rate limits + # and slowing down the request significantly for 50 users. + + username = user.name if user else f"User {uid}" + avatar = user.display_avatar.url if user else None + + leaderboard.append({ + "user_id": str(uid), + "username": username, + "avatar_url": avatar, + "level": row[1], + "xp": row[2], + "messages": row[3], + "voice_minutes": round(row[4], 1) + }) + + return {"success": True, "leaderboard": leaderboard} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + @router_public.get("/version") async def get_version(request: Request): return { diff --git a/src/api/dashboard/user_routes.py b/src/api/dashboard/user_routes.py index 572f45c..bdcb50b 100644 --- a/src/api/dashboard/user_routes.py +++ b/src/api/dashboard/user_routes.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Request, HTTPException, Depends from src.api.dashboard.auth_routes import get_current_user -from mx_devtools import SettingsDB, StatsDB +from mx_devtools import SettingsDB, StatsDB, EconomyDatabase import discord import sqlite3 import os @@ -110,6 +110,10 @@ async def get_user_settings(user: dict = Depends(get_current_user)): "global_chat": { "total_messages": global_chat_messages }, + "economy": { + "global_coins": EconomyDatabase().get_global_balance(user_id), + "overrides": EconomyDatabase().get_equipped_overrides(user_id) + }, "top_servers": top_servers } } diff --git a/src/bot/cogs/bot/botjoinevent.py b/src/bot/cogs/bot/botjoinevent.py new file mode 100644 index 0000000..2040be4 --- /dev/null +++ b/src/bot/cogs/bot/botjoinevent.py @@ -0,0 +1,51 @@ +import discord +from discord.ui import Container # Dein ezcord/discord.ui Import +import ezcord + +class BotJoinEvents(ezcord.Cog): + def __init__(self, bot): + self.bot = bot + + @ezcord.Cog.listener() + async def on_guild_join(self, guild): + # Den Serverbesitzer schnappen + owner = guild.owner + + if owner: + # Container erstellen (Passend zu deinem Branding in Rot) + container = Container(color=discord.Color.red()) + + # Content im Container-Style aufbauen + container.add_text(f"# 👋 Hello! I am {self.bot.user.name}") + container.add_text( + f"Thanks for inviting me to **{guild.name}**! I'm here to support you with " + "**Moderation, Global Chat, and Economy**." + ) + + container.add_separator() + + container.add_text("### 🏆 Current Season") + container.add_text("Check out the global leaderboard using `/stats`!") + + container.add_text("### 🛠️ Setup") + container.add_text("Use `/help` to configure my settings.") + + container.add_separator() + + container.add_text("### 🔗 Links") + container.add_text("🔗 [Website](https://managerx-bot.de) × [Top.gg](https://top.gg/bot/1368201272624287754) × [Support](https://discord.gg/9T28DWup3g) × [GitHub](https://github.com/ManagerX-Development/ManagerX)") + + # Die DesignerView für den Container erstellen + view = discord.ui.DesignerView(container, timeout=0) + + try: + # Den modernen Container per DM an den Owner senden + await owner.send(view=view) + except discord.Forbidden: + # Falls DMs beim Owner zu sind + pass + except Exception as e: + print(f"Error sending Container-DM: {e}") + +def setup(bot): + bot.add_cog(BotJoinEvents(bot)) \ No newline at end of file diff --git a/src/bot/cogs/bot/join_alert.py b/src/bot/cogs/bot/server_join_alert.py similarity index 100% rename from src/bot/cogs/bot/join_alert.py rename to src/bot/cogs/bot/server_join_alert.py diff --git a/src/bot/cogs/bot/leave_alert.py b/src/bot/cogs/bot/server_leave_alert.py similarity index 100% rename from src/bot/cogs/bot/leave_alert.py rename to src/bot/cogs/bot/server_leave_alert.py diff --git a/src/bot/cogs/economy/gb_economy.py b/src/bot/cogs/economy/gb_economy.py new file mode 100644 index 0000000..f3f1871 --- /dev/null +++ b/src/bot/cogs/economy/gb_economy.py @@ -0,0 +1,127 @@ +# Copyright (c) 2026 OPPRO.NET Network +import discord +from discord.ext import commands +from discord import SlashCommandGroup, Option +import ezcord +from mx_devtools import EconomyDatabase +from datetime import datetime, timedelta +import random + +class GlobalEconomy(ezcord.Cog): + def __init__(self, bot): + self.bot = bot + self.db = EconomyDatabase() + + global_grp = SlashCommandGroup("global", "Global commands") + economy = global_grp.create_subgroup("economy", "Global economy commands") + shop = global_grp.create_subgroup("shop", "Global shop commands") + + @economy.command(name="balance", description="Check your global coin balance") + async def global_balance(self, ctx: discord.ApplicationContext, user: Option(discord.Member, "Select a user", required=False)): + target = user or ctx.author + coins = self.db.get_global_balance(target.id) + + embed = discord.Embed( + title="🌍 Globaler Kontostand", + description=f"**{target.display_name}** hat **{coins}** 🪙 Coins.", + color=discord.Color.gold() + ) + embed.set_thumbnail(url=target.display_avatar.url) + await ctx.respond(embed=embed) + + @economy.command(name="daily", description="Claim your daily global coins") + async def global_daily(self, ctx: discord.ApplicationContext): + user_info = self.db.get_user_economy_info(ctx.author.id) + last_daily_raw = user_info.get('last_daily') + + if last_daily_raw: + # Handle both SQLite timestamp formats + try: + last_daily = datetime.strptime(last_daily_raw, "%Y-%m-%d %H:%M:%S") + except ValueError: + try: + last_daily = datetime.fromisoformat(last_daily_raw) + except ValueError: + last_daily = None + + if last_daily and datetime.utcnow() < last_daily + timedelta(days=1): + next_daily = last_daily + timedelta(days=1) + wait_time = next_daily - datetime.utcnow() + hours, remainder = divmod(int(wait_time.total_seconds()), 3600) + minutes, _ = divmod(remainder, 60) + return await ctx.respond(f"❌ Du hast deine täglichen Coins bereits abgeholt. Warte noch **{max(0, hours)}h {max(0, minutes)}m**.", ephemeral=True) + + amount = random.randint(100, 250) + if self.db.claim_daily(ctx.author.id, amount): + embed = discord.Embed( + title="🎁 Täglicher Bonus", + description=f"Du hast **{amount}** 🪙 Coins erhalten!", + color=discord.Color.green() + ) + await ctx.respond(embed=embed) + else: + await ctx.respond("❌ Fehler beim Abholen des Bonus.", ephemeral=True) + + @shop.command(name="browse", description="Browse items in the global shop") + async def shop_browse(self, ctx: discord.ApplicationContext): + items = self.db.get_shop_items() + if not items: + return await ctx.respond("Der Shop ist aktuell leer.", ephemeral=True) + + embed = discord.Embed( + title="🛒 Global Shop", + description="Kaufe Upgrades für den GlobalChat!", + color=discord.Color.blue() + ) + + for item in items: + embed.add_field( + name=f"{item['name']} (ID: {item['item_id']})", + value=f"Preis: **{item['price']}** 🪙\n{item['description']}", + inline=False + ) + + await ctx.respond(embed=embed) + + @shop.command(name="buy", description="Buy an item from the shop") + async def shop_buy(self, ctx: discord.ApplicationContext, item_id: Option(int, "Enter the ID of the item")): + success, message = self.db.buy_item(ctx.author.id, item_id) + await ctx.respond(message, ephemeral=True) + + @shop.command(name="inventory", description="View your purchased items") + async def shop_inventory(self, ctx: discord.ApplicationContext): + items = self.db.get_user_inventory(ctx.author.id) + if not items: + return await ctx.respond("Dein Inventar ist leer. Kaufe etwas im `/global shop browse`!", ephemeral=True) + + embed = discord.Embed( + title="🎒 Dein Inventar", + description="Hier sind deine gekauften Upgrades.", + color=discord.Color.purple() + ) + + for item in items: + status = "✅ Ausgerüstet" if item['is_equipped'] else "❌ Nicht ausgerüstet" + embed.add_field( + name=f"{item['name']} (ID: {item['item_id']})", + value=f"{status}\n{item['description']}", + inline=False + ) + + await ctx.respond(embed=embed) + + @shop.command(name="equip", description="Equip an item from your inventory") + async def shop_equip(self, ctx: discord.ApplicationContext, item_id: Option(int, "Enter the ID of the item")): + inventory = self.db.get_user_inventory(ctx.author.id) + owned_ids = [i['item_id'] for i in inventory] + + if item_id not in owned_ids: + return await ctx.respond("❌ Du besitzt dieses Item nicht.", ephemeral=True) + + if self.db.equip_item(ctx.author.id, item_id): + await ctx.respond("✅ Item erfolgreich ausgerüstet!", ephemeral=True) + else: + await ctx.respond("❌ Fehler beim Ausrüsten.", ephemeral=True) + +def setup(bot): + bot.add_cog(GlobalEconomy(bot)) diff --git a/src/bot/cogs/economy/gld_economy.py b/src/bot/cogs/economy/gld_economy.py new file mode 100644 index 0000000..a18e87c --- /dev/null +++ b/src/bot/cogs/economy/gld_economy.py @@ -0,0 +1,55 @@ +# Copyright (c) 2026 OPPRO.NET Network +import discord +from discord.ext import commands +from discord import slash_command, Option +import ezcord +from mx_devtools import EconomyDatabase + +class GuildEconomy(ezcord.Cog): + def __init__(self, bot): + self.bot = bot + self.db = EconomyDatabase() + + @slash_command(name="balance", description="Check your server coin balance") + async def balance(self, ctx: discord.ApplicationContext, user: Option(discord.Member, "Select a user", required=False)): + target = user or ctx.author + if target.bot: + return await ctx.respond("Bots haben kein Konto.", ephemeral=True) + + coins = self.db.get_guild_balance(ctx.guild.id, target.id) + + embed = discord.Embed( + title=f"💰 Kontostand - {ctx.guild.name}", + description=f"**{target.display_name}** hat **{coins}** 🪙 Coins auf diesem Server.", + color=discord.Color.blue() + ) + embed.set_thumbnail(url=target.display_avatar.url) + await ctx.respond(embed=embed) + + @slash_command(name="pay", description="Transfer coins to another user on this server") + async def pay(self, ctx: discord.ApplicationContext, + user: Option(discord.Member, "Who do you want to pay?"), + amount: Option(int, "How many coins?", min_value=1)): + + if user.id == ctx.author.id: + return await ctx.respond("Du kannst dir nicht selbst Geld senden.", ephemeral=True) + if user.bot: + return await ctx.respond("Du kannst Bots kein Geld senden.", ephemeral=True) + + author_balance = self.db.get_guild_balance(ctx.guild.id, ctx.author.id) + if author_balance < amount: + return await ctx.respond(f"Du hast nicht genug Coins (Guthaben: {author_balance}).", ephemeral=True) + + # Transfer + self.db.add_guild_coins(ctx.guild.id, ctx.author.id, -amount) + self.db.add_guild_coins(ctx.guild.id, user.id, amount) + + embed = discord.Embed( + title="💸 Überweisung erfolgreich", + description=f"Du hast **{amount}** 🪙 Coins an **{user.display_name}** gesendet.", + color=discord.Color.green() + ) + await ctx.respond(embed=embed) + +def setup(bot): + bot.add_cog(GuildEconomy(bot)) diff --git a/src/bot/cogs/guild/globalchat.py b/src/bot/cogs/guild/globalchat.py index 5868a06..4f5f03e 100644 --- a/src/bot/cogs/guild/globalchat.py +++ b/src/bot/cogs/guild/globalchat.py @@ -11,6 +11,7 @@ import aiohttp import io import json +import random from datetime import datetime, timedelta import ezcord from collections import defaultdict @@ -273,6 +274,24 @@ async def create_message_embed(self, message: discord.Message, settings: Dict, a # Author mit Badges und Username-Tag author_text, badges = self._build_author_info(message.author) + + # --- ECONOMY OVERRIDES --- + from mx_devtools import EconomyDatabase + eco_db = EconomyDatabase() + overrides = eco_db.get_equipped_overrides(message.author.id) + + # Color Override + if 'color' in overrides: + embed_color = self._parse_color(overrides['color']) + embed.color = embed_color + + # Emoji Override + if 'emoji' in overrides: + emoji = overrides['emoji'] + # Add emoji to the author name + author_text = f"{emoji} {author_text}" + # ------------------------- + embed.set_author( name=author_text, icon_url=message.author.display_avatar.url @@ -836,6 +855,31 @@ async def on_message(self, message: discord.Message): pass # Kann Nachricht nicht löschen/reagieren return + # --- ECONOMY: Award Coins --- + from mx_devtools import EconomyDatabase + eco_db = EconomyDatabase() + # Award 5-15 coins per message with a simple cooldown check + user_info = eco_db.get_user_economy_info(message.author.id) + last_msg_raw = user_info.get('last_message_at') + can_earn = True + if last_msg_raw: + try: + # Handle common SQLite formats + try: + last_dt = datetime.strptime(last_msg_raw, "%Y-%m-%d %H:%M:%S") + except ValueError: + last_dt = datetime.fromisoformat(last_msg_raw) + + if datetime.utcnow() < last_dt + timedelta(seconds=30): + can_earn = False + except Exception: pass + + if can_earn: + amount = random.randint(5, 15) + eco_db.add_global_coins(message.author.id, amount) + eco_db.update_last_message(message.author.id) + # ---------------------------- + # Rate Limiting prüfen bucket = self.message_cooldown.get_bucket(message) retry_after = bucket.update_rate_limit() diff --git a/src/web/App.tsx b/src/web/App.tsx index c0356f9..280167a 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -14,6 +14,7 @@ const Status = lazy(() => import("./pages/Status")); const CommandsPage = lazy(() => import("./pages/CommandsPage")); const TeamPage = lazy(() => import("./pages/TeamPage")); const RoadmapPage = lazy(() => import("./pages/RoadmapPage")); +const LeaderboardPage = lazy(() => import("./pages/LeaderboardPage")); const License = lazy(() => import("./pages/License").then(module => ({ default: module.License }))); const LoginPage = lazy(() => import("./dashboard/LoginPage")); const SettingsPage = lazy(() => import("./dashboard/SettingsPage")); @@ -104,6 +105,7 @@ const MainRoutes = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/web/components/Navbar.tsx b/src/web/components/Navbar.tsx index 30cce03..9377e45 100644 --- a/src/web/components/Navbar.tsx +++ b/src/web/components/Navbar.tsx @@ -3,7 +3,7 @@ import { motion, AnimatePresence } from "framer-motion"; import { Link, useLocation } from "react-router-dom"; import { Shield, Menu, X, Sparkles, Puzzle, Activity, Terminal, - Newspaper, Users, Milestone, ChevronDown, LayoutDashboard, User // Added User + Newspaper, Users, Milestone, ChevronDown, LayoutDashboard, User, Trophy // Added Trophy } from "lucide-react"; import { cn } from "../lib/utils"; import { useAuth } from "./AuthProvider"; @@ -11,6 +11,7 @@ import { useAuth } from "./AuthProvider"; const mainLinks = [ { label: "Features", href: "/#features", icon: Sparkles }, { label: "Commands", href: "/commands", icon: Terminal }, + { label: "Leaderboard", href: "/leaderboard", icon: Trophy }, ]; const dropdownLinks = [ diff --git a/src/web/pages/LeaderboardPage.tsx b/src/web/pages/LeaderboardPage.tsx new file mode 100644 index 0000000..2b8b404 --- /dev/null +++ b/src/web/pages/LeaderboardPage.tsx @@ -0,0 +1,267 @@ +/// +import { memo, useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Navbar } from "../components/Navbar"; +import { Footer } from "../components/Footer"; +import { SEO } from "../components/SEO"; +import { Trophy, Medal, Crown, MessageSquare, Mic, Zap, TrendingUp, Search } from "lucide-react"; +import { cn } from "../lib/utils"; + +interface LeaderboardUser { + user_id: string; + username: string; + avatar_url: string | null; + level: number; + xp: number; + messages: number; + voice_minutes: number; +} + +export const LeaderboardPage = memo(function LeaderboardPage() { + const [leaderboard, setLeaderboard] = useState([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + + useEffect(() => { + const fetchLeaderboard = async () => { + try { + const baseUrl = import.meta.env.VITE_API_URL || "http://localhost:8040"; + const response = await fetch(`${baseUrl}/dashboard/v1/managerx/leaderboard`); + if (response.ok) { + const data = await response.json(); + if (data.success) { + setLeaderboard(data.leaderboard); + } + } + } catch (error) { + console.error("Error fetching leaderboard:", error); + } finally { + setLoading(false); + } + }; + + fetchLeaderboard(); + }, []); + + const filteredLeaderboard = leaderboard.filter(user => + user.username.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const PodiumCard = ({ user, rank, delay }: { user: LeaderboardUser, rank: number, delay: number }) => { + const isFirst = rank === 1; + const Icon = isFirst ? Crown : (rank === 2 ? Medal : Medal); + const colorClass = isFirst ? "from-yellow-400 to-yellow-600" : (rank === 2 ? "from-slate-300 to-slate-500" : "from-amber-600 to-amber-800"); + const borderColor = isFirst ? "border-yellow-500/50" : (rank === 2 ? "border-slate-400/50" : "border-amber-700/50"); + + return ( + + {/* Glow Effect */} +
+ +
+
+
+ {user.avatar_url ? ( + {user.username} + ) : ( +
+ {user.username[0].toUpperCase()} +
+ )} +
+
+
+ +
+
+ +
+

{user.username}

+
+ + Level {user.level} +
+ +
+
+

XP

+

{user.xp.toLocaleString()}

+
+
+

Msgs

+

{user.messages.toLocaleString()}

+
+
+
+ + ); + }; + + return ( +
+ + + +
+
+ + + Globale Rangliste + +

+ Die Legenden +

+

+ Wer sind die aktivsten Nutzer im gesamten ManagerX Netzwerk? Hier findest du die Top 50 unserer Community. +

+
+ + {!loading && leaderboard.length > 0 && ( +
+ {leaderboard.length >= 2 ? ( + + ) :
} + + {leaderboard.length >= 1 && ( + + )} + + {leaderboard.length >= 3 ? ( + + ) :
} +
+ )} + +
+ {/* Search Bar */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full bg-white/5 border border-white/10 rounded-2xl py-3 pl-12 pr-4 text-white focus:outline-none focus:border-primary/50 transition-all font-medium" + /> +
+
+ + Top 50 Aktiv +
+
+ +
+ {loading ? ( +
+
+

Synchronisiere Daten...

+
+ ) : ( + <> + {filteredLeaderboard.length > 3 ? ( +
+ + + + + + + + + + + + + {filteredLeaderboard.slice(3).map((user, idx) => ( + + + + + + + + ))} + + +
RangUserLevelAktivitätXP
+ #{idx + 4} + +
+
+ {user.avatar_url ? ( + {user.username} + ) : ( +
+ {user.username[0].toUpperCase()} +
+ )} +
+ {user.username} +
+
+
+ Lvl {user.level} +
+
+
+
+
+
+
+ + {user.messages.toLocaleString()} +
+
+ + {user.voice_minutes.toLocaleString()}m +
+
+
+ {user.xp.toLocaleString()} +
+
+ ) : ( + filteredLeaderboard.length === 0 ? ( +
+

Keine Legenden mit diesem Namen gefunden.

+
+ ) : ( +
+

Alle aktuellen Legenden sind bereits auf dem Treppchen zu bewundern!

+
+ ) + )} + + )} +
+
+
+ +
+
+ ); +}); + +export default LeaderboardPage; diff --git a/src/web/pages/License.tsx b/src/web/pages/License.tsx index 336a62e..3b98456 100644 --- a/src/web/pages/License.tsx +++ b/src/web/pages/License.tsx @@ -8,30 +8,118 @@ import { Navbar } from "../components/Navbar"; import { Footer } from "../components/Footer"; import { motion } from "framer-motion"; -const pythonDependencies = [ - { name: "py-cord", version: "2.7.0", license: "MIT", url: "https://github.com/Pycord-Development/pycord" }, - { name: "ezcord", version: "0.7.4", license: "MIT", url: "https://github.com/ezcord-org/ezcord" }, - { name: "aiosqlite", version: "0.22.1", license: "MIT", url: "https://github.com/omnilib/aiosqlite" }, - { name: "aiohttp", version: "3.13.3", license: "Apache 2.0", url: "https://github.com/aio-libs/aiohttp" }, - { name: "aiocache", version: "0.12.3", license: "BSD", url: "https://github.com/aio-libs/aiocache" }, - { name: "requests", version: "2.32.5", license: "Apache 2.0", url: "https://github.com/psf/requests" }, - { name: "wikipedia", version: "1.4.0", license: "MIT", url: "https://github.com/goldsmith/Wikipedia" }, - { name: "beautifulsoup4", version: "4.14.3", license: "MIT", url: "https://www.crummy.com/software/BeautifulSoup/" }, +const pythonCategories = [ + { + title: "ManagerX Ecosystem", + deps: [ + { name: "ManagerX-DevTools", version: "1.2026.3.15", license: "GPL-3.0", url: "https://github.com/ManagerX-Development/ManagerX-DevTools" }, + { name: "ManagerX-Handler", version: "1.2026.2.9.4", license: "GPL-3.0", url: "https://github.com/ManagerX-Development/ManagerX-Handler" }, + ] + }, + { + title: "Core Frameworks & Discord", + deps: [ + { name: "ezcord", version: "0.7.4", license: "MIT", url: "https://github.com/ezcord-org/ezcord" }, + { name: "py-cord", version: "2.7.1", license: "MIT", url: "https://github.com/Pycord-Development/pycord" }, + { name: "fastapi", version: "0.135.1", license: "MIT", url: "https://github.com/tiangolo/fastapi" }, + { name: "uvicorn", version: "0.41.0", license: "BSD-3", url: "https://github.com/encode/uvicorn" }, + { name: "better-ipc", version: "2.0.3", license: "MIT", url: "https://github.com/Marseel-E/better-ipc" }, + { name: "PyJWT", version: "2.10.1", license: "MIT", url: "https://github.com/jpadilla/pyjwt" }, + ] + }, + { + title: "Async & Performance", + deps: [ + { name: "aiohttp", version: "3.13.3", license: "Apache 2.0", url: "https://github.com/aio-libs/aiohttp" }, + { name: "aiosqlite", version: "0.22.1", license: "MIT", url: "https://github.com/omnilib/aiosqlite" }, + { name: "aiocache", version: "0.12.3", license: "BSD-3", url: "https://github.com/aio-libs/aiocache" }, + { name: "anyio", version: "4.12.1", license: "MIT", url: "https://github.com/agronholm/anyio" }, + { name: "websockets", version: "16.0", license: "BSD-3", url: "https://github.com/python-websockets/websockets" }, + ] + }, + { + title: "Data & Validation", + deps: [ + { name: "pydantic", version: "2.12.5", license: "MIT", url: "https://github.com/pydantic/pydantic" }, + { name: "PyYAML", version: "6.0.1", license: "MIT", url: "https://github.com/yaml/pyyaml" }, + { name: "annotated-types", version: "0.7.0", license: "MIT", url: "https://github.com/pydantic/annotated-types" }, + ] + }, + { + title: "UI, Imaging & Logging", + deps: [ + { name: "beautifulsoup4", version: "4.14.3", license: "MIT", url: "https://www.crummy.com/software/BeautifulSoup/" }, + { name: "pillow", version: "10.4.0", license: "HPND", url: "https://github.com/python-pillow/Pillow" }, + { name: "rich", version: "13.5.2", license: "MIT", url: "https://github.com/Textualize/rich" }, + { name: "easy-pil", version: "0.4.0", license: "MIT", url: "https://github.com/Oogle-S/easy-pil" }, + ] + }, + { + title: "System & Utilities", + deps: [ + { name: "psutil", version: "5.9.5", license: "BSD-3", url: "https://github.com/giampaolo/psutil" }, + { name: "python-dotenv", version: "1.2.2", license: "BSD-3", url: "https://github.com/theskumar/python-dotenv" }, + { name: "click", version: "8.3.1", license: "BSD-3", url: "https://github.com/pallets/click" }, + { name: "jinja2", version: "3.1.6", license: "BSD-3", url: "https://github.com/pallets/jinja" }, + { name: "wikipedia", version: "1.4.0", license: "MIT", url: "https://github.com/goldsmith/Wikipedia" }, + ] + } ]; -const nodeDependencies = [ - { name: "react", version: "18+", license: "MIT", url: "https://github.com/facebook/react" }, - { name: "typescript", version: "5+", license: "Apache 2.0", url: "https://github.com/microsoft/TypeScript" }, - { name: "vite", version: "5+", license: "MIT", url: "https://github.com/vitejs/vite" }, - { name: "framer-motion", version: "12+", license: "MIT", url: "https://github.com/framer/motion" }, - { name: "lucide-react", version: "latest", license: "ISC", url: "https://github.com/lucide-icons/lucide" } +const nodeCategories = [ + { + title: "Framework & Core", + deps: [ + { name: "react", version: "19.2.4", license: "MIT", url: "https://github.com/facebook/react" }, + { name: "react-dom", version: "19.2.4", license: "MIT", url: "https://github.com/facebook/react" }, + { name: "react-router-dom", version: "7.13.1", license: "MIT", url: "https://github.com/remix-run/react-router" }, + { name: "vite", version: "8.0.0", license: "MIT", url: "https://github.com/vitejs/vite" }, + { name: "typescript", version: "5.9.3", license: "Apache 2.0", url: "https://github.com/microsoft/TypeScript" }, + ] + }, + { + title: "UI & Animations", + deps: [ + { name: "framer-motion", version: "12.36.0", license: "MIT", url: "https://github.com/framer/motion" }, + { name: "lucide-react", version: "0.577.0", license: "ISC", url: "https://github.com/lucide-icons/lucide" }, + { name: "embla-carousel-react", version: "8.6.0", license: "MIT", url: "https://github.com/davidjerleke/embla-carousel" }, + { name: "sonner", version: "2.0.7", license: "MIT", url: "https://github.com/emilkowalski/sonner" }, + { name: "vaul", version: "1.1.2", license: "MIT", url: "https://github.com/emilkowalski/vaul" }, + ] + }, + { + title: "State & Data Management", + deps: [ + { name: "@tanstack/react-query", version: "5.90.21", license: "MIT", url: "https://github.com/tanstack/query" }, + { name: "zod", version: "4.3.6", license: "MIT", url: "https://github.com/colinhacks/zod" }, + { name: "react-hook-form", version: "7.71.2", license: "MIT", url: "https://github.com/react-hook-form/react-hook-form" }, + ] + }, + { + title: "Styling", + deps: [ + { name: "tailwindcss", version: "4.2.1", license: "MIT", url: "https://github.com/tailwindlabs/tailwindcss" }, + { name: "clsx", version: "2.1.1", license: "MIT", url: "https://github.com/lukeed/clsx" }, + { name: "tailwind-merge", version: "3.5.0", license: "MIT", url: "https://github.com/dcastil/tailwind-merge" }, + ] + }, + { + title: "Radix UI Primitives", + deps: [ + { name: "@radix-ui/react-accordion", version: "1.2.12", license: "MIT", url: "https://github.com/radix-ui/primitives" }, + { name: "@radix-ui/react-alert-dialog", version: "1.1.15", license: "MIT", url: "https://github.com/radix-ui/primitives" }, + { name: "@radix-ui/react-dropdown-menu", version: "2.1.16", license: "MIT", url: "https://github.com/radix-ui/primitives" }, + { name: "@radix-ui/react-tooltip", version: "1.2.8", license: "MIT", url: "https://github.com/radix-ui/primitives" }, + { name: "@radix-ui/react-dialog", version: "1.1.15", license: "MIT", url: "https://github.com/radix-ui/primitives" }, + ] + } ]; const SECTIONS = [ { id: "project-license", title: "Project License", icon: Shield }, { id: "python-deps", title: "Python Deps", icon: Box }, { id: "node-deps", title: "Node.js Deps", icon: Package }, - { id: "other-deps", title: "Other Tools", icon: Terminal }, + { id: "other-deps", title: "Other Details", icon: Terminal }, { id: "contributing", title: "Contributing", icon: Heart }, { id: "copyright", title: "Copyright", icon: HelpCircle }, ]; @@ -55,13 +143,13 @@ const DependencyCard = ({ dep }: { dep: any }) => ( href={dep.url} target="_blank" rel="noopener noreferrer" - className="group flex flex-col p-6 rounded-2xl bg-[#111318] border border-white/5 hover:border-primary/20 transition-all" + className="group flex flex-col p-6 rounded-2xl bg-[#111318] border border-white/5 hover:border-primary/20 transition-all h-full" >
-

{dep.name}

- +

{dep.name}

+
-
+
v{dep.version} {dep.license} @@ -118,8 +206,8 @@ export const License = memo(function License() { key={section.id} onClick={() => scrollToSection(section.id)} className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 group text-sm font-semibold border ${activeSection === section.id - ? "bg-primary/10 text-primary border-primary/20 shadow-lg shadow-primary/5" - : "text-slate-400 hover:text-slate-200 hover:bg-white/5 border-transparent" + ? "bg-primary/10 text-primary border-primary/20 shadow-lg shadow-primary/5" + : "text-slate-400 hover:text-slate-200 hover:bg-white/5 border-transparent" }`} > {/* Content Area */} -
+
-
+
@@ -188,24 +276,51 @@ export const License = memo(function License() {
-
- {pythonDependencies.map((dep) => ( - +
+ {pythonCategories.map((cat) => ( +
+

{cat.title}

+
+ {cat.deps.map((dep) => ( + + ))} +
+
))}
-
- {nodeDependencies.map((dep) => ( - +
+ {nodeCategories.map((cat) => ( +
+

{cat.title}

+
+ {cat.deps.map((dep) => ( + + ))} +
+
))}
-
+
-

Wir nutzen zudem SQLite 3+ (Public Domain) für die lokale Datenspeicherung und Sphinx für die Dokumentation.

+

Neben den oben genannten Bibliotheken nutzen wir zahlreiche weitere Tools zur Qualitätssicherung und Entwicklung:

+
    +
  • • SQLite 3+
  • +
  • • Sphinx Docs
  • +
  • • ESLint
  • +
  • • Vitest
  • +
  • • PostCSS
  • +
  • • Autoprefixer
  • +
  • • Pytest
  • +
  • • Black (Formatting)
  • +
+

+ Alle verwendeten Bibliotheken unterliegen ihren jeweiligen Lizenzen (MIT, Apache 2.0, BSD, etc.). +