Compare commits
4 Commits
main
...
e21d9e11b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e21d9e11b6 | ||
|
|
5681e14f8d | ||
|
|
67643cb54d | ||
|
|
86282fbe07 |
@@ -1,27 +0,0 @@
|
|||||||
name: SonarQube
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sonar:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: sonarsource/sonarqube-scan-action@v5
|
|
||||||
env:
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
SONAR_HOST_URL: http://10.0.0.15:9001
|
|
||||||
with:
|
|
||||||
args: >
|
|
||||||
-Dsonar.projectKey=Papo
|
|
||||||
-Dsonar.projectName=Papo
|
|
||||||
-Dsonar.sources=.
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,2 @@
|
|||||||
.env
|
.env
|
||||||
node_modules
|
node_modules
|
||||||
debug.log
|
|
||||||
dist/
|
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[DEPLOY] Starte docker compose..."
|
echo "[DEPLOY] Starte docker compose..."
|
||||||
docker-compose pull || true
|
docker compose pull || true
|
||||||
docker-compose build
|
docker compose build
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
echo "[DEPLOY] Aufräumen..."
|
echo "[DEPLOY] Aufräumen..."
|
||||||
docker image prune -f || true
|
docker image prune -f || true
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ services:
|
|||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: dockerfile
|
dockerfile: Dockerfile
|
||||||
image: papo-app:latest
|
image: papo-app:latest
|
||||||
working_dir: /usr/src/app
|
working_dir: /usr/src/app
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: sh -c "npx prisma migrate deploy --schema=src/database/schema.prisma && npm run dev"
|
command: sh -c "npm run dev"
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
20
dockerfile
20
dockerfile
@@ -12,30 +12,20 @@ ENV PATH="/usr/src/app/node_modules/.bin:${PATH}"
|
|||||||
ENV DATABASE_URL=postgresql://user:pass@localhost:5432/papo?schema=public
|
ENV DATABASE_URL=postgresql://user:pass@localhost:5432/papo?schema=public
|
||||||
ENV PRISMA_IGNORE_ENV_LOAD=true
|
ENV PRISMA_IGNORE_ENV_LOAD=true
|
||||||
|
|
||||||
# Install backend dependencies
|
# Install dependencies (inkl. dev)
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --include=dev
|
RUN npm ci --include=dev
|
||||||
|
|
||||||
# Install frontend dependencies
|
|
||||||
COPY frontend/package*.json ./frontend/
|
|
||||||
RUN npm --prefix frontend ci
|
|
||||||
|
|
||||||
# Copy source
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build frontend
|
|
||||||
RUN npm run build:web
|
|
||||||
|
|
||||||
# Ensure prisma CLI available globally (avoids path issues)
|
# Ensure prisma CLI available globally (avoids path issues)
|
||||||
RUN npm install -g prisma@5.4.2
|
RUN npm install -g prisma@5.4.2
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
# Generate Prisma client (explicit schema path)
|
# Generate Prisma client (explicit schema path)
|
||||||
RUN prisma generate --schema=src/database/schema.prisma
|
RUN prisma generate --schema=src/database/schema.prisma
|
||||||
|
|
||||||
# Build backend (tsc emits JS even with type errors; exit code suppressed for pre-existing errors)
|
|
||||||
RUN npm run build:web && npx tsc || true
|
|
||||||
|
|
||||||
# Optional: show versions in build log
|
# Optional: show versions in build log
|
||||||
RUN node -v && npm -v && npx prisma -v
|
RUN node -v && npm -v && npx prisma -v
|
||||||
|
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "run", "dev"]
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Papo Dashboard</title>
|
|
||||||
<script>__PAPO_CONFIG__</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="./src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
3313
frontend/package-lock.json
generated
3313
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "papo-dashboard-frontend",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.1.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@heroui/react": "^3.2.1",
|
|
||||||
"@heroui/styles": "^3.2.1",
|
|
||||||
"framer-motion": "^12.23.24",
|
|
||||||
"lucide-react": "^0.542.0",
|
|
||||||
"react": "^19.2.0",
|
|
||||||
"react-dom": "^19.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
|
||||||
"@types/react": "^19.2.2",
|
|
||||||
"@types/react-dom": "^19.2.2",
|
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
|
||||||
"tailwindcss": "^4.1.13",
|
|
||||||
"typescript": "^5.9.2",
|
|
||||||
"vite": "^7.1.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useApp } from './context/AppContext';
|
|
||||||
import { AppLayout } from './components/layout/AppLayout';
|
|
||||||
import { GuildSelect } from './pages/GuildSelect';
|
|
||||||
import { Dashboard } from './pages/Dashboard';
|
|
||||||
import { Tickets } from './pages/Tickets';
|
|
||||||
import { SupportLogin } from './pages/SupportLogin';
|
|
||||||
import { Automod } from './pages/Automod';
|
|
||||||
import { Welcome } from './pages/Welcome';
|
|
||||||
import { DynamicVoice } from './pages/DynamicVoice';
|
|
||||||
import { Birthday } from './pages/Birthday';
|
|
||||||
import { ReactionRoles } from './pages/ReactionRoles';
|
|
||||||
import { Statuspage } from './pages/Statuspage';
|
|
||||||
import { ServerStats } from './pages/ServerStats';
|
|
||||||
import { Register } from './pages/Register';
|
|
||||||
import { MusicPage } from './pages/Music';
|
|
||||||
import { SettingsPage } from './pages/Settings';
|
|
||||||
import { ModulesPage } from './pages/Modules';
|
|
||||||
import { Events } from './pages/Events';
|
|
||||||
import { Tasks } from './pages/Tasks';
|
|
||||||
import { Watchlist } from './pages/Watchlist';
|
|
||||||
import { Admin } from './pages/Admin';
|
|
||||||
import { Branding } from './pages/Branding';
|
|
||||||
import { Growth } from './pages/Growth';
|
|
||||||
import { Permissions } from './pages/Permissions';
|
|
||||||
import { Panels } from './pages/Panels';
|
|
||||||
|
|
||||||
const THEME_COLORS: Record<string, string> = {
|
|
||||||
orange: '#f97316',
|
|
||||||
blue: '#3b82f6',
|
|
||||||
green: '#22c55e',
|
|
||||||
purple: '#a855f7',
|
|
||||||
red: '#ef4444'
|
|
||||||
};
|
|
||||||
|
|
||||||
function AppContent() {
|
|
||||||
const { guilds, currentGuildId, section, settings } = useApp();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const branding = settings.brandingConfig || {};
|
|
||||||
const color = branding.embedColor || (branding.theme ? THEME_COLORS[branding.theme] : undefined);
|
|
||||||
if (color) document.documentElement.style.setProperty('--accent', color);
|
|
||||||
}, [settings.brandingConfig]);
|
|
||||||
|
|
||||||
if (!guilds.length) {
|
|
||||||
return <GuildSelect />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentGuildId) {
|
|
||||||
return <GuildSelect />;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (section) {
|
|
||||||
case 'overview': return <Dashboard />;
|
|
||||||
case 'tickets': return <Tickets />;
|
|
||||||
case 'supportlogin': return <SupportLogin />;
|
|
||||||
case 'automod': return <Automod />;
|
|
||||||
case 'welcome': return <Welcome />;
|
|
||||||
case 'dynamicvoice': return <DynamicVoice />;
|
|
||||||
case 'birthday': return <Birthday />;
|
|
||||||
case 'reactionroles': return <ReactionRoles />;
|
|
||||||
case 'statuspage': return <Statuspage />;
|
|
||||||
case 'serverstats': return <ServerStats />;
|
|
||||||
case 'register': return <Register />;
|
|
||||||
case 'music': return <MusicPage />;
|
|
||||||
case 'settings': return <SettingsPage />;
|
|
||||||
case 'modules': return <ModulesPage />;
|
|
||||||
case 'events': return <Events />;
|
|
||||||
case 'tasks': return <Tasks />;
|
|
||||||
case 'watchlist': return <Watchlist />;
|
|
||||||
case 'branding': return <Branding />;
|
|
||||||
case 'growth': return <Growth />;
|
|
||||||
case 'permissions': return <Permissions />;
|
|
||||||
case 'panels': return <Panels />;
|
|
||||||
case 'admin': return <Admin />;
|
|
||||||
default: return <Dashboard />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<AppLayout>
|
|
||||||
<AppContent />
|
|
||||||
</AppLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@import "@heroui/styles";
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
:root,
|
|
||||||
.light,
|
|
||||||
.dark {
|
|
||||||
/* Brand accent: matches the orange used across bot embeds and the login page
|
|
||||||
(see 0xf97316 in src/services/*.ts and src/web/server.ts), overriding
|
|
||||||
HeroUI's blue default theme token. */
|
|
||||||
--accent: #f97316;
|
|
||||||
--accent-foreground: var(--snow);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* HeroUI ships fields with a transparent border and 0 border-width by default,
|
|
||||||
so an Input on a plain Card is invisible (identical bg, no outline). Give
|
|
||||||
every field a real, visible box across the whole app. */
|
|
||||||
--field-border-width: 1px;
|
|
||||||
--field-border: var(--border-secondary);
|
|
||||||
--field-background: var(--surface-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body, #root {
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
width: 100%;
|
|
||||||
border: var(--border-width-field) solid var(--field-border, var(--border));
|
|
||||||
border-radius: var(--radius-field);
|
|
||||||
background: var(--field-background, var(--default));
|
|
||||||
color: var(--field-foreground, var(--foreground));
|
|
||||||
font: inherit;
|
|
||||||
padding: 0.6rem 0.85rem;
|
|
||||||
transition: border-color 150ms ease, background-color 150ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
select:hover {
|
|
||||||
background: var(--field-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--field-border-focus);
|
|
||||||
background: var(--field-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 0.35rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--border);
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Spinner } from '@heroui/react';
|
|
||||||
import { Sidebar } from './Sidebar';
|
|
||||||
import { Header } from './Header';
|
|
||||||
import { useApp } from '../../context/AppContext';
|
|
||||||
|
|
||||||
export function AppLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
const { loading, guilds } = useApp();
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center gap-3">
|
|
||||||
<Spinner color="accent" size="lg" />
|
|
||||||
<p className="text-sm text-muted">Dashboard wird geladen...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!guilds.length) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
|
||||||
<p className="text-muted">Keine Server verfügbar</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen overflow-hidden">
|
|
||||||
<div className="hidden shrink-0 lg:block">
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main className="flex flex-1 flex-col overflow-y-auto">
|
|
||||||
<div className="mx-auto w-full max-w-[1520px] px-6 py-6">
|
|
||||||
<Header />
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { Button, Chip, Tooltip, Dropdown, DropdownTrigger, DropdownPopover, DropdownMenu, DropdownItem } from '@heroui/react';
|
|
||||||
import { Moon, Sun, ChevronDown, LogOut, Settings } from 'lucide-react';
|
|
||||||
import { useTheme } from '../../hooks/useTheme';
|
|
||||||
import { useApp } from '../../context/AppContext';
|
|
||||||
import { AppAvatar } from '../shared/AppAvatar';
|
|
||||||
|
|
||||||
const navLabels: Record<string, string> = {
|
|
||||||
overview: 'Übersicht',
|
|
||||||
tickets: 'Ticketsystem',
|
|
||||||
supportlogin: 'Support Login',
|
|
||||||
automod: 'Automod',
|
|
||||||
welcome: 'Willkommen',
|
|
||||||
dynamicvoice: 'Dynamic Voice',
|
|
||||||
birthday: 'Birthday',
|
|
||||||
reactionroles: 'Reaction Roles',
|
|
||||||
statuspage: 'Statuspage',
|
|
||||||
serverstats: 'Server Stats',
|
|
||||||
register: 'Registrierung',
|
|
||||||
music: 'Musik',
|
|
||||||
settings: 'Einstellungen',
|
|
||||||
modules: 'Module',
|
|
||||||
events: 'Events',
|
|
||||||
admin: 'Admin',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Header() {
|
|
||||||
const { dark, toggle } = useTheme();
|
|
||||||
const { guildName, statusMessage } = useHeaderData();
|
|
||||||
const { user, handleLogout, setSection, section } = useApp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
|
||||||
<div className="flex items-center gap-1.5 text-sm text-muted min-w-0">
|
|
||||||
<button className="hover:text-foreground transition-colors" onClick={() => setSection('overview')}>
|
|
||||||
Dashboard
|
|
||||||
</button>
|
|
||||||
{section !== 'overview' && (
|
|
||||||
<>
|
|
||||||
<span>/</span>
|
|
||||||
<span className="text-accent font-semibold truncate">{navLabels[section] || section}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
|
||||||
{statusMessage && (
|
|
||||||
<Chip size="sm" variant="soft" color="accent" className="max-w-[220px]">
|
|
||||||
<span className="truncate">{statusMessage}</span>
|
|
||||||
</Chip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Tooltip>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<Button isIconOnly size="sm" variant="ghost" onPress={toggle}>
|
|
||||||
{dark ? <Sun size={16} /> : <Moon size={16} />}
|
|
||||||
</Button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content placement="bottom">{dark ? 'Helles Design' : 'Dunkles Design'}</Tooltip.Content>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownTrigger className="inline-flex h-9 items-center gap-2 rounded-3xl px-3 text-sm font-medium hover:bg-default-hover">
|
|
||||||
<AppAvatar name={user?.username} size="sm" className="size-6" />
|
|
||||||
<span className="hidden sm:inline text-sm">{user?.username}</span>
|
|
||||||
<ChevronDown size={14} />
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownPopover placement="bottom end">
|
|
||||||
<DropdownMenu aria-label="User Menu">
|
|
||||||
<DropdownItem key="settings" onAction={() => setSection('settings')}>
|
|
||||||
<Settings size={14} /> Einstellungen
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem key="logout" variant="danger" onAction={handleLogout}>
|
|
||||||
<LogOut size={14} /> Abmelden
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenu>
|
|
||||||
</DropdownPopover>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useHeaderData() {
|
|
||||||
const { guildInfo, guilds, currentGuildId, statusMessage } = useApp();
|
|
||||||
const selectedGuild = guilds.find((g) => g.id === currentGuildId);
|
|
||||||
return {
|
|
||||||
guildName: guildInfo?.name || selectedGuild?.name || 'Dashboard',
|
|
||||||
statusMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import {
|
|
||||||
Button, ScrollShadow, Tooltip,
|
|
||||||
Select, SelectTrigger, SelectValue, SelectPopover, ListBox, ListBoxItem
|
|
||||||
} from '@heroui/react';
|
|
||||||
import {
|
|
||||||
LogOut, PanelLeftClose, PanelLeft, Activity, AudioLines, CalendarDays,
|
|
||||||
ClipboardList, Eye, Home, LogIn, ListChecks, Music, Palette, Puzzle, RadioTower, Settings,
|
|
||||||
Shield, ShieldAlert, Sparkles, Tag, Ticket, TrendingUp, LayoutPanelTop, Wrench
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useApp } from '../../context/AppContext';
|
|
||||||
import { AppAvatar } from '../shared/AppAvatar';
|
|
||||||
|
|
||||||
const navGroups = [
|
|
||||||
{
|
|
||||||
label: 'Dashboard',
|
|
||||||
items: [
|
|
||||||
{ key: 'overview', label: 'Uebersicht', icon: <Home size={18} /> },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Community',
|
|
||||||
items: [
|
|
||||||
{ key: 'welcome', label: 'Willkommen', icon: <Sparkles size={18} /> },
|
|
||||||
{ key: 'birthday', label: 'Birthday', icon: <CalendarDays size={18} /> },
|
|
||||||
{ key: 'events', label: 'Events', icon: <Activity size={18} /> },
|
|
||||||
{ key: 'reactionroles', label: 'Reaction Roles', icon: <Tag size={18} /> },
|
|
||||||
{ key: 'panels', label: 'Info-Panels', icon: <LayoutPanelTop size={18} /> },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Support',
|
|
||||||
items: [
|
|
||||||
{ key: 'tickets', label: 'Ticketsystem', icon: <Ticket size={18} /> },
|
|
||||||
{ key: 'supportlogin', label: 'Support Login', icon: <LogIn size={18} /> },
|
|
||||||
{ key: 'register', label: 'Registrierung', icon: <ClipboardList size={18} /> },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Moderation',
|
|
||||||
items: [
|
|
||||||
{ key: 'automod', label: 'Automod', icon: <Shield size={18} /> },
|
|
||||||
{ key: 'permissions', label: 'Rechte-Scanner', icon: <ShieldAlert size={18} /> },
|
|
||||||
{ key: 'tasks', label: 'Team-Aufgaben', icon: <ListChecks size={18} /> },
|
|
||||||
{ key: 'watchlist', label: 'Watchlist', icon: <Eye size={18} /> },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Funktionen',
|
|
||||||
items: [
|
|
||||||
{ key: 'dynamicvoice', label: 'Dynamic Voice', icon: <AudioLines size={18} /> },
|
|
||||||
{ key: 'music', label: 'Musik', icon: <Music size={18} /> },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Statistiken',
|
|
||||||
items: [
|
|
||||||
{ key: 'statuspage', label: 'Statuspage', icon: <RadioTower size={18} /> },
|
|
||||||
{ key: 'serverstats', label: 'Server Stats', icon: <Activity size={18} /> },
|
|
||||||
{ key: 'growth', label: 'Wachstum', icon: <TrendingUp size={18} /> },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'System',
|
|
||||||
items: [
|
|
||||||
{ key: 'modules', label: 'Module', icon: <Puzzle size={18} /> },
|
|
||||||
{ key: 'branding', label: 'Branding', icon: <Palette size={18} /> },
|
|
||||||
{ key: 'settings', label: 'Einstellungen', icon: <Settings size={18} /> },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Sidebar() {
|
|
||||||
const { user, guilds, currentGuildId, section, setCurrentGuildId, setSection, handleLogout, settings } = useApp();
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const branding = settings.brandingConfig || {};
|
|
||||||
const botName = branding.botName || 'Papo';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className={`bg-surface border-r border-border flex h-full flex-col transition-all duration-200 ${collapsed ? 'w-14' : 'w-48'}`}>
|
|
||||||
<div className={`flex items-center gap-2 px-2.5 pt-3 pb-2.5 ${collapsed ? 'flex-col' : 'justify-between'}`}>
|
|
||||||
<div className={`flex min-w-0 items-center gap-2 ${collapsed ? 'flex-col' : ''}`}>
|
|
||||||
{branding.logoUrl ? (
|
|
||||||
<img src={branding.logoUrl} alt={botName} className="size-8 shrink-0 rounded-xl object-cover" />
|
|
||||||
) : (
|
|
||||||
<div className="bg-accent text-accent-foreground flex size-8 shrink-0 items-center justify-center rounded-xl text-sm font-black">
|
|
||||||
{botName.trim()[0]?.toUpperCase() || 'P'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="truncate text-sm font-bold leading-tight">{botName}</div>
|
|
||||||
<div className="text-[9px] uppercase tracking-widest text-muted">Dashboard</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button isIconOnly size="sm" variant="ghost" className="shrink-0" onPress={() => setCollapsed((c) => !c)}>
|
|
||||||
{collapsed ? <PanelLeft size={15} /> : <PanelLeftClose size={15} />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-2 pb-2">
|
|
||||||
<Select
|
|
||||||
aria-label="Guild auswaehlen"
|
|
||||||
selectedKey={currentGuildId}
|
|
||||||
onSelectionChange={(key) => {
|
|
||||||
if (typeof key === 'string') setCurrentGuildId(key);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue className="truncate" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectPopover>
|
|
||||||
<ListBox>
|
|
||||||
{guilds.map((g) => (
|
|
||||||
<ListBoxItem key={g.id} id={g.id} textValue={g.name}>
|
|
||||||
{collapsed ? g.name.slice(0, 2) : g.name}
|
|
||||||
</ListBoxItem>
|
|
||||||
))}
|
|
||||||
</ListBox>
|
|
||||||
</SelectPopover>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScrollShadow className="flex-1 px-2 py-2" hideScrollBar>
|
|
||||||
<nav className="flex flex-col gap-3">
|
|
||||||
{navGroups.map((group) => (
|
|
||||||
<div key={group.label}>
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="px-2 pb-1 text-[9px] font-semibold uppercase tracking-[0.16em] text-muted">
|
|
||||||
{group.label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
{group.items
|
|
||||||
.filter((item) => item.key !== 'admin' || user?.isAdmin)
|
|
||||||
.map((item) => {
|
|
||||||
const isActive = section === item.key;
|
|
||||||
return (
|
|
||||||
<Tooltip key={item.key} isDisabled={!collapsed}>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<Button
|
|
||||||
className={`h-9 justify-start gap-2.5 px-2.5 text-sm font-medium ${isActive ? 'bg-accent-soft text-accent-soft-foreground' : 'text-muted'}`}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onPress={() => setSection(item.key as any)}
|
|
||||||
>
|
|
||||||
{item.icon} {!collapsed && <span className="truncate">{item.label}</span>}
|
|
||||||
</Button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content placement="right" offset={8}>{item.label}</Tooltip.Content>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{user?.isAdmin && (
|
|
||||||
<div>
|
|
||||||
{!collapsed && (
|
|
||||||
<div className="px-2 pb-1 text-[9px] font-semibold uppercase tracking-[0.16em] text-muted">
|
|
||||||
Admin
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Tooltip isDisabled={!collapsed}>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<Button
|
|
||||||
className={`h-9 justify-start gap-2.5 px-2.5 text-sm font-medium ${section === 'admin' ? 'bg-accent-soft text-accent-soft-foreground' : 'text-muted'}`}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onPress={() => setSection('admin' as any)}
|
|
||||||
>
|
|
||||||
<Wrench size={18} /> {!collapsed && 'Admin'}
|
|
||||||
</Button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content placement="right" offset={8}>Admin</Tooltip.Content>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
</ScrollShadow>
|
|
||||||
|
|
||||||
<div className="border-t border-border p-2">
|
|
||||||
<div className={`group flex items-center gap-2 rounded-lg p-1.5 transition-colors hover:bg-surface-secondary ${collapsed ? 'justify-center' : ''}`}>
|
|
||||||
<AppAvatar name={user?.username} size="sm" className="shrink-0" />
|
|
||||||
{!collapsed && (
|
|
||||||
<>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="truncate text-xs font-semibold">{user?.username}</div>
|
|
||||||
<div className="text-[10px] text-muted">Angemeldet</div>
|
|
||||||
</div>
|
|
||||||
<Tooltip>
|
|
||||||
<Tooltip.Trigger>
|
|
||||||
<Button
|
|
||||||
isIconOnly
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
||||||
onPress={handleLogout}
|
|
||||||
>
|
|
||||||
<LogOut size={14} />
|
|
||||||
</Button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content placement="top">Abmelden</Tooltip.Content>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Button, Chip } from '@heroui/react';
|
|
||||||
import { Moon, Sun } from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
guildName: string;
|
|
||||||
statusMessage?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Topbar({ guildName, statusMessage, children }: Props) {
|
|
||||||
const [dark, setDark] = useState(() => localStorage.getItem('papo-theme') !== 'light');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.classList.toggle('dark', dark);
|
|
||||||
localStorage.setItem('papo-theme', dark ? 'dark' : 'light');
|
|
||||||
}, [dark]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight">{guildName}</h1>
|
|
||||||
<p className="mt-1 text-sm text-muted">Bot-Dashboard Verwaltung</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{statusMessage && (
|
|
||||||
<Chip color="warning" size="sm" variant="soft">{statusMessage}</Chip>
|
|
||||||
)}
|
|
||||||
<Button isIconOnly size="sm" variant="ghost" onPress={() => setDark((d) => !d)}>
|
|
||||||
{dark ? <Sun size={16} /> : <Moon size={16} />}
|
|
||||||
</Button>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Card, CardContent, Chip } from '@heroui/react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
icon: ReactNode;
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ActivityTile({ icon, label, value }: Props) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="flex flex-row items-center justify-between gap-4 p-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Chip color="accent" size="sm" variant="soft">
|
|
||||||
{icon} {label}
|
|
||||||
</Chip>
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-black">{value}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Avatar } from '@heroui/react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
src?: string;
|
|
||||||
name?: string;
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AppAvatar({ src, name, size = 'md', className }: Props) {
|
|
||||||
return (
|
|
||||||
<Avatar size={size} className={className}>
|
|
||||||
{src && <Avatar.Image src={src} alt={name || 'Avatar'} />}
|
|
||||||
<Avatar.Fallback>{name?.trim()?.[0]?.toUpperCase() || '?'}</Avatar.Fallback>
|
|
||||||
</Avatar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Switch } from '@heroui/react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isSelected: boolean;
|
|
||||||
onChange: (value: boolean) => void;
|
|
||||||
label?: ReactNode;
|
|
||||||
description?: ReactNode;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
size?: 'sm' | 'md' | 'lg';
|
|
||||||
'aria-label'?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AppSwitch({ isSelected, onChange, label, description, isDisabled, size, ...rest }: Props) {
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
isSelected={isSelected}
|
|
||||||
onChange={onChange}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
size={size}
|
|
||||||
aria-label={!label ? rest['aria-label'] || 'Umschalten' : undefined}
|
|
||||||
>
|
|
||||||
<Switch.Content>
|
|
||||||
<Switch.Control>
|
|
||||||
<Switch.Thumb />
|
|
||||||
</Switch.Control>
|
|
||||||
{label && <span>{label}</span>}
|
|
||||||
</Switch.Content>
|
|
||||||
{description && <p data-slot="description" className="text-xs text-muted">{description}</p>}
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
type Item = {
|
|
||||||
label: string;
|
|
||||||
value: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
items: Item[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function BarComparisonChart({ items }: Props) {
|
|
||||||
const max = Math.max(1, ...items.map((i) => i.value));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{items.map((item) => (
|
|
||||||
<div key={item.label} className="flex items-center gap-3">
|
|
||||||
<span className="w-24 shrink-0 truncate text-xs text-muted">{item.label}</span>
|
|
||||||
<div className="h-2 flex-1 rounded-full bg-default">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-r-full bg-accent"
|
|
||||||
style={{ width: `${Math.max(2, (item.value / max) * 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="w-10 shrink-0 text-right text-xs font-semibold tabular-nums">{item.value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { Select, SelectTrigger, SelectValue, SelectPopover, ListBox, ListBoxItem } from '@heroui/react';
|
|
||||||
import { Hash, Volume2, Folder, ChevronDown } from 'lucide-react';
|
|
||||||
|
|
||||||
export type ChannelOption = { id: string; name: string; type?: string };
|
|
||||||
|
|
||||||
function optionIcon(type?: string) {
|
|
||||||
if (type === 'voice') return <Volume2 size={14} className="shrink-0 text-muted" />;
|
|
||||||
if (type === 'category') return <Folder size={14} className="shrink-0 text-muted" />;
|
|
||||||
return <Hash size={14} className="shrink-0 text-muted" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
options: ChannelOption[];
|
|
||||||
value?: string;
|
|
||||||
onChange: (id: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
'aria-label'?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ChannelSelect({ options, value, onChange, placeholder = 'Channel wählen', ...rest }: Props) {
|
|
||||||
const items = value && !options.some((o) => o.id === value)
|
|
||||||
? [...options, { id: value, name: value, type: 'unknown' }]
|
|
||||||
: options;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
aria-label={rest['aria-label'] || placeholder}
|
|
||||||
placeholder={placeholder}
|
|
||||||
selectedKey={value || null}
|
|
||||||
onSelectionChange={(key) => { if (typeof key === 'string') onChange(key); }}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full justify-between">
|
|
||||||
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
||||||
{optionIcon(items.find((o) => o.id === value)?.type)}
|
|
||||||
<SelectValue className="truncate" />
|
|
||||||
</span>
|
|
||||||
<ChevronDown size={14} className="shrink-0 text-muted" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectPopover className="max-h-72 w-[--trigger-width]">
|
|
||||||
<ListBox>
|
|
||||||
{items.length ? items.map((o) => (
|
|
||||||
<ListBoxItem key={o.id} id={o.id} textValue={o.name}>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{optionIcon(o.type)} {o.type === 'unknown' ? `Unbekannt (${o.name})` : o.name}
|
|
||||||
</span>
|
|
||||||
</ListBoxItem>
|
|
||||||
)) : (
|
|
||||||
<ListBoxItem key="__empty" id="__empty" isDisabled textValue="Keine Channels gefunden">
|
|
||||||
Keine Channels gefunden
|
|
||||||
</ListBoxItem>
|
|
||||||
)}
|
|
||||||
</ListBox>
|
|
||||||
</SelectPopover>
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
import { Bot } from 'lucide-react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
botName?: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
footer?: string;
|
|
||||||
image?: string;
|
|
||||||
accentColor?: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const now = () => new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
|
||||||
|
|
||||||
export function DiscordPreview({ botName = 'Papo', title, description, footer, image, accentColor = '#5865f2', children }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg bg-[#313338] p-4">
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-[#5865f2] text-white">
|
|
||||||
<Bot size={20} />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex flex-wrap items-baseline gap-2">
|
|
||||||
<span className="text-[15px] font-medium text-white">{botName}</span>
|
|
||||||
<span className="rounded bg-[#5865f2] px-1 py-px text-[10px] font-medium leading-none text-white">BOT</span>
|
|
||||||
<span className="text-xs text-[#949ba4]">Heute um {now()}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(title || description || footer || image) && (
|
|
||||||
<div
|
|
||||||
className="mt-1 max-w-md rounded border-l-4 bg-[#2b2d31] p-3"
|
|
||||||
style={{ borderLeftColor: accentColor }}
|
|
||||||
>
|
|
||||||
{title && <div className="text-[15px] font-semibold text-white">{title}</div>}
|
|
||||||
{description && (
|
|
||||||
<div className="mt-1 whitespace-pre-wrap text-sm leading-snug text-[#dbdee1]">{description}</div>
|
|
||||||
)}
|
|
||||||
{footer && <div className="mt-2.5 text-xs text-[#949ba4]">{footer}</div>}
|
|
||||||
{image && <img src={image} alt="" className="mt-2.5 max-h-64 w-full max-w-full rounded object-cover" />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children && <div className="mt-2 flex flex-wrap gap-2">{children}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DiscordButton({ variant = 'primary', children }: { variant?: 'primary' | 'secondary'; children: ReactNode }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`flex h-8 items-center gap-1.5 rounded px-3 text-sm font-medium text-white ${
|
|
||||||
variant === 'primary' ? 'bg-[#5865f2]' : 'bg-[#4e5058]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Card, CardContent } from '@heroui/react';
|
|
||||||
import { Inbox } from 'lucide-react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
message?: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function EmptyState({ message = 'Keine Daten vorhanden', icon }: Props) {
|
|
||||||
return (
|
|
||||||
<Card className="bg-surface-tertiary">
|
|
||||||
<CardContent className="flex flex-col items-center gap-3 py-8">
|
|
||||||
<div className="flex size-12 items-center justify-center rounded-full bg-default-soft text-muted">
|
|
||||||
{icon || <Inbox size={24} />}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted">{message}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Card, CardContent } from '@heroui/react';
|
|
||||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
message?: string;
|
|
||||||
onRetry?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ErrorState({ message = 'Ein Fehler ist aufgetreten', onRetry }: Props) {
|
|
||||||
return (
|
|
||||||
<Card className="border border-danger-soft bg-danger-soft/40">
|
|
||||||
<CardContent className="flex flex-col items-center gap-3 py-8">
|
|
||||||
<div className="flex size-12 items-center justify-center rounded-full bg-danger-soft text-danger">
|
|
||||||
<AlertTriangle size={24} />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted">{message}</p>
|
|
||||||
{onRetry && (
|
|
||||||
<button className="flex items-center gap-1 text-xs text-accent hover:opacity-80 transition-opacity" onClick={onRetry}>
|
|
||||||
<RefreshCw size={12} /> Erneut versuchen
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Card, CardHeader, CardContent, CardTitle } from '@heroui/react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string;
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FormPanel({ title, children }: Props) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{title}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">{children}</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Card, CardHeader, CardContent, CardTitle } from '@heroui/react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string;
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ListPanel({ title, children, className }: Props) {
|
|
||||||
return (
|
|
||||||
<Card className={className}>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{title}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{children}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Card, CardContent, Skeleton } from '@heroui/react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
lines?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LoadingSkeleton({ lines = 3 }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{Array.from({ length: lines }).map((_, i) => (
|
|
||||||
<Card key={i}>
|
|
||||||
<CardContent className="gap-2 p-5">
|
|
||||||
<Skeleton className="h-4 w-3/4 rounded-lg" />
|
|
||||||
{i < 2 && <Skeleton className="mt-3 h-3 w-1/2 rounded-lg" />}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { ReactNode } from 'react';
|
|
||||||
import { AppSwitch } from './AppSwitch';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
icon: ReactNode;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
isSelected: boolean;
|
|
||||||
onChange: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ModuleActiveToggle({ icon, title, description, isSelected, onChange }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-3 rounded-xl border border-accent bg-accent-soft p-3">
|
|
||||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-accent text-accent-foreground">
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="text-sm font-semibold">{title}</div>
|
|
||||||
<div className="text-xs text-muted">{description}</div>
|
|
||||||
</div>
|
|
||||||
<AppSwitch aria-label={title} isSelected={isSelected} onChange={onChange} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { Select, SelectTrigger, SelectValue, SelectPopover, ListBox, ListBoxItem } from '@heroui/react';
|
|
||||||
import { ChevronDown, AtSign } from 'lucide-react';
|
|
||||||
|
|
||||||
export type RoleOption = { id: string; name: string; color?: string };
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
options: RoleOption[];
|
|
||||||
value?: string;
|
|
||||||
onChange: (id: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
'aria-label'?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function RoleSelect({ options, value, onChange, placeholder = 'Rolle wählen', ...rest }: Props) {
|
|
||||||
const items = value && !options.some((o) => o.id === value)
|
|
||||||
? [...options, { id: value, name: value }]
|
|
||||||
: options;
|
|
||||||
const selected = items.find((o) => o.id === value);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
aria-label={rest['aria-label'] || placeholder}
|
|
||||||
placeholder={placeholder}
|
|
||||||
selectedKey={value || null}
|
|
||||||
onSelectionChange={(key) => { if (typeof key === 'string') onChange(key); }}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full justify-between">
|
|
||||||
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
||||||
{selected?.color ? (
|
|
||||||
<span className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: selected.color }} />
|
|
||||||
) : (
|
|
||||||
<AtSign size={14} className="shrink-0 text-muted" />
|
|
||||||
)}
|
|
||||||
<SelectValue className="truncate" />
|
|
||||||
</span>
|
|
||||||
<ChevronDown size={14} className="shrink-0 text-muted" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectPopover className="max-h-72 w-[--trigger-width]">
|
|
||||||
<ListBox>
|
|
||||||
{items.length ? items.map((o) => (
|
|
||||||
<ListBoxItem key={o.id} id={o.id} textValue={o.name}>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
{o.color ? <span className="size-2.5 rounded-full" style={{ backgroundColor: o.color }} /> : <AtSign size={14} className="text-muted" />}
|
|
||||||
{o.name}
|
|
||||||
</span>
|
|
||||||
</ListBoxItem>
|
|
||||||
)) : (
|
|
||||||
<ListBoxItem key="__empty" id="__empty" isDisabled textValue="Keine Rollen gefunden">
|
|
||||||
Keine Rollen gefunden
|
|
||||||
</ListBoxItem>
|
|
||||||
)}
|
|
||||||
</ListBox>
|
|
||||||
</SelectPopover>
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Card, CardHeader, CardContent, CardTitle, CardDescription } from '@heroui/react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string;
|
|
||||||
subtitle?: string;
|
|
||||||
children: ReactNode;
|
|
||||||
action?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SectionCard({ title, subtitle, children, action }: Props) {
|
|
||||||
return (
|
|
||||||
<Card className="bg-surface-secondary">
|
|
||||||
<CardHeader className="flex items-start justify-between gap-4">
|
|
||||||
<div className="min-w-0 flex flex-col gap-1">
|
|
||||||
<CardTitle>{title}</CardTitle>
|
|
||||||
{subtitle && <CardDescription>{subtitle}</CardDescription>}
|
|
||||||
</div>
|
|
||||||
{action && <div className="shrink-0">{action}</div>}
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>{children}</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Card, CardContent } from '@heroui/react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
icon: ReactNode;
|
|
||||||
label: string;
|
|
||||||
value: string | number;
|
|
||||||
trend?: string;
|
|
||||||
color?: 'accent' | 'success' | 'warning' | 'danger' | 'default';
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconClasses: Record<NonNullable<Props['color']>, string> = {
|
|
||||||
accent: 'bg-accent-soft text-accent-soft-foreground',
|
|
||||||
success: 'bg-success-soft text-success-soft-foreground',
|
|
||||||
warning: 'bg-warning-soft text-warning-soft-foreground',
|
|
||||||
danger: 'bg-danger-soft text-danger-soft-foreground',
|
|
||||||
default: 'bg-default-soft text-muted',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function StatCard({ icon, label, value, trend, color = 'default' }: Props) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="flex flex-row items-center gap-3 p-3">
|
|
||||||
<div className={`flex size-8 shrink-0 items-center justify-center rounded-lg ${iconClasses[color]}`}>
|
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="truncate text-xs text-muted">{label}</div>
|
|
||||||
<div className="text-lg font-bold leading-tight tracking-tight">{value}</div>
|
|
||||||
</div>
|
|
||||||
{trend && (
|
|
||||||
<span className={`shrink-0 text-xs font-medium ${trend.startsWith('+') ? 'text-success' : trend.startsWith('-') ? 'text-danger' : 'text-muted'}`}>
|
|
||||||
{trend}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardHeader, CardContent, Chip } from '@heroui/react';
|
|
||||||
import { GripVertical } from 'lucide-react';
|
|
||||||
import { formatDate } from '../../utils/formatters';
|
|
||||||
|
|
||||||
type TicketRecord = {
|
|
||||||
id: string;
|
|
||||||
topic?: string;
|
|
||||||
createdAt?: string | number;
|
|
||||||
category?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Column = { key: string; label: string; color: 'warning' | 'accent' | 'default' | 'success' };
|
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
|
||||||
{ key: 'neu', label: 'Neu', color: 'warning' },
|
|
||||||
{ key: 'in_bearbeitung', label: 'In Bearbeitung', color: 'accent' },
|
|
||||||
{ key: 'warten_auf_user', label: 'Warten auf User', color: 'default' },
|
|
||||||
{ key: 'erledigt', label: 'Erledigt', color: 'success' },
|
|
||||||
];
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
pipeline: Record<string, TicketRecord[]>;
|
|
||||||
updateTicketStatus: (ticketId: string, status: string) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function TicketKanban({ pipeline, updateTicketStatus }: Props) {
|
|
||||||
const [draggedId, setDraggedId] = useState<string | null>(null);
|
|
||||||
const [dragOverKey, setDragOverKey] = useState<string | null>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-5 grid gap-4 lg:grid-cols-2 2xl:grid-cols-4">
|
|
||||||
{COLUMNS.map(({ key, label, color }) => {
|
|
||||||
const items = pipeline[key] || [];
|
|
||||||
const isDropTarget = dragOverKey === key;
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={key}
|
|
||||||
className={`transition-colors ${isDropTarget ? 'border-accent' : 'border-transparent'} border-2`}
|
|
||||||
onDragOver={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (dragOverKey !== key) setDragOverKey(key);
|
|
||||||
}}
|
|
||||||
onDragLeave={() => setDragOverKey((k) => (k === key ? null : k))}
|
|
||||||
onDrop={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const id = e.dataTransfer.getData('text/plain') || draggedId;
|
|
||||||
if (id) updateTicketStatus(id, key);
|
|
||||||
setDragOverKey(null);
|
|
||||||
setDraggedId(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardHeader className="px-4 pt-4 pb-0">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className={`size-2.5 rounded-full bg-${color}`} />
|
|
||||||
<h3 className="text-sm font-semibold">{label}</h3>
|
|
||||||
<Chip size="sm" variant="soft" color={color}>{items.length}</Chip>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex min-h-[120px] flex-col gap-2 p-4">
|
|
||||||
{items.length ? items.map((t) => (
|
|
||||||
<div
|
|
||||||
key={t.id}
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) => {
|
|
||||||
e.dataTransfer.setData('text/plain', t.id);
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
setDraggedId(t.id);
|
|
||||||
}}
|
|
||||||
onDragEnd={() => {
|
|
||||||
setDraggedId(null);
|
|
||||||
setDragOverKey(null);
|
|
||||||
}}
|
|
||||||
className={`bg-surface-tertiary flex cursor-grab items-start gap-2 rounded-xl p-3 text-sm active:cursor-grabbing ${
|
|
||||||
draggedId === t.id ? 'opacity-40' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<GripVertical size={14} className="mt-0.5 shrink-0 text-muted" />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="font-medium truncate">{t.topic || t.id}</div>
|
|
||||||
<div className="mt-1 flex items-center gap-1.5 text-xs text-muted">
|
|
||||||
{t.category && <span>{t.category}</span>}
|
|
||||||
{t.category && <span>·</span>}
|
|
||||||
<span>{formatDate(t.createdAt)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<p className="py-4 text-center text-xs text-muted">Tickets hierher ziehen</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,745 +0,0 @@
|
|||||||
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
||||||
import { apiFetch } from '../utils/api';
|
|
||||||
import type {
|
|
||||||
AppConfig, User, Guild, NavKey, TicketRecord, StatusService,
|
|
||||||
EventItem, ReactionRoleSet, ModuleItem, LogEntry, SettingsState,
|
|
||||||
SupportLoginConfig, SupportLoginStatus, RegisterForm, RegisterFormField,
|
|
||||||
RegisterApplication, MusicSession, StaffTask, WatchlistEntry, GrowthStats,
|
|
||||||
InviteBreakdownEntry, RecentJoinEntry, PermissionScanResult, InfoPanel, InfoPanelType,
|
|
||||||
TicketTopicConfig
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
const appConfig: AppConfig = (window as any).__PAPO__ || {};
|
|
||||||
|
|
||||||
type AppState = {
|
|
||||||
user: User | null;
|
|
||||||
guilds: Guild[];
|
|
||||||
currentGuildId: string;
|
|
||||||
section: NavKey;
|
|
||||||
guildInfo: any;
|
|
||||||
overview: any;
|
|
||||||
activity: any;
|
|
||||||
logs: LogEntry[];
|
|
||||||
tickets: TicketRecord[];
|
|
||||||
pipeline: Record<string, TicketRecord[]>;
|
|
||||||
sla: any;
|
|
||||||
automations: any[];
|
|
||||||
kbArticles: any[];
|
|
||||||
settings: SettingsState;
|
|
||||||
modules: ModuleItem[];
|
|
||||||
birthday: any;
|
|
||||||
reactionRoles: ReactionRoleSet[];
|
|
||||||
statuspage: any;
|
|
||||||
serverStats: any;
|
|
||||||
events: EventItem[];
|
|
||||||
admin: any;
|
|
||||||
statusMessage: string;
|
|
||||||
loading: boolean;
|
|
||||||
supportLogin: { config: SupportLoginConfig; status: SupportLoginStatus; supportRoleId?: string } | null;
|
|
||||||
registerForms: RegisterForm[];
|
|
||||||
registerApps: RegisterApplication[];
|
|
||||||
musicStatus: { activeGuilds: number; sessions: MusicSession[] };
|
|
||||||
automodStrikes: { userId: string; count: number; lastAt: string; reasons: string[] }[];
|
|
||||||
registerStatusFilter: string;
|
|
||||||
registerFormFilter: string;
|
|
||||||
selectedAppId: string | null;
|
|
||||||
appNotes: any[];
|
|
||||||
appHistory: any[];
|
|
||||||
noteDraft: string;
|
|
||||||
tasks: StaffTask[];
|
|
||||||
taskDraft: { title: string; description: string };
|
|
||||||
watchlistEntries: WatchlistEntry[];
|
|
||||||
growthStats: GrowthStats | null;
|
|
||||||
inviteBreakdown: InviteBreakdownEntry[];
|
|
||||||
recentJoins: RecentJoinEntry[];
|
|
||||||
permissionScan: PermissionScanResult | null;
|
|
||||||
permissionScanLoading: boolean;
|
|
||||||
infoPanels: InfoPanel[];
|
|
||||||
panelDraft: { type: InfoPanelType; channelId: string; title: string; description: string; items: string };
|
|
||||||
ticketTopics: Record<string, TicketTopicConfig>;
|
|
||||||
ticketTopicDraft: { topic: string; roleId: string; questions: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
type AppContextType = AppState & {
|
|
||||||
setCurrentGuildId: (id: string) => void;
|
|
||||||
setSection: (key: NavKey) => void;
|
|
||||||
setSettings: (s: SettingsState | ((prev: SettingsState) => SettingsState)) => void;
|
|
||||||
setBirthday: (s: any | ((prev: any) => any)) => void;
|
|
||||||
setSupportLogin: (s: any | ((prev: any) => any)) => void;
|
|
||||||
setStatusDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
setStatsDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
setStatusMessage: (msg: string) => void;
|
|
||||||
loadGuildData: (guildId: string) => Promise<void>;
|
|
||||||
saveSettingsPayload: (payload: Record<string, any>, okMessage: string) => Promise<void>;
|
|
||||||
saveBirthday: () => Promise<void>;
|
|
||||||
saveStatuspage: () => Promise<void>;
|
|
||||||
saveServerStats: () => Promise<void>;
|
|
||||||
toggleModule: (key: string, enabled: boolean) => Promise<void>;
|
|
||||||
handleLogout: () => void;
|
|
||||||
loadTicketData: (guildId: string) => Promise<void>;
|
|
||||||
loadTicketMessages: (ticketId: string) => Promise<void>;
|
|
||||||
updateTicketStatus: (ticketId: string, status: string) => Promise<void>;
|
|
||||||
closeTicket: (ticketId: string) => Promise<void>;
|
|
||||||
saveAutomation: () => Promise<void>;
|
|
||||||
saveKbArticle: () => Promise<void>;
|
|
||||||
updateKbArticle: (id: string) => Promise<void>;
|
|
||||||
deleteKbArticle: (id: string) => Promise<void>;
|
|
||||||
updateAutomation: (id: string) => Promise<void>;
|
|
||||||
deleteAutomation: (id: string) => Promise<void>;
|
|
||||||
saveSupportLogin: () => Promise<void>;
|
|
||||||
saveForm: () => Promise<void>;
|
|
||||||
deleteForm: (id: string) => Promise<void>;
|
|
||||||
sendFormPanel: (formId: string) => Promise<void>;
|
|
||||||
addStatusService: () => Promise<void>;
|
|
||||||
deleteStatusService: (id: string) => Promise<void>;
|
|
||||||
addStatsItem: () => Promise<void>;
|
|
||||||
deleteStatsItem: (index: number) => Promise<void>;
|
|
||||||
saveEvent: () => Promise<void>;
|
|
||||||
deleteEvent: (id: string) => Promise<void>;
|
|
||||||
saveReactionRole: () => Promise<void>;
|
|
||||||
ticketTab: string;
|
|
||||||
setTicketTab: (tab: string) => void;
|
|
||||||
automationDraft: any;
|
|
||||||
setAutomationDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
kbDraft: any;
|
|
||||||
setKbDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
eventDraft: any;
|
|
||||||
setEventDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
statusDraft: any;
|
|
||||||
statsDraft: any;
|
|
||||||
reactionDraft: any;
|
|
||||||
setReactionDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
formDraft: any;
|
|
||||||
setFormDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
editingFormId: string | null;
|
|
||||||
setEditingFormId: (id: string | null) => void;
|
|
||||||
registerTab: string;
|
|
||||||
setRegisterTab: (tab: string) => void;
|
|
||||||
statusServiceDraft: any;
|
|
||||||
setStatusServiceDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
statsItemDraft: any;
|
|
||||||
setStatsItemDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
ticketDetail: TicketRecord | null;
|
|
||||||
setTicketDetail: (t: TicketRecord | null) => void;
|
|
||||||
ticketMessages: any[];
|
|
||||||
kbEditDraft: any;
|
|
||||||
setKbEditDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
automationEditDraft: any;
|
|
||||||
setAutomationEditDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
loadAutomodStrikes: () => Promise<void>;
|
|
||||||
resetAutomodStrike: (userId: string) => Promise<void>;
|
|
||||||
setRegisterStatusFilter: (v: string) => void;
|
|
||||||
setRegisterFormFilter: (v: string) => void;
|
|
||||||
loadRegisterApps: (overrides?: { status?: string; formId?: string }) => Promise<void>;
|
|
||||||
openAppDetail: (id: string) => Promise<void>;
|
|
||||||
setNoteDraft: (v: string) => void;
|
|
||||||
addAppNote: () => Promise<void>;
|
|
||||||
setTaskDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
createTask: () => Promise<void>;
|
|
||||||
updateTaskStatus: (id: string, status: string) => Promise<void>;
|
|
||||||
deleteTask: (id: string) => Promise<void>;
|
|
||||||
loadWatchlist: () => Promise<void>;
|
|
||||||
removeFromWatchlist: (userId: string) => Promise<void>;
|
|
||||||
loadGrowthStats: () => Promise<void>;
|
|
||||||
loadInviteBreakdown: () => Promise<void>;
|
|
||||||
loadPermissionScan: () => Promise<void>;
|
|
||||||
loadInfoPanels: () => Promise<void>;
|
|
||||||
setPanelDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
createInfoPanel: () => Promise<void>;
|
|
||||||
deleteInfoPanel: (id: string) => Promise<void>;
|
|
||||||
loadTicketTopics: () => Promise<void>;
|
|
||||||
setTicketTopicDraft: (s: any | ((prev: any) => any)) => void;
|
|
||||||
saveTicketTopic: () => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppContext = createContext<AppContextType | null>(null);
|
|
||||||
|
|
||||||
export function AppProvider({ children }: { children: ReactNode }) {
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
const [guilds, setGuilds] = useState<Guild[]>([]);
|
|
||||||
const [currentGuildId, setCurrentGuildId] = useState(appConfig.initialGuildId || '');
|
|
||||||
const [section, setSectionState] = useState<NavKey>('overview');
|
|
||||||
const [guildInfo, setGuildInfo] = useState<any>(null);
|
|
||||||
const [overview, setOverview] = useState<any>(null);
|
|
||||||
const [activity, setActivity] = useState<any>(null);
|
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
||||||
const [tickets, setTickets] = useState<TicketRecord[]>([]);
|
|
||||||
const [pipeline, setPipeline] = useState<Record<string, TicketRecord[]>>({});
|
|
||||||
const [sla, setSla] = useState<any>({ supporters: [], days: [] });
|
|
||||||
const [automations, setAutomations] = useState<any[]>([]);
|
|
||||||
const [kbArticles, setKbArticles] = useState<any[]>([]);
|
|
||||||
const [settings, setSettings] = useState<SettingsState>({});
|
|
||||||
const [modules, setModules] = useState<ModuleItem[]>([]);
|
|
||||||
const [birthday, setBirthday] = useState<any>({ config: {}, birthdays: [] });
|
|
||||||
const [reactionRoles, setReactionRoles] = useState<ReactionRoleSet[]>([]);
|
|
||||||
const [statuspage, setStatuspage] = useState<any>({ services: [] });
|
|
||||||
const [serverStats, setServerStats] = useState<any>({ items: [] });
|
|
||||||
const [events, setEvents] = useState<EventItem[]>([]);
|
|
||||||
const [admin, setAdmin] = useState<any>({ overview: null, activity: null, logs: [] });
|
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
|
||||||
const [ticketTab, setTicketTab] = useState('overview');
|
|
||||||
const [automationDraft, setAutomationDraft] = useState({ name: '', conditionValue: '', actionValue: '' });
|
|
||||||
const [kbDraft, setKbDraft] = useState({ title: '', keywords: '', content: '' });
|
|
||||||
const [eventDraft, setEventDraft] = useState({ title: '', description: '', channelId: '', startsAt: '' });
|
|
||||||
const [statusDraft, setStatusDraft] = useState<any>(null);
|
|
||||||
const [statsDraft, setStatsDraft] = useState<any>(null);
|
|
||||||
const [reactionDraft, setReactionDraft] = useState<{ title: string; channelId: string; entries: { emoji: string; roleId: string; label: string; description: string }[] }>({ title: '', channelId: '', entries: [] });
|
|
||||||
const [supportLogin, setSupportLogin] = useState<{ config: SupportLoginConfig; status: SupportLoginStatus; supportRoleId?: string } | null>(null);
|
|
||||||
const [registerForms, setRegisterForms] = useState<RegisterForm[]>([]);
|
|
||||||
const [registerApps, setRegisterApps] = useState<RegisterApplication[]>([]);
|
|
||||||
const [formDraft, setFormDraft] = useState({ name: '', description: '', reviewChannelId: '', notifyRoleIds: '', fields: '' });
|
|
||||||
const [editingFormId, setEditingFormId] = useState<string | null>(null);
|
|
||||||
const [registerTab, setRegisterTab] = useState('forms');
|
|
||||||
const [musicStatus, setMusicStatus] = useState<{ activeGuilds: number; sessions: MusicSession[] }>({ activeGuilds: 0, sessions: [] });
|
|
||||||
const [kbEditDraft, setKbEditDraft] = useState<{ id: string; title: string; keywords: string; content: string } | null>(null);
|
|
||||||
const [automationEditDraft, setAutomationEditDraft] = useState<{ id: string; name: string; conditionValue: string; actionValue: string } | null>(null);
|
|
||||||
const [statusServiceDraft, setStatusServiceDraft] = useState<{ id?: string; name: string; url: string; status: string }>({ name: '', url: '', status: 'unknown' });
|
|
||||||
const [statsItemDraft, setStatsItemDraft] = useState<{ id?: string; label: string; type: string }>({ label: '', type: 'members' });
|
|
||||||
const [ticketDetail, setTicketDetail] = useState<TicketRecord | null>(null);
|
|
||||||
const [ticketMessages, setTicketMessages] = useState<any[]>([]);
|
|
||||||
const [automodStrikes, setAutomodStrikes] = useState<{ userId: string; count: number; lastAt: string; reasons: string[] }[]>([]);
|
|
||||||
const [registerStatusFilter, setRegisterStatusFilter] = useState('');
|
|
||||||
const [registerFormFilter, setRegisterFormFilter] = useState('');
|
|
||||||
const [selectedAppId, setSelectedAppId] = useState<string | null>(null);
|
|
||||||
const [appNotes, setAppNotes] = useState<any[]>([]);
|
|
||||||
const [appHistory, setAppHistory] = useState<any[]>([]);
|
|
||||||
const [noteDraft, setNoteDraft] = useState('');
|
|
||||||
const [tasks, setTasks] = useState<StaffTask[]>([]);
|
|
||||||
const [taskDraft, setTaskDraft] = useState({ title: '', description: '' });
|
|
||||||
const [watchlistEntries, setWatchlistEntries] = useState<WatchlistEntry[]>([]);
|
|
||||||
const [growthStats, setGrowthStats] = useState<GrowthStats | null>(null);
|
|
||||||
const [inviteBreakdown, setInviteBreakdown] = useState<InviteBreakdownEntry[]>([]);
|
|
||||||
const [recentJoins, setRecentJoins] = useState<RecentJoinEntry[]>([]);
|
|
||||||
const [permissionScan, setPermissionScan] = useState<PermissionScanResult | null>(null);
|
|
||||||
const [permissionScanLoading, setPermissionScanLoading] = useState(false);
|
|
||||||
const [infoPanels, setInfoPanels] = useState<InfoPanel[]>([]);
|
|
||||||
const [panelDraft, setPanelDraft] = useState<{ type: InfoPanelType; channelId: string; title: string; description: string; items: string }>({ type: 'rules', channelId: '', title: '', description: '', items: '' });
|
|
||||||
const [ticketTopics, setTicketTopics] = useState<Record<string, TicketTopicConfig>>({});
|
|
||||||
const [ticketTopicDraft, setTicketTopicDraft] = useState({ topic: '', roleId: '', questions: '' });
|
|
||||||
|
|
||||||
const setSection = useCallback((key: NavKey) => {
|
|
||||||
setSectionState(key);
|
|
||||||
window.location.hash = key;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hash = window.location.hash.replace('#', '') as NavKey;
|
|
||||||
const validKeys: NavKey[] = ['overview', 'tickets', 'supportlogin', 'automod', 'welcome', 'dynamicvoice', 'birthday', 'reactionroles', 'statuspage', 'serverstats', 'register', 'music', 'settings', 'modules', 'events', 'tasks', 'watchlist', 'branding', 'growth', 'permissions', 'panels', 'admin'];
|
|
||||||
if (validKeys.includes(hash)) setSectionState(hash);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentGuildId) loadGuildData(currentGuildId);
|
|
||||||
}, [currentGuildId]);
|
|
||||||
|
|
||||||
useEffect(() => { bootstrap(); }, []);
|
|
||||||
|
|
||||||
async function bootstrap() {
|
|
||||||
try {
|
|
||||||
const me = await apiFetch<{ user: User }>('/me');
|
|
||||||
const guildRes = await apiFetch<{ guilds: Guild[] }>('/guilds');
|
|
||||||
setUser(me.user);
|
|
||||||
setGuilds(guildRes.guilds || []);
|
|
||||||
if (!currentGuildId && guildRes.guilds?.length) setCurrentGuildId(guildRes.guilds[0].id);
|
|
||||||
} finally { setLoading(false); }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTicketData(guildId: string) {
|
|
||||||
try {
|
|
||||||
const [ticketRes, pipelineRes, slaRes, automationRes, kbRes] = await Promise.all([
|
|
||||||
apiFetch<any>(`/tickets?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/tickets/pipeline?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/tickets/sla?guildId=${encodeURIComponent(guildId)}&range=30`),
|
|
||||||
apiFetch<any>(`/automations?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/kb?guildId=${encodeURIComponent(guildId)}`)
|
|
||||||
]);
|
|
||||||
setTickets(ticketRes.tickets || []);
|
|
||||||
setPipeline(pipelineRes.pipeline || {});
|
|
||||||
setSla(slaRes || { supporters: [], days: [] });
|
|
||||||
setAutomations(automationRes.rules || []);
|
|
||||||
setKbArticles(kbRes.articles || []);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGuildData(guildId: string) {
|
|
||||||
setStatusMessage('Lade Daten...');
|
|
||||||
try {
|
|
||||||
const [guildInfoRes, overviewRes, activityRes, logsRes, settingsRes, modulesRes,
|
|
||||||
birthdayRes, reactionRes, statusRes, statsRes, eventsRes, supportLoginRes,
|
|
||||||
registerFormsRes, registerAppsRes, tasksRes, watchlistRes] = await Promise.all([
|
|
||||||
apiFetch<any>(`/guild/info?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/overview?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/guild/activity?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/guild/logs?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/settings?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/modules?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/birthday?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/reactionroles?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/statuspage?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/server-stats?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/events?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/tickets/support-login?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/register/forms?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/register/apps?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/tasks?guildId=${encodeURIComponent(guildId)}`),
|
|
||||||
apiFetch<any>(`/watchlist?guildId=${encodeURIComponent(guildId)}`)
|
|
||||||
]);
|
|
||||||
setGuildInfo(guildInfoRes.guild || null);
|
|
||||||
setOverview(overviewRes);
|
|
||||||
setMusicStatus(overviewRes.music || { activeGuilds: 0, sessions: [] });
|
|
||||||
setActivity(activityRes.activity || {});
|
|
||||||
setLogs(logsRes.logs || []);
|
|
||||||
setSettings(settingsRes.settings || {});
|
|
||||||
setModules(modulesRes.modules || []);
|
|
||||||
setBirthday(birthdayRes);
|
|
||||||
setReactionRoles(reactionRes.sets || []);
|
|
||||||
setStatuspage(statusRes.config || { services: [] });
|
|
||||||
setServerStats(statsRes.config || { items: [] });
|
|
||||||
setStatsDraft(statsRes.config || { items: [] });
|
|
||||||
setStatusDraft(statusRes.config || { services: [] });
|
|
||||||
setEvents(eventsRes.events || []);
|
|
||||||
setSupportLogin(supportLoginRes);
|
|
||||||
setRegisterForms(registerFormsRes.forms || []);
|
|
||||||
setRegisterApps(registerAppsRes.applications || []);
|
|
||||||
setTasks(tasksRes.tasks || []);
|
|
||||||
setWatchlistEntries(watchlistRes.entries || []);
|
|
||||||
setReactionDraft({ title: '', channelId: '', entries: [] });
|
|
||||||
await Promise.all([loadTicketData(guildId), loadAdminData()]);
|
|
||||||
setStatusMessage('');
|
|
||||||
} catch { setStatusMessage('Daten konnten nicht geladen werden'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAdminData() {
|
|
||||||
if (!user?.isAdmin) return;
|
|
||||||
try {
|
|
||||||
const [overviewRes, activityRes, logsRes] = await Promise.all([
|
|
||||||
apiFetch<any>('/admin/overview'),
|
|
||||||
apiFetch<any>('/admin/activity'),
|
|
||||||
apiFetch<any>('/admin/logs')
|
|
||||||
]);
|
|
||||||
setAdmin({
|
|
||||||
overview: overviewRes.overview || {},
|
|
||||||
activity: activityRes.points || [],
|
|
||||||
logs: logsRes.logs || [],
|
|
||||||
guildList: overviewRes.guildList || [],
|
|
||||||
usage: overviewRes.usage || {}
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSettingsPayload(payload: Record<string, any>, okMessage: string) {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
await apiFetch('/settings', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, ...payload }) });
|
|
||||||
setStatusMessage(okMessage);
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveBirthday() {
|
|
||||||
await apiFetch('/birthday', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
guildId: currentGuildId,
|
|
||||||
enabled: birthday.config?.enabled ?? true,
|
|
||||||
channelId: birthday.config?.channelId || '',
|
|
||||||
sendHour: birthday.config?.sendHour || 9,
|
|
||||||
messageTemplate: birthday.config?.messageTemplate || ''
|
|
||||||
})
|
|
||||||
});
|
|
||||||
setStatusMessage('Birthday gespeichert');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveStatuspage() {
|
|
||||||
await apiFetch('/statuspage', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, config: statusDraft }) });
|
|
||||||
setStatusMessage('Statuspage gespeichert');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveServerStats() {
|
|
||||||
await apiFetch('/server-stats', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, config: statsDraft }) });
|
|
||||||
setStatusMessage('Server Stats gespeichert');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveEvent() {
|
|
||||||
if (!eventDraft.title) return;
|
|
||||||
await apiFetch('/events', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
guildId: currentGuildId, title: eventDraft.title,
|
|
||||||
description: eventDraft.description, channelId: eventDraft.channelId || undefined,
|
|
||||||
startsAt: eventDraft.startsAt || undefined
|
|
||||||
})
|
|
||||||
});
|
|
||||||
setEventDraft({ title: '', description: '', channelId: '', startsAt: '' });
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
setStatusMessage('Event gespeichert');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteEvent(id: string) {
|
|
||||||
await apiFetch(`/events/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) });
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveReactionRole() {
|
|
||||||
const entries = (reactionDraft.entries as any[]).filter((e) => e.emoji && e.roleId);
|
|
||||||
await apiFetch('/reactionroles', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, channelId: reactionDraft.channelId, title: reactionDraft.title, entries }) });
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
setReactionDraft({ title: '', channelId: '', entries: [] });
|
|
||||||
setStatusMessage('Reaction Role gespeichert');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleModule(key: string, enabled: boolean) {
|
|
||||||
await saveSettingsPayload({ [key]: enabled }, `${key} aktualisiert`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveAutomation() {
|
|
||||||
await apiFetch('/automations', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
guildId: currentGuildId, name: automationDraft.name || 'Automation',
|
|
||||||
condition: { category: automationDraft.conditionValue },
|
|
||||||
action: { type: 'reminder', message: automationDraft.actionValue || 'Reminder' }, active: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
setAutomationDraft({ name: '', conditionValue: '', actionValue: '' });
|
|
||||||
await loadTicketData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveKbArticle() {
|
|
||||||
await apiFetch('/kb', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ guildId: currentGuildId, title: kbDraft.title || 'Artikel', keywords: kbDraft.keywords, content: kbDraft.content })
|
|
||||||
});
|
|
||||||
setKbDraft({ title: '', keywords: '', content: '' });
|
|
||||||
await loadTicketData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateKbArticle(id: string) {
|
|
||||||
if (!kbEditDraft) return;
|
|
||||||
await apiFetch(`/kb/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({ guildId: currentGuildId, title: kbEditDraft.title, keywords: kbEditDraft.keywords, content: kbEditDraft.content })
|
|
||||||
});
|
|
||||||
setKbEditDraft(null);
|
|
||||||
setStatusMessage('KB-Artikel aktualisiert');
|
|
||||||
await loadTicketData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteKbArticle(id: string) {
|
|
||||||
await apiFetch(`/kb/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) });
|
|
||||||
setStatusMessage('KB-Artikel gelöscht');
|
|
||||||
await loadTicketData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateAutomation(id: string) {
|
|
||||||
if (!automationEditDraft) return;
|
|
||||||
await apiFetch(`/automations/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({ guildId: currentGuildId, name: automationEditDraft.name, condition: { category: automationEditDraft.conditionValue }, action: { type: 'reminder', message: automationEditDraft.actionValue }, active: true })
|
|
||||||
});
|
|
||||||
setAutomationEditDraft(null);
|
|
||||||
setStatusMessage('Automation aktualisiert');
|
|
||||||
await loadTicketData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteAutomation(id: string) {
|
|
||||||
await apiFetch(`/automations/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) });
|
|
||||||
setStatusMessage('Automation gelöscht');
|
|
||||||
await loadTicketData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSupportLogin() {
|
|
||||||
if (!supportLogin) return;
|
|
||||||
await apiFetch('/tickets/support-login', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ guildId: currentGuildId, ...supportLogin.config })
|
|
||||||
});
|
|
||||||
setStatusMessage('Support Login gespeichert');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveForm() {
|
|
||||||
const fields = formDraft.fields.split('\n').filter(Boolean).map((line) => {
|
|
||||||
const parts = line.split('|').map((s) => s.trim());
|
|
||||||
return { label: parts[0] || 'Feld', type: (parts[1] || 'text') as any, required: parts[2] === 'required', options: parts[3] ? parts[3].split(',').map((s) => s.trim()) : undefined };
|
|
||||||
});
|
|
||||||
const body: any = { guildId: currentGuildId, name: formDraft.name, description: formDraft.description, reviewChannelId: formDraft.reviewChannelId || undefined, notifyRoleIds: formDraft.notifyRoleIds.split(',').map((s) => s.trim()).filter(Boolean), fields, isActive: true };
|
|
||||||
if (editingFormId) {
|
|
||||||
await apiFetch(`/register/forms/${editingFormId}`, { method: 'PUT', body: JSON.stringify(body) });
|
|
||||||
setStatusMessage('Formular aktualisiert');
|
|
||||||
} else {
|
|
||||||
await apiFetch('/register/forms', { method: 'POST', body: JSON.stringify(body) });
|
|
||||||
setStatusMessage('Formular erstellt');
|
|
||||||
}
|
|
||||||
setFormDraft({ name: '', description: '', reviewChannelId: '', notifyRoleIds: '', fields: '' });
|
|
||||||
setEditingFormId(null);
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteForm(id: string) {
|
|
||||||
await apiFetch(`/register/forms/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) });
|
|
||||||
setStatusMessage('Formular gelöscht');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendFormPanel(formId: string) {
|
|
||||||
if (!supportLogin?.config?.panelChannelId) { setStatusMessage('Bitte zuerst Support Login konfigurieren'); return; }
|
|
||||||
await apiFetch(`/register/forms/${formId}/panel`, { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, channelId: supportLogin.config.panelChannelId }) });
|
|
||||||
setStatusMessage('Panel gesendet');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addStatusService() {
|
|
||||||
if (!statusServiceDraft.name) return;
|
|
||||||
await apiFetch('/statuspage/service', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ guildId: currentGuildId, name: statusServiceDraft.name, url: statusServiceDraft.url, status: statusServiceDraft.status })
|
|
||||||
});
|
|
||||||
setStatusServiceDraft({ name: '', url: '', status: 'unknown' });
|
|
||||||
setStatusMessage('Service hinzugefügt');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteStatusService(id: string) {
|
|
||||||
await apiFetch(`/statuspage/service/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) });
|
|
||||||
setStatusMessage('Service entfernt');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addStatsItem() {
|
|
||||||
if (!statsItemDraft.label) return;
|
|
||||||
const draft = statsDraft || { enabled: true, categoryName: '', refreshMinutes: 10, items: [] };
|
|
||||||
const items = [...(draft.items || []), { key: statsItemDraft.label.toLowerCase().replace(/\s+/g, '_'), label: statsItemDraft.label, type: statsItemDraft.type }];
|
|
||||||
const updated = { ...draft, items };
|
|
||||||
setStatsDraft(updated);
|
|
||||||
await apiFetch('/server-stats', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, config: updated }) });
|
|
||||||
setStatsItemDraft({ label: '', type: 'members' });
|
|
||||||
setStatusMessage('Stat-Item hinzugefügt');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteStatsItem(index: number) {
|
|
||||||
const draft = statsDraft || { enabled: true, categoryName: '', refreshMinutes: 10, items: [] };
|
|
||||||
const items = (draft.items || []).filter((_: any, i: number) => i !== index);
|
|
||||||
const updated = { ...draft, items };
|
|
||||||
setStatsDraft(updated);
|
|
||||||
await apiFetch('/server-stats', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, config: updated }) });
|
|
||||||
setStatusMessage('Stat-Item entfernt');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTicketMessages(ticketId: string) {
|
|
||||||
const res = await apiFetch<any>(`/tickets/${ticketId}/messages`);
|
|
||||||
setTicketMessages(res.messages || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateTicketStatus(ticketId: string, status: string) {
|
|
||||||
await apiFetch(`/tickets/${ticketId}/status`, { method: 'POST', body: JSON.stringify({ status }) });
|
|
||||||
setStatusMessage('Status aktualisiert');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function closeTicket(ticketId: string) {
|
|
||||||
await apiFetch(`/tickets/${ticketId}/close`, { method: 'POST', body: JSON.stringify({ guildId: currentGuildId }) });
|
|
||||||
setStatusMessage('Ticket geschlossen');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAutomodStrikes() {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
const res = await apiFetch<any>(`/automod/strikes?guildId=${encodeURIComponent(currentGuildId)}`);
|
|
||||||
setAutomodStrikes(res.strikes || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetAutomodStrike(userId: string) {
|
|
||||||
await apiFetch(`/automod/strikes?guildId=${encodeURIComponent(currentGuildId)}&userId=${encodeURIComponent(userId)}`, { method: 'DELETE' });
|
|
||||||
await loadAutomodStrikes();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRegisterApps(overrides?: { status?: string; formId?: string }) {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
const params = new URLSearchParams({ guildId: currentGuildId });
|
|
||||||
const status = overrides?.status ?? registerStatusFilter;
|
|
||||||
const formId = overrides?.formId ?? registerFormFilter;
|
|
||||||
if (status) params.set('status', status);
|
|
||||||
if (formId) params.set('formId', formId);
|
|
||||||
const res = await apiFetch<any>(`/register/apps?${params.toString()}`);
|
|
||||||
setRegisterApps(res.applications || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openAppDetail(id: string) {
|
|
||||||
if (selectedAppId === id) {
|
|
||||||
setSelectedAppId(null);
|
|
||||||
setAppNotes([]);
|
|
||||||
setAppHistory([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSelectedAppId(id);
|
|
||||||
const [notesRes, historyRes] = await Promise.all([
|
|
||||||
apiFetch<any>(`/register/apps/${id}/notes`),
|
|
||||||
apiFetch<any>(`/register/apps/${id}/history`)
|
|
||||||
]);
|
|
||||||
setAppNotes(notesRes.notes || []);
|
|
||||||
setAppHistory(historyRes.applications || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addAppNote() {
|
|
||||||
if (!selectedAppId || !noteDraft.trim()) return;
|
|
||||||
await apiFetch(`/register/apps/${selectedAppId}/notes`, { method: 'POST', body: JSON.stringify({ body: noteDraft.trim() }) });
|
|
||||||
setNoteDraft('');
|
|
||||||
const notesRes = await apiFetch<any>(`/register/apps/${selectedAppId}/notes`);
|
|
||||||
setAppNotes(notesRes.notes || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createTask() {
|
|
||||||
if (!taskDraft.title.trim() || !currentGuildId) return;
|
|
||||||
await apiFetch('/tasks', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ guildId: currentGuildId, title: taskDraft.title.trim(), description: taskDraft.description.trim() || undefined })
|
|
||||||
});
|
|
||||||
setTaskDraft({ title: '', description: '' });
|
|
||||||
setStatusMessage('Aufgabe erstellt');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateTaskStatus(id: string, status: string) {
|
|
||||||
await apiFetch(`/tasks/${id}/status`, { method: 'POST', body: JSON.stringify({ status }) });
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteTask(id: string) {
|
|
||||||
await apiFetch(`/tasks/${id}`, { method: 'DELETE', body: JSON.stringify({ guildId: currentGuildId }) });
|
|
||||||
setStatusMessage('Aufgabe gelöscht');
|
|
||||||
await loadGuildData(currentGuildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadWatchlist() {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
const res = await apiFetch<any>(`/watchlist?guildId=${encodeURIComponent(currentGuildId)}`);
|
|
||||||
setWatchlistEntries(res.entries || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeFromWatchlist(userId: string) {
|
|
||||||
await apiFetch(`/watchlist?guildId=${encodeURIComponent(currentGuildId)}&userId=${encodeURIComponent(userId)}`, { method: 'DELETE' });
|
|
||||||
setStatusMessage('Von der Watchlist entfernt');
|
|
||||||
await loadWatchlist();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadGrowthStats() {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
const res = await apiFetch<any>(`/growth?guildId=${encodeURIComponent(currentGuildId)}`);
|
|
||||||
setGrowthStats(res.stats || null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadInviteBreakdown() {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
const res = await apiFetch<any>(`/growth/invites?guildId=${encodeURIComponent(currentGuildId)}`);
|
|
||||||
setInviteBreakdown(res.breakdown || []);
|
|
||||||
setRecentJoins(res.recentJoins || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPermissionScan() {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
setPermissionScanLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await apiFetch<any>(`/permissions/scan?guildId=${encodeURIComponent(currentGuildId)}`);
|
|
||||||
setPermissionScan(res.result || null);
|
|
||||||
} finally {
|
|
||||||
setPermissionScanLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadInfoPanels() {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
const res = await apiFetch<any>(`/panels?guildId=${encodeURIComponent(currentGuildId)}`);
|
|
||||||
setInfoPanels(res.panels || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createInfoPanel() {
|
|
||||||
if (!currentGuildId || !panelDraft.channelId || !panelDraft.title) return;
|
|
||||||
const items = panelDraft.type === 'faq'
|
|
||||||
? panelDraft.items.split('\n').map((l) => l.trim()).filter(Boolean).slice(0, 25).map((line) => {
|
|
||||||
const [q, ...rest] = line.split(':');
|
|
||||||
return { question: (q || '').trim(), answer: rest.join(':').trim() || 'Keine Antwort hinterlegt.' };
|
|
||||||
}).filter((i) => i.question)
|
|
||||||
: undefined;
|
|
||||||
await apiFetch('/panels', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ guildId: currentGuildId, channelId: panelDraft.channelId, type: panelDraft.type, title: panelDraft.title, description: panelDraft.description || undefined, items })
|
|
||||||
});
|
|
||||||
setPanelDraft({ type: 'rules', channelId: '', title: '', description: '', items: '' });
|
|
||||||
setStatusMessage('Panel wurde gepostet');
|
|
||||||
await loadInfoPanels();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteInfoPanel(id: string) {
|
|
||||||
await apiFetch(`/panels/${id}?guildId=${encodeURIComponent(currentGuildId)}`, { method: 'DELETE' });
|
|
||||||
setStatusMessage('Panel gelöscht');
|
|
||||||
await loadInfoPanels();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTicketTopics() {
|
|
||||||
if (!currentGuildId) return;
|
|
||||||
const res = await apiFetch<any>(`/ticketconfig?guildId=${encodeURIComponent(currentGuildId)}`);
|
|
||||||
setTicketTopics(res.topics || {});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveTicketTopic() {
|
|
||||||
if (!currentGuildId || !ticketTopicDraft.topic.trim()) return;
|
|
||||||
const questions = ticketTopicDraft.questions.split('\n').map((q) => q.trim()).filter(Boolean).slice(0, 5);
|
|
||||||
await apiFetch('/ticketconfig', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ guildId: currentGuildId, topic: ticketTopicDraft.topic.trim().toLowerCase(), roleId: ticketTopicDraft.roleId || undefined, questions })
|
|
||||||
});
|
|
||||||
setTicketTopicDraft({ topic: '', roleId: '', questions: '' });
|
|
||||||
setStatusMessage('Ticket-Kategorie gespeichert');
|
|
||||||
await loadTicketTopics();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogout = useCallback(() => {
|
|
||||||
window.location.href = `${appConfig.baseAuth || '/auth'}/logout`;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppContext.Provider value={{
|
|
||||||
user, guilds, currentGuildId, section, guildInfo, overview, activity,
|
|
||||||
logs, tickets, pipeline, sla, automations, kbArticles, settings, modules,
|
|
||||||
birthday, reactionRoles, statuspage, serverStats, events, admin, statusMessage,
|
|
||||||
loading, supportLogin, registerForms, registerApps, musicStatus, ticketTab,
|
|
||||||
automationDraft, kbDraft, eventDraft, statusDraft, statsDraft, reactionDraft,
|
|
||||||
formDraft, editingFormId, registerTab, statusServiceDraft, statsItemDraft,
|
|
||||||
ticketDetail, ticketMessages, kbEditDraft, automationEditDraft,
|
|
||||||
automodStrikes, registerStatusFilter, registerFormFilter, selectedAppId,
|
|
||||||
appNotes, appHistory, noteDraft, tasks, taskDraft, watchlistEntries, growthStats,
|
|
||||||
inviteBreakdown, recentJoins, permissionScan, permissionScanLoading, infoPanels,
|
|
||||||
panelDraft, ticketTopics, ticketTopicDraft,
|
|
||||||
setCurrentGuildId, setSection, setSettings, setBirthday, setSupportLogin,
|
|
||||||
setStatusDraft, setStatsDraft, setStatusMessage, loadGuildData,
|
|
||||||
saveSettingsPayload, saveBirthday, saveStatuspage, saveServerStats,
|
|
||||||
toggleModule, handleLogout, loadTicketData, loadTicketMessages,
|
|
||||||
updateTicketStatus, closeTicket, saveAutomation, saveKbArticle,
|
|
||||||
updateKbArticle, deleteKbArticle, updateAutomation, deleteAutomation,
|
|
||||||
saveSupportLogin, saveForm, deleteForm, sendFormPanel, addStatusService,
|
|
||||||
deleteStatusService, addStatsItem, deleteStatsItem, saveEvent, deleteEvent,
|
|
||||||
saveReactionRole, setTicketTab, setAutomationDraft, setKbDraft, setEventDraft,
|
|
||||||
setReactionDraft, setFormDraft, setEditingFormId, setRegisterTab,
|
|
||||||
setStatusServiceDraft, setStatsItemDraft, setTicketDetail, setKbEditDraft,
|
|
||||||
setAutomationEditDraft,
|
|
||||||
loadAutomodStrikes, resetAutomodStrike, setRegisterStatusFilter, setRegisterFormFilter,
|
|
||||||
loadRegisterApps, openAppDetail, setNoteDraft, addAppNote,
|
|
||||||
setTaskDraft, createTask, updateTaskStatus, deleteTask,
|
|
||||||
loadWatchlist, removeFromWatchlist, loadGrowthStats, loadInviteBreakdown,
|
|
||||||
loadPermissionScan, loadInfoPanels, setPanelDraft, createInfoPanel, deleteInfoPanel,
|
|
||||||
loadTicketTopics, setTicketTopicDraft, saveTicketTopic,
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useApp() {
|
|
||||||
const ctx = useContext(AppContext);
|
|
||||||
if (!ctx) throw new Error('useApp must be used within AppProvider');
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
import { apiFetch } from '../utils/api';
|
|
||||||
|
|
||||||
type Channel = { id: string; name: string; type: string; parentId?: string };
|
|
||||||
type Role = { id: string; name: string; color: string };
|
|
||||||
type Category = { id: string; name: string };
|
|
||||||
|
|
||||||
export function useGuildResources(guildId?: string) {
|
|
||||||
const [channels, setChannels] = useState<Channel[]>([]);
|
|
||||||
const [roles, setRoles] = useState<Role[]>([]);
|
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!guildId) return;
|
|
||||||
apiFetch<{ channels: Channel[]; roles: Role[]; categories: Category[] }>(
|
|
||||||
`/guild/resources?guildId=${encodeURIComponent(guildId)}`
|
|
||||||
).then((res) => {
|
|
||||||
setChannels(res.channels || []);
|
|
||||||
setRoles(res.roles || []);
|
|
||||||
setCategories(res.categories || []);
|
|
||||||
}).catch(() => {});
|
|
||||||
}, [guildId]);
|
|
||||||
|
|
||||||
return { channels, roles, categories };
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
|
|
||||||
export function useTheme() {
|
|
||||||
const [dark, setDark] = useState(() => localStorage.getItem('papo-theme') !== 'light');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.classList.toggle('dark', dark);
|
|
||||||
localStorage.setItem('papo-theme', dark ? 'dark' : 'light');
|
|
||||||
}, [dark]);
|
|
||||||
|
|
||||||
const toggle = useCallback(() => setDark((d) => !d), []);
|
|
||||||
|
|
||||||
return { dark, toggle };
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import App from './App';
|
|
||||||
import { AppProvider } from './context/AppContext';
|
|
||||||
import './app.css';
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<AppProvider>
|
|
||||||
<App />
|
|
||||||
</AppProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Chip } from '@heroui/react';
|
|
||||||
import { Activity, Award, ClipboardList, Clock, Eye, Handshake, Server, Terminal, Users } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { StatCard } from '../components/shared/StatCard';
|
|
||||||
import { AppAvatar } from '../components/shared/AppAvatar';
|
|
||||||
import { BarComparisonChart } from '../components/shared/BarComparisonChart';
|
|
||||||
import { formatDate, formatDuration, guildIconUrl } from '../utils/formatters';
|
|
||||||
|
|
||||||
const LEVEL_COLORS: Record<string, 'accent' | 'warning' | 'danger'> = {
|
|
||||||
INFO: 'accent',
|
|
||||||
WARN: 'warning',
|
|
||||||
ERROR: 'danger',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Admin() {
|
|
||||||
const { user, admin } = useApp();
|
|
||||||
|
|
||||||
if (!user?.isAdmin) return null;
|
|
||||||
|
|
||||||
const overview = admin.overview || {};
|
|
||||||
const activityPoints: { hour: string; count: number }[] = admin.activity || [];
|
|
||||||
const logs: { timestamp: number; level: string; message: string; guildId?: string; category?: string }[] = admin.logs || [];
|
|
||||||
const guildList: { id: string; name: string; icon?: string; memberCount: number; boostCount: number }[] = admin.guildList || [];
|
|
||||||
const usage = admin.usage || {};
|
|
||||||
|
|
||||||
const chartItems = activityPoints.slice(-12).map((p) => ({
|
|
||||||
label: new Date(p.hour).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }),
|
|
||||||
value: p.count
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Admin" subtitle="Bot-weite Übersichten">
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<StatCard icon={<Server size={18} />} label="Guilds" value={overview.guildCount ?? '-'} color="accent" />
|
|
||||||
<StatCard icon={<Activity size={18} />} label="Aktive Guilds (24h)" value={overview.activeGuilds24 ?? '-'} color="success" />
|
|
||||||
<StatCard icon={<Clock size={18} />} label="Uptime" value={formatDuration(overview.uptimeMs)} color="warning" />
|
|
||||||
<StatCard icon={<Terminal size={18} />} label="Log-Einträge" value={logs.length} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="mb-3 mt-6 text-sm font-semibold text-muted">Feature-Nutzung</h3>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<StatCard icon={<Award size={18} />} label="Abzeichen vergeben" value={usage.badgesAwarded ?? 0} color="accent" />
|
|
||||||
<StatCard icon={<ClipboardList size={18} />} label="Offene Aufgaben" value={usage.openTasks ?? 0} color="warning" />
|
|
||||||
<StatCard icon={<Eye size={18} />} label="Aktive Watchlist" value={usage.activeWatchlist ?? 0} color="danger" />
|
|
||||||
<StatCard icon={<Handshake size={18} />} label="Offene Partner-Anfragen" value={usage.pendingPartners ?? 0} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Aktivität (letzte Stunden)</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-5">
|
|
||||||
{chartItems.length ? (
|
|
||||||
<BarComparisonChart items={chartItems} />
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<Activity size={24} />
|
|
||||||
Noch keine Aktivität erfasst
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex items-center justify-between px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Server</h3>
|
|
||||||
<Chip size="sm" variant="soft" color="accent">
|
|
||||||
{guildList.length} Guilds
|
|
||||||
</Chip>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex max-h-96 flex-col gap-2 overflow-y-auto p-5">
|
|
||||||
{guildList.length ? guildList.map((g) => (
|
|
||||||
<div key={g.id} className="bg-surface-tertiary flex items-center gap-3 rounded-xl px-4 py-3 text-sm">
|
|
||||||
<AppAvatar size="sm" src={guildIconUrl(g as any)} name={g.name} />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="truncate font-medium">{g.name}</div>
|
|
||||||
<div className="flex items-center gap-3 text-xs text-muted">
|
|
||||||
<span className="flex items-center gap-1"><Users size={12} /> {g.memberCount}</span>
|
|
||||||
{g.boostCount > 0 && <span>💎 {g.boostCount}</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<Server size={20} />
|
|
||||||
Keine Server
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="mt-5">
|
|
||||||
<CardHeader className="flex items-center justify-between px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Letzte Admin Logs</h3>
|
|
||||||
<Chip size="sm" variant="soft" color="warning">
|
|
||||||
{logs.length} Einträge
|
|
||||||
</Chip>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex max-h-96 flex-col gap-2 overflow-y-auto p-5">
|
|
||||||
{logs.length ? logs.slice(0, 30).map((log, i) => (
|
|
||||||
<div key={i} className="bg-surface-tertiary flex items-start gap-3 rounded-xl px-4 py-3 text-sm">
|
|
||||||
<Terminal size={14} className="mt-0.5 text-muted shrink-0" />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Chip size="sm" variant="soft" color={LEVEL_COLORS[log.level] || 'accent'}>{log.level}</Chip>
|
|
||||||
{log.category && <span className="text-xs text-muted">{log.category}</span>}
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-muted">{log.message || '-'}</p>
|
|
||||||
<p className="text-xs text-muted mt-0.5">{formatDate(log.timestamp)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<Terminal size={20} />
|
|
||||||
Keine Logs
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,516 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, Input, TextArea, Button, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { Shield, Link2, Ban, AlertTriangle, Save, Info, MailWarning, AtSign, CaseUpper, X, UserPlus, Plus, Trash2, Siren } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { ChannelSelect, type ChannelOption } from '../components/shared/ChannelSelect';
|
|
||||||
import { RoleSelect, type RoleOption } from '../components/shared/RoleSelect';
|
|
||||||
import { AppSwitch } from '../components/shared/AppSwitch';
|
|
||||||
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
const FILTERS = [
|
|
||||||
{
|
|
||||||
key: 'badWordFilter' as const,
|
|
||||||
icon: <Ban size={16} />,
|
|
||||||
title: 'Bad-Word-Filter',
|
|
||||||
description: 'Entfernt Nachrichten mit unerwünschten Begriffen automatisch.',
|
|
||||||
defaultAction: 'delete' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'linkFilter' as const,
|
|
||||||
icon: <Link2 size={16} />,
|
|
||||||
title: 'Link-Filter',
|
|
||||||
description: 'Blockiert Links, die nicht auf der Whitelist stehen.',
|
|
||||||
defaultAction: 'delete' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'inviteFilter' as const,
|
|
||||||
icon: <MailWarning size={16} />,
|
|
||||||
title: 'Einladungslink-Filter',
|
|
||||||
description: 'Blockiert Discord-Invites unabhängig vom Link-Filter.',
|
|
||||||
defaultAction: 'delete' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'mentionSpamFilter' as const,
|
|
||||||
icon: <AtSign size={16} />,
|
|
||||||
title: 'Mass-Mention-Schutz',
|
|
||||||
description: 'Verhindert Raid-artiges Massen-Pingen von Usern/Rollen.',
|
|
||||||
defaultAction: 'timeout' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'capsFilter' as const,
|
|
||||||
icon: <CaseUpper size={16} />,
|
|
||||||
title: 'Caps-Filter',
|
|
||||||
description: 'Entfernt Nachrichten mit überwiegend GROSSBUCHSTABEN.',
|
|
||||||
defaultAction: 'delete' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'spamFilter' as const,
|
|
||||||
icon: <AlertTriangle size={16} />,
|
|
||||||
title: 'Spam-Filter',
|
|
||||||
description: 'Erkennt und unterdrückt Mehrfachnachrichten in kurzer Zeit.',
|
|
||||||
defaultAction: 'timeout' as const,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ACTION_LABELS: Record<string, string> = {
|
|
||||||
delete: 'Nur löschen',
|
|
||||||
warn: 'Warnen',
|
|
||||||
timeout: 'Timeout',
|
|
||||||
kick: 'Kick',
|
|
||||||
ban: 'Ban',
|
|
||||||
};
|
|
||||||
|
|
||||||
const ESCALATION_ACTION_LABELS: Record<string, string> = {
|
|
||||||
timeout: 'Timeout',
|
|
||||||
kick: 'Kick',
|
|
||||||
ban: 'Ban',
|
|
||||||
};
|
|
||||||
|
|
||||||
function toList(value?: string) {
|
|
||||||
return (value || '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RoleChipList({ roles, values, onChange }: { roles: RoleOption[]; values: string[]; onChange: (v: string[]) => void }) {
|
|
||||||
const [draft, setDraft] = useState('');
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{values.length ? values.map((roleId) => {
|
|
||||||
const role = roles.find((r) => r.id === roleId);
|
|
||||||
return (
|
|
||||||
<div key={roleId} className="bg-default-soft flex items-center gap-1.5 rounded-full py-1 pl-3 pr-1.5 text-xs font-medium">
|
|
||||||
{role?.color && <span className="size-2 rounded-full" style={{ backgroundColor: role.color }} />}
|
|
||||||
{role?.name || roleId}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex size-4 items-center justify-center rounded-full text-muted hover:bg-danger-soft hover:text-danger"
|
|
||||||
onClick={() => onChange(values.filter((id) => id !== roleId))}
|
|
||||||
>
|
|
||||||
<X size={11} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}) : <p className="text-xs text-muted">Keine Rollen-Ausnahmen</p>}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<RoleSelect options={roles.filter((r) => !values.includes(r.id))} value={draft} onChange={setDraft} placeholder="Rolle ausnehmen" />
|
|
||||||
<Button
|
|
||||||
size="sm" variant="tertiary" className="shrink-0" isDisabled={!draft}
|
|
||||||
onPress={() => { if (draft && !values.includes(draft)) { onChange([...values, draft]); setDraft(''); } }}
|
|
||||||
>
|
|
||||||
<Plus size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChannelChipList({ channels, values, onChange }: { channels: ChannelOption[]; values: string[]; onChange: (v: string[]) => void }) {
|
|
||||||
const [draft, setDraft] = useState('');
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{values.length ? values.map((channelId) => {
|
|
||||||
const channel = channels.find((c) => c.id === channelId);
|
|
||||||
return (
|
|
||||||
<div key={channelId} className="bg-default-soft flex items-center gap-1.5 rounded-full py-1 pl-3 pr-1.5 text-xs font-medium">
|
|
||||||
#{channel?.name || channelId}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex size-4 items-center justify-center rounded-full text-muted hover:bg-danger-soft hover:text-danger"
|
|
||||||
onClick={() => onChange(values.filter((id) => id !== channelId))}
|
|
||||||
>
|
|
||||||
<X size={11} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}) : <p className="text-xs text-muted">Keine Kanal-Ausnahmen</p>}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<ChannelSelect options={channels.filter((c) => !values.includes(c.id))} value={draft} onChange={setDraft} placeholder="Kanal ausnehmen" />
|
|
||||||
<Button
|
|
||||||
size="sm" variant="tertiary" className="shrink-0" isDisabled={!draft}
|
|
||||||
onPress={() => { if (draft && !values.includes(draft)) { onChange([...values, draft]); setDraft(''); } }}
|
|
||||||
>
|
|
||||||
<Plus size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Automod() {
|
|
||||||
const {
|
|
||||||
settings, setSettings, saveSettingsPayload, currentGuildId,
|
|
||||||
automodStrikes, loadAutomodStrikes, resetAutomodStrike,
|
|
||||||
} = useApp();
|
|
||||||
const { channels, roles } = useGuildResources(currentGuildId);
|
|
||||||
const [roleDraft, setRoleDraft] = useState('');
|
|
||||||
|
|
||||||
const cfg = settings.automodConfig || {};
|
|
||||||
const whitelistRoles: string[] = cfg.whitelistRoles || [];
|
|
||||||
const strikeCfg = cfg.strikeConfig || { enabled: false, decayHours: 24, thresholds: [] };
|
|
||||||
const badwordRules: { pattern: string; isRegex?: boolean; severity?: string }[] =
|
|
||||||
cfg.badwordRules && cfg.badwordRules.length
|
|
||||||
? cfg.badwordRules
|
|
||||||
: (cfg.customBadwords || []).map((w: string) => ({ pattern: w, isRegex: false, severity: 'low' }));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentGuildId) loadAutomodStrikes();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentGuildId]);
|
|
||||||
|
|
||||||
const patchConfig = (patch: Record<string, any>) =>
|
|
||||||
setSettings((s) => ({ ...s, automodConfig: { ...(s.automodConfig || {}), ...patch } }));
|
|
||||||
|
|
||||||
const patchFilter = (key: string, patch: Record<string, any>) => {
|
|
||||||
const filters = { ...(cfg.filters || {}) };
|
|
||||||
filters[key] = { ...(filters[key] || {}), ...patch };
|
|
||||||
patchConfig({ filters });
|
|
||||||
};
|
|
||||||
|
|
||||||
const addWhitelistRole = () => {
|
|
||||||
if (!roleDraft || whitelistRoles.includes(roleDraft)) return;
|
|
||||||
patchConfig({ whitelistRoles: [...whitelistRoles, roleDraft] });
|
|
||||||
setRoleDraft('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBadword = (idx: number, patch: Record<string, any>) => {
|
|
||||||
patchConfig({ badwordRules: badwordRules.map((r, i) => (i === idx ? { ...r, ...patch } : r)) });
|
|
||||||
};
|
|
||||||
const removeBadword = (idx: number) => {
|
|
||||||
patchConfig({ badwordRules: badwordRules.filter((_, i) => i !== idx) });
|
|
||||||
};
|
|
||||||
const addBadword = () => {
|
|
||||||
patchConfig({ badwordRules: [...badwordRules, { pattern: '', isRegex: false, severity: 'low' }] });
|
|
||||||
};
|
|
||||||
|
|
||||||
const patchStrike = (patch: Record<string, any>) => patchConfig({ strikeConfig: { ...strikeCfg, ...patch } });
|
|
||||||
const thresholds: { count: number; action: string; timeoutMinutes?: number }[] = strikeCfg.thresholds || [];
|
|
||||||
const updateThreshold = (idx: number, patch: Record<string, any>) => {
|
|
||||||
patchStrike({ thresholds: thresholds.map((t, i) => (i === idx ? { ...t, ...patch } : t)) });
|
|
||||||
};
|
|
||||||
const removeThreshold = (idx: number) => {
|
|
||||||
patchStrike({ thresholds: thresholds.filter((_, i) => i !== idx) });
|
|
||||||
};
|
|
||||||
const addThreshold = () => {
|
|
||||||
patchStrike({ thresholds: [...thresholds, { count: 3, action: 'timeout', timeoutMinutes: 10 }] });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Automod" subtitle="Filter, Aktionen, Eskalation und Sicherheit">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Automod</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<ModuleActiveToggle
|
|
||||||
icon={<Shield size={16} />}
|
|
||||||
title="Automod aktiv"
|
|
||||||
description="Schaltet alle Filter unten gesammelt ein oder aus."
|
|
||||||
isSelected={settings.automodEnabled !== false}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, automodEnabled: v }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{FILTERS.map((f) => {
|
|
||||||
const filterCfg = cfg.filters?.[f.key] || {};
|
|
||||||
const action = filterCfg.action || f.defaultAction;
|
|
||||||
const exemptRoleIds: string[] = filterCfg.exemptRoleIds || [];
|
|
||||||
const exemptChannelIds: string[] = filterCfg.exemptChannelIds || [];
|
|
||||||
return (
|
|
||||||
<div key={f.key} className="bg-surface-secondary rounded-xl p-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-default-soft text-muted">
|
|
||||||
{f.icon}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="text-sm font-medium">{f.title}</div>
|
|
||||||
<div className="text-xs text-muted">{f.description}</div>
|
|
||||||
</div>
|
|
||||||
<AppSwitch
|
|
||||||
aria-label={f.title}
|
|
||||||
isSelected={cfg[f.key] ?? true}
|
|
||||||
onChange={(v) => patchConfig({ [f.key]: v })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(cfg[f.key] ?? true) && (
|
|
||||||
<div className="mt-3 flex flex-col gap-3 border-t border-border pt-3">
|
|
||||||
<div className="flex items-end gap-2">
|
|
||||||
<TextField className="w-48">
|
|
||||||
<Label className="text-xs">Aktion bei Verstoß</Label>
|
|
||||||
<select
|
|
||||||
className="w-full rounded-xl px-3 py-2 text-sm"
|
|
||||||
value={action}
|
|
||||||
onChange={(e) => patchFilter(f.key, { action: e.target.value })}
|
|
||||||
>
|
|
||||||
{Object.entries(ACTION_LABELS).map(([value, label]) => (
|
|
||||||
<option key={value} value={value}>{label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</TextField>
|
|
||||||
{action === 'timeout' && (
|
|
||||||
<TextField className="w-36">
|
|
||||||
<Label className="text-xs">Timeout (Min.)</Label>
|
|
||||||
<Input
|
|
||||||
type="number" min="1"
|
|
||||||
value={String(filterCfg.timeoutMinutes ?? cfg.spamTimeoutMinutes ?? 10)}
|
|
||||||
onChange={(e) => patchFilter(f.key, { timeoutMinutes: Number(e.target.value || 10) })}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{f.key === 'linkFilter' && (
|
|
||||||
<TextField>
|
|
||||||
<Label className="text-xs">Whitelist Links (Komma-getrennt)</Label>
|
|
||||||
<TextArea
|
|
||||||
value={(cfg.linkWhitelist || []).join(', ')}
|
|
||||||
onChange={(e) => patchConfig({ linkWhitelist: toList(e.target.value) })}
|
|
||||||
placeholder="trusted-domain.com, another-safe.site"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{f.key === 'mentionSpamFilter' && (
|
|
||||||
<TextField className="w-56">
|
|
||||||
<Label className="text-xs">Max. Erwähnungen pro Nachricht</Label>
|
|
||||||
<Input
|
|
||||||
type="number" min="2"
|
|
||||||
value={String(cfg.maxMentions ?? 5)}
|
|
||||||
onChange={(e) => patchConfig({ maxMentions: Number(e.target.value || 5) })}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{f.key === 'spamFilter' && (
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<TextField>
|
|
||||||
<Label className="text-xs">Nachrichten</Label>
|
|
||||||
<Input
|
|
||||||
type="number" min="2"
|
|
||||||
value={String(cfg.spamThreshold ?? 5)}
|
|
||||||
onChange={(e) => patchConfig({ spamThreshold: Number(e.target.value || 5) })}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label className="text-xs">Zeitfenster (Sek.)</Label>
|
|
||||||
<Input
|
|
||||||
type="number" min="1"
|
|
||||||
value={String(Math.round((cfg.windowMs ?? 7000) / 1000))}
|
|
||||||
onChange={(e) => patchConfig({ windowMs: Number(e.target.value || 7) * 1000 })}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Ausgenommene Rollen</Label>
|
|
||||||
<RoleChipList roles={roles} values={exemptRoleIds} onChange={(v) => patchFilter(f.key, { exemptRoleIds: v })} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">Ausgenommene Kanäle</Label>
|
|
||||||
<ChannelChipList channels={channels} values={exemptChannelIds} onChange={(v) => patchFilter(f.key, { exemptChannelIds: v })} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Log Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={cfg.logChannelId}
|
|
||||||
onChange={(id) => patchConfig({ logChannelId: id })}
|
|
||||||
placeholder="Channel für Automod-Logs wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={() => saveSettingsPayload({ automodEnabled: settings.automodEnabled !== false, automodConfig: cfg }, 'Automod gespeichert')}>
|
|
||||||
<Save size={16} /> Speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Bad-Word-Regeln</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-5">
|
|
||||||
<p className="text-xs text-muted">Wörter, Phrasen oder reguläre Ausdrücke mit eigenem Schweregrad. Ergänzt die eingebaute Standard-Liste.</p>
|
|
||||||
{badwordRules.map((rule, idx) => (
|
|
||||||
<div key={idx} className="flex items-center gap-2 rounded-xl bg-surface-secondary p-2">
|
|
||||||
<Input
|
|
||||||
className="flex-1"
|
|
||||||
placeholder={rule.isRegex ? 'Regex, z.B. \\bwort\\d+\\b' : 'Wort oder Phrase'}
|
|
||||||
value={rule.pattern}
|
|
||||||
onChange={(e) => updateBadword(idx, { pattern: e.target.value })}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
className="rounded-xl px-2 py-2 text-xs"
|
|
||||||
value={rule.severity || 'low'}
|
|
||||||
onChange={(e) => updateBadword(idx, { severity: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value="low">Niedrig</option>
|
|
||||||
<option value="medium">Mittel</option>
|
|
||||||
<option value="high">Hoch</option>
|
|
||||||
</select>
|
|
||||||
<label className="flex items-center gap-1 text-xs text-muted shrink-0">
|
|
||||||
<AppSwitch aria-label="Regex" isSelected={!!rule.isRegex} onChange={(v) => updateBadword(idx, { isRegex: v })} />
|
|
||||||
Regex
|
|
||||||
</label>
|
|
||||||
<Button isIconOnly size="sm" variant="danger-soft" onPress={() => removeBadword(idx)}>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button size="sm" variant="tertiary" onPress={addBadword}>
|
|
||||||
<Plus size={14} /> Regel hinzufügen
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="flex items-center gap-2 text-base font-semibold"><Siren size={16} /> Eskalation / Strikes</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-5">
|
|
||||||
<div className="flex items-center gap-3 rounded-xl bg-surface-secondary p-3">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="text-sm font-medium">Strike-System aktiv</div>
|
|
||||||
<div className="text-xs text-muted">Verstöße werden gezählt; ab einer Schwelle greift automatisch eine härtere Strafe.</div>
|
|
||||||
</div>
|
|
||||||
<AppSwitch aria-label="Strike-System aktiv" isSelected={!!strikeCfg.enabled} onChange={(v) => patchStrike({ enabled: v })} />
|
|
||||||
</div>
|
|
||||||
{strikeCfg.enabled && (
|
|
||||||
<>
|
|
||||||
<TextField className="w-48">
|
|
||||||
<Label className="text-xs">Verstöße verfallen nach (Std.)</Label>
|
|
||||||
<Input
|
|
||||||
type="number" min="0" placeholder="0 = nie"
|
|
||||||
value={String(strikeCfg.decayHours ?? 24)}
|
|
||||||
onChange={(e) => patchStrike({ decayHours: Number(e.target.value || 0) })}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{thresholds.map((t, idx) => (
|
|
||||||
<div key={idx} className="flex items-center gap-2 rounded-xl bg-surface-secondary p-2">
|
|
||||||
<TextField className="w-24">
|
|
||||||
<Label className="text-xs">Ab Anzahl</Label>
|
|
||||||
<Input type="number" min="1" value={String(t.count)} onChange={(e) => updateThreshold(idx, { count: Number(e.target.value || 1) })} />
|
|
||||||
</TextField>
|
|
||||||
<TextField className="w-32">
|
|
||||||
<Label className="text-xs">Aktion</Label>
|
|
||||||
<select
|
|
||||||
className="w-full rounded-xl px-3 py-2 text-sm"
|
|
||||||
value={t.action}
|
|
||||||
onChange={(e) => updateThreshold(idx, { action: e.target.value })}
|
|
||||||
>
|
|
||||||
{Object.entries(ESCALATION_ACTION_LABELS).map(([value, label]) => (
|
|
||||||
<option key={value} value={value}>{label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</TextField>
|
|
||||||
{t.action === 'timeout' && (
|
|
||||||
<TextField className="w-28">
|
|
||||||
<Label className="text-xs">Minuten</Label>
|
|
||||||
<Input type="number" min="1" value={String(t.timeoutMinutes ?? 10)} onChange={(e) => updateThreshold(idx, { timeoutMinutes: Number(e.target.value || 10) })} />
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
<Button isIconOnly size="sm" variant="danger-soft" className="mt-4" onPress={() => removeThreshold(idx)}>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<Button size="sm" variant="tertiary" onPress={addThreshold}>
|
|
||||||
<Plus size={14} /> Schwelle hinzufügen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Aktuelle Verstöße</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-2 p-5">
|
|
||||||
{automodStrikes.length ? automodStrikes.map((s) => (
|
|
||||||
<div key={s.userId} className="flex items-center justify-between gap-2 rounded-xl bg-surface-secondary p-3">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="text-sm font-medium truncate">{s.userId}</div>
|
|
||||||
<div className="text-xs text-muted truncate">{s.reasons.join(' · ') || 'Keine Details'}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
|
||||||
<span className="rounded-full bg-danger-soft px-2 py-1 text-xs font-semibold text-danger">{s.count}</span>
|
|
||||||
<Button size="sm" variant="tertiary" onPress={() => resetAutomodStrike(s.userId)}>Zurücksetzen</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<p className="text-xs text-muted">Keine aktiven Verstöße.</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Globale Rollen-Whitelist</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-5">
|
|
||||||
<p className="text-xs text-muted">Mitglieder mit diesen Rollen werden von Automod komplett ignoriert (alle Filter).</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{whitelistRoles.length ? whitelistRoles.map((roleId) => {
|
|
||||||
const role = roles.find((r) => r.id === roleId);
|
|
||||||
return (
|
|
||||||
<div key={roleId} className="bg-default-soft flex items-center gap-1.5 rounded-full py-1 pl-3 pr-1.5 text-xs font-medium">
|
|
||||||
{role?.color && <span className="size-2 rounded-full" style={{ backgroundColor: role.color }} />}
|
|
||||||
{role?.name || roleId}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex size-4 items-center justify-center rounded-full text-muted hover:bg-danger-soft hover:text-danger"
|
|
||||||
onClick={() => patchConfig({ whitelistRoles: whitelistRoles.filter((id) => id !== roleId) })}
|
|
||||||
>
|
|
||||||
<X size={11} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}) : <p className="text-xs text-muted">Keine Rollen ausgenommen</p>}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<RoleSelect
|
|
||||||
options={roles.filter((r) => !whitelistRoles.includes(r.id))}
|
|
||||||
value={roleDraft}
|
|
||||||
onChange={setRoleDraft}
|
|
||||||
placeholder="Rolle wählen"
|
|
||||||
/>
|
|
||||||
<Button size="sm" variant="tertiary" className="shrink-0" onPress={addWhitelistRole} isDisabled={!roleDraft}>
|
|
||||||
<UserPlus size={14} /> Hinzufügen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardContent className="flex items-start gap-3 p-4">
|
|
||||||
<Info size={16} className="mt-0.5 shrink-0 text-accent" />
|
|
||||||
<p className="text-sm text-muted">Änderungen werden nach dem Speichern sofort aktiv, ohne dass der Bot neu gestartet werden muss.</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, InputGroup, TextArea, Button, Chip, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { CalendarDays, Save, Cake, Clock } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
export function Birthday() {
|
|
||||||
const { birthday, setBirthday, saveBirthday, currentGuildId } = useApp();
|
|
||||||
const { channels } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Birthday" subtitle="Geburtstags-Feature und gespeicherte Einträge">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Konfiguration</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<ModuleActiveToggle
|
|
||||||
icon={<Cake size={16} />}
|
|
||||||
title="Birthday aktiv"
|
|
||||||
description="Speichert Geburtstage und sendet automatisch Glückwünsche."
|
|
||||||
isSelected={birthday.config?.enabled !== false}
|
|
||||||
onChange={(v) => setBirthday((s) => ({ ...s, config: { ...s.config, enabled: v } }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={birthday.config?.channelId}
|
|
||||||
onChange={(id) => setBirthday((s) => ({ ...s, config: { ...s.config, channelId: id } }))}
|
|
||||||
placeholder="Channel für Geburtstagsnachrichten wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Sendezeit (Stunde)</Label>
|
|
||||||
<InputGroup>
|
|
||||||
<InputGroup.Prefix><Clock size={16} className="text-muted" /></InputGroup.Prefix>
|
|
||||||
<InputGroup.Input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="23"
|
|
||||||
value={String(birthday.config?.sendHour ?? 9)}
|
|
||||||
onChange={(e) => setBirthday((s) => ({ ...s, config: { ...s.config, sendHour: Number(e.target.value || 0) } }))}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Template</Label>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Alles Gute zum Geburtstag, {user}!"
|
|
||||||
value={birthday.config?.messageTemplate || ''}
|
|
||||||
onChange={(e) => setBirthday((s) => ({ ...s, config: { ...s.config, messageTemplate: e.target.value } }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={saveBirthday}><Save size={16} /> Speichern</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Gespeicherte Geburtstage ({(birthday.birthdays || []).length})</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-2 p-5">
|
|
||||||
{(birthday.birthdays || []).length ? (birthday.birthdays || []).map((entry, i) => (
|
|
||||||
<div key={i} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Cake size={14} className="text-accent" />
|
|
||||||
<span className="font-medium">{entry.userId}</span>
|
|
||||||
</div>
|
|
||||||
<Chip size="sm" variant="soft" color="accent">
|
|
||||||
{String(entry.birthDate || '').replace(/^--/, '')}
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<CalendarDays size={20} />
|
|
||||||
Keine Einträge
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Button, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { Palette, Save } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { DiscordPreview } from '../components/shared/DiscordPreview';
|
|
||||||
|
|
||||||
const THEMES: { key: string; label: string; color: string }[] = [
|
|
||||||
{ key: 'orange', label: 'Orange', color: '#f97316' },
|
|
||||||
{ key: 'blue', label: 'Blau', color: '#3b82f6' },
|
|
||||||
{ key: 'green', label: 'Grün', color: '#22c55e' },
|
|
||||||
{ key: 'purple', label: 'Lila', color: '#a855f7' },
|
|
||||||
{ key: 'red', label: 'Rot', color: '#ef4444' }
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Branding() {
|
|
||||||
const { settings, setSettings, saveSettingsPayload } = useApp();
|
|
||||||
const branding = settings.brandingConfig || {};
|
|
||||||
|
|
||||||
const patch = (patchValue: Record<string, any>) =>
|
|
||||||
setSettings((s) => ({ ...s, brandingConfig: { ...(s.brandingConfig || {}), ...patchValue } }));
|
|
||||||
|
|
||||||
const previewColor = branding.embedColor || THEMES.find((t) => t.key === branding.theme)?.color || '#f97316';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Branding" subtitle="Passe Papo an das Erscheinungsbild deines Servers an.">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<CardTitle><Palette size={16} className="inline mr-1.5" /> Branding konfigurieren</CardTitle>
|
|
||||||
<CardDescription>Farbe, Logo, Footer und Bot-Name für dieses Dashboard und Bot-Embeds.</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<TextField>
|
|
||||||
<Label>Embed-Farbe (Hex)</Label>
|
|
||||||
<Input placeholder="#f97316" value={branding.embedColor || ''} onChange={(e) => patch({ embedColor: e.target.value })} />
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Logo-URL</Label>
|
|
||||||
<Input placeholder="https://..." value={branding.logoUrl || ''} onChange={(e) => patch({ logoUrl: e.target.value })} />
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Footer-Text</Label>
|
|
||||||
<Input value={branding.footerText || ''} onChange={(e) => patch({ footerText: e.target.value })} />
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Bot-Name im Dashboard</Label>
|
|
||||||
<Input placeholder="Papo" value={branding.botName || ''} onChange={(e) => patch({ botName: e.target.value })} />
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Theme</Label>
|
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
|
||||||
{THEMES.map((t) => (
|
|
||||||
<button
|
|
||||||
key={t.key}
|
|
||||||
type="button"
|
|
||||||
onClick={() => patch({ theme: t.key })}
|
|
||||||
className={`flex items-center gap-2 rounded-xl border px-3 py-2 text-sm ${branding.theme === t.key ? 'border-accent bg-accent-soft' : 'border-border'}`}
|
|
||||||
>
|
|
||||||
<span className="size-3 rounded-full" style={{ backgroundColor: t.color }} />
|
|
||||||
{t.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Button size="lg" variant="primary" onPress={() => saveSettingsPayload({ brandingConfig: branding }, 'Branding gespeichert')}>
|
|
||||||
<Save size={16} /> Speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<CardTitle>Live Vorschau</CardTitle>
|
|
||||||
<CardDescription>So sehen deine Embeds ungefähr aus.</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<DiscordPreview
|
|
||||||
botName={branding.botName || 'Papo'}
|
|
||||||
title="Beispiel-Embed"
|
|
||||||
description="So wirkt dein gewähltes Branding auf Nachrichten von Papo."
|
|
||||||
footer={branding.footerText}
|
|
||||||
accentColor={previewColor}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Chip, Button, ScrollShadow } from '@heroui/react';
|
|
||||||
import {
|
|
||||||
Bot, CalendarDays, Users, Ticket, Shield, MessageSquare,
|
|
||||||
ChevronRight, Activity, Clock, ArrowUpRight, RefreshCw, Send,
|
|
||||||
Settings, Sparkles, Hash, Gauge, Zap, Bell, Tag, Command
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { formatDate, guildIconUrl } from '../utils/formatters';
|
|
||||||
import { StatCard } from '../components/shared/StatCard';
|
|
||||||
import { AppAvatar } from '../components/shared/AppAvatar';
|
|
||||||
import { BarComparisonChart } from '../components/shared/BarComparisonChart';
|
|
||||||
|
|
||||||
export function Dashboard() {
|
|
||||||
const { guildInfo, guilds, currentGuildId, overview, activity, logs, setSection } = useApp();
|
|
||||||
const selectedGuild = guilds.find((g) => g.id === currentGuildId);
|
|
||||||
const moduleFlags = guildInfo?.modules || {};
|
|
||||||
|
|
||||||
const quickActions = [
|
|
||||||
{ key: 'tickets', label: 'Ticket Panel senden', icon: <Send size={16} /> },
|
|
||||||
{ key: 'serverstats', label: 'Sync starten', icon: <RefreshCw size={16} /> },
|
|
||||||
{ key: 'modules', label: 'Module aktualisieren', icon: <Zap size={16} /> },
|
|
||||||
{ key: 'settings', label: 'Einstellungen oeffnen', icon: <Settings size={16} /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<Card className="bg-surface-secondary">
|
|
||||||
<CardContent className="flex flex-col gap-6 xl:flex-row xl:items-center xl:justify-between">
|
|
||||||
<div className="flex min-w-0 items-center gap-5">
|
|
||||||
<AppAvatar className="size-20 shrink-0" size="lg" src={guildIconUrl(selectedGuild)} name={guildInfo?.name || selectedGuild?.name} />
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h1 className="truncate text-3xl font-black tracking-tight">{guildInfo?.name || selectedGuild?.name}</h1>
|
|
||||||
<Chip color="success" size="sm" variant="soft">
|
|
||||||
<Bot size={12} /> Online
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-sm text-muted">ID: {guildInfo?.id || selectedGuild?.id}</div>
|
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
|
||||||
{Object.entries(moduleFlags).filter(([, v]) => v).map(([key]) => (
|
|
||||||
<Chip key={key} size="sm" variant="soft" color="accent">
|
|
||||||
{key.replace('Enabled', '').replace(/([A-Z])/g, ' $1').trim()}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 shrink-0">
|
|
||||||
<Chip size="sm" variant="soft" color="accent">
|
|
||||||
<Gauge size={12} /> Ping: {guildInfo?.ping || '-'}ms
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
|
||||||
<StatCard icon={<Users size={18} />} label="Mitglieder" value={guildInfo?.memberCount ?? 0} />
|
|
||||||
<StatCard icon={<Activity size={18} />} label="Online" value={guildInfo?.onlineCount ?? '-'} />
|
|
||||||
<StatCard icon={<Hash size={18} />} label="Channels" value={`${guildInfo?.textCount || 0}`} />
|
|
||||||
<StatCard icon={<Shield size={18} />} label="Rollen" value={guildInfo?.roleCount ?? '-'} />
|
|
||||||
<StatCard icon={<ArrowUpRight size={18} />} label="Boost Level" value={guildInfo?.boostLevel ?? '-'} />
|
|
||||||
<StatCard icon={<Ticket size={18} />} label="Offene Tickets" value={overview?.tickets?.open ?? 0} color="warning" />
|
|
||||||
<StatCard icon={<Command size={18} />} label="Commands (24h)" value={activity?.commands24h ?? 0} />
|
|
||||||
<StatCard icon={<MessageSquare size={18} />} label="Nachrichten (24h)" value={activity?.messages24h ?? 0} />
|
|
||||||
<StatCard icon={<Bot size={18} />} label="Automod (24h)" value={activity?.automod24h ?? 0} color="danger" />
|
|
||||||
<StatCard icon={<Clock size={18} />} label="Uptime" value={guildInfo?.uptime || '-'} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2 2xl:grid-cols-3">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-bold">Activity Bereich</h2>
|
|
||||||
<p className="mt-0.5 text-xs text-muted">Live Statistiken</p>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<BarComparisonChart
|
|
||||||
items={[
|
|
||||||
{ label: 'Nachrichten', value: activity?.messages24h ?? 0 },
|
|
||||||
{ label: 'Commands', value: activity?.commands24h ?? 0 },
|
|
||||||
{ label: 'Automod', value: activity?.automod24h ?? 0 },
|
|
||||||
{ label: 'Neue User', value: activity?.newUsers24h ?? 0 },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-bold">Guild Logs</h2>
|
|
||||||
<p className="mt-0.5 text-xs text-muted">Letzte Ereignisse</p>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" variant="ghost" onPress={() => setSection('settings')}>
|
|
||||||
Alle <ChevronRight size={14} />
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ScrollShadow className="max-h-[320px] space-y-2 pr-1" hideScrollBar>
|
|
||||||
{logs.length ? logs.slice(0, 15).map((log, i) => (
|
|
||||||
<Card key={`${log.timestamp}-${i}`} className="bg-surface-tertiary">
|
|
||||||
<CardContent className="flex items-start gap-3 p-3">
|
|
||||||
<div className={`mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-lg ${
|
|
||||||
log.level === 'error' ? 'bg-danger-soft text-danger' :
|
|
||||||
log.level === 'warn' ? 'bg-warning-soft text-warning' :
|
|
||||||
'bg-accent-soft text-accent-soft-foreground'
|
|
||||||
}`}>
|
|
||||||
{log.level === 'error' ? <Shield size={12} /> :
|
|
||||||
log.level === 'warn' ? <Bell size={12} /> :
|
|
||||||
<Activity size={12} />}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Chip
|
|
||||||
color={log.level === 'error' ? 'danger' : log.level === 'warn' ? 'warning' : 'default'}
|
|
||||||
size="sm"
|
|
||||||
variant="soft"
|
|
||||||
>
|
|
||||||
{(log.level || 'info').toUpperCase()}
|
|
||||||
</Chip>
|
|
||||||
<span className="text-xs text-muted">{formatDate(log.timestamp)}</span>
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-sm text-foreground/80">
|
|
||||||
{log.category ? <span className="text-muted">[{log.category}] </span> : ''}{log.message || '-'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-6 text-center text-sm text-muted">
|
|
||||||
<Activity size={20} />
|
|
||||||
Keine Logs
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ScrollShadow>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<h2 className="text-lg font-bold">Quick Actions</h2>
|
|
||||||
<p className="mt-0.5 text-xs text-muted">Schnellzugriff</p>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-2">
|
|
||||||
{quickActions.map((action) => (
|
|
||||||
<Button
|
|
||||||
key={action.key}
|
|
||||||
variant="tertiary"
|
|
||||||
className="justify-start font-medium"
|
|
||||||
onPress={() => setSection(action.key as any)}
|
|
||||||
>
|
|
||||||
{action.icon} {action.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
|
||||||
{(['tickets', 'supportlogin', 'automod', 'welcome', 'birthday', 'reactionroles'] as const).map((key) => {
|
|
||||||
const item = navItemMap[key];
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={key}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => setSection(key)}
|
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setSection(key); }}
|
|
||||||
className="cursor-pointer border border-transparent transition-colors hover:border-accent"
|
|
||||||
>
|
|
||||||
<CardContent className="flex flex-col items-start gap-2">
|
|
||||||
<Chip size="sm" variant="soft" color="accent">
|
|
||||||
{item.icon} {item.label}
|
|
||||||
</Chip>
|
|
||||||
<div className="font-semibold text-sm">{item.label}</div>
|
|
||||||
<div className="text-xs text-muted">Modul verwalten</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const navItemMap: Record<string, { label: string; icon: React.ReactNode }> = {
|
|
||||||
tickets: { label: 'Ticketsystem', icon: <Ticket size={18} /> },
|
|
||||||
supportlogin: { label: 'Support Login', icon: <Send size={18} /> },
|
|
||||||
automod: { label: 'Automod', icon: <Shield size={18} /> },
|
|
||||||
welcome: { label: 'Willkommen', icon: <Sparkles size={18} /> },
|
|
||||||
birthday: { label: 'Birthday', icon: <CalendarDays size={18} /> },
|
|
||||||
reactionroles: { label: 'Reaction Roles', icon: <Tag size={18} /> },
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Input, Button, Chip, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { AudioLines, Save, Mic, Users } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
export function DynamicVoice() {
|
|
||||||
const { settings, setSettings, saveSettingsPayload, currentGuildId } = useApp();
|
|
||||||
const { channels, categories } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Dynamic Voice" subtitle="Voice-Lobby, Template und Limits">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Konfiguration</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<ModuleActiveToggle
|
|
||||||
icon={<AudioLines size={16} />}
|
|
||||||
title="Dynamic Voice aktiv"
|
|
||||||
description="Erstellt private Voice-Channels aus der konfigurierten Lobby."
|
|
||||||
isSelected={settings.dynamicVoiceEnabled !== false}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, dynamicVoiceEnabled: v }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Lobby Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels.filter((c) => c.type === 'voice')}
|
|
||||||
value={settings.dynamicVoiceConfig?.lobbyChannelId}
|
|
||||||
onChange={(id) => setSettings((s) => ({ ...s, dynamicVoiceConfig: { ...(s.dynamicVoiceConfig || {}), lobbyChannelId: id } }))}
|
|
||||||
placeholder="Voice-Channel der Lobby wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Kategorie</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={categories.map((c) => ({ ...c, type: 'category' }))}
|
|
||||||
value={settings.dynamicVoiceConfig?.categoryId}
|
|
||||||
onChange={(id) => setSettings((s) => ({ ...s, dynamicVoiceConfig: { ...(s.dynamicVoiceConfig || {}), categoryId: id } }))}
|
|
||||||
placeholder="Kategorie für neue Channels wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Template</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Channel-Name Template"
|
|
||||||
value={settings.dynamicVoiceConfig?.template || ''}
|
|
||||||
onChange={(e) => setSettings((s) => ({ ...s, dynamicVoiceConfig: { ...(s.dynamicVoiceConfig || {}), template: e.target.value } }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={() => saveSettingsPayload({ dynamicVoiceConfig: settings.dynamicVoiceConfig || {} }, 'Dynamic Voice gespeichert')}>
|
|
||||||
<Save size={16} /> Speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Info</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-5">
|
|
||||||
<div className="bg-surface-tertiary flex items-center gap-3 rounded-xl px-4 py-3 text-sm">
|
|
||||||
<Mic size={16} className="text-accent" />
|
|
||||||
<span className="text-muted">Benutzer erstellen eigene Voice-Channels durch Beitreten der Lobby</span>
|
|
||||||
</div>
|
|
||||||
<div className="bg-surface-tertiary flex items-center gap-3 rounded-xl px-4 py-3 text-sm">
|
|
||||||
<Users size={16} className="text-success" />
|
|
||||||
<span className="text-muted">Channel-Owner können Limits und Berechtigungen verwalten</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { CalendarDays, Trash2, Plus, Clock } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { formatDate } from '../utils/formatters';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
export function Events() {
|
|
||||||
const { events, eventDraft, setEventDraft, saveEvent, deleteEvent, currentGuildId } = useApp();
|
|
||||||
const { channels } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Events" subtitle="Bestehende Events und schneller Neu-Anlage-Flow">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Bestehende Events ({(events || []).length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{(events || []).length ? (events || []).map((event) => (
|
|
||||||
<Card key={event.id}>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<CalendarDays size={16} className="text-accent shrink-0" />
|
|
||||||
<span className="font-semibold text-sm truncate">{event.title}</span>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" variant="danger-soft" onPress={() => deleteEvent(event.id)}>
|
|
||||||
<Trash2 size={14} /> Löschen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted">{event.description || 'Keine Beschreibung'}</p>
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted">
|
|
||||||
<Clock size={12} />
|
|
||||||
{formatDate(event.startsAt)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<CalendarDays size={24} />
|
|
||||||
Keine Events
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Neues Event</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Titel</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Event Name"
|
|
||||||
value={eventDraft.title}
|
|
||||||
onChange={(e) => setEventDraft((s) => ({ ...s, title: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Beschreibung</Label>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Event Beschreibung"
|
|
||||||
value={eventDraft.description}
|
|
||||||
onChange={(e) => setEventDraft((s) => ({ ...s, description: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={eventDraft.channelId}
|
|
||||||
onChange={(id) => setEventDraft((s) => ({ ...s, channelId: id }))}
|
|
||||||
placeholder="Channel für Erinnerungen wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Start (ISO)</Label>
|
|
||||||
<Input
|
|
||||||
type="datetime-local"
|
|
||||||
placeholder="2024-12-24T18:00"
|
|
||||||
value={eventDraft.startsAt}
|
|
||||||
onChange={(e) => setEventDraft((s) => ({ ...s, startsAt: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={saveEvent}>
|
|
||||||
<Plus size={16} /> Event speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, Chip } from '@heroui/react';
|
|
||||||
import { TrendingUp, TrendingDown, Gem, Link2, Handshake, Users } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { StatCard } from '../components/shared/StatCard';
|
|
||||||
import { BarComparisonChart } from '../components/shared/BarComparisonChart';
|
|
||||||
import { formatDate } from '../utils/formatters';
|
|
||||||
|
|
||||||
export function Growth() {
|
|
||||||
const { currentGuildId, growthStats, loadGrowthStats, inviteBreakdown, recentJoins, loadInviteBreakdown } = useApp();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentGuildId) {
|
|
||||||
loadGrowthStats();
|
|
||||||
loadInviteBreakdown();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentGuildId]);
|
|
||||||
|
|
||||||
const stats = growthStats;
|
|
||||||
const chartItems = (stats?.dailyJoins || []).map((d) => ({
|
|
||||||
label: new Date(d.day).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }),
|
|
||||||
value: d.count
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Wachstum" subtitle="Joins, Leaves, Invites und Booster im Überblick.">
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<StatCard icon={<TrendingUp size={18} />} label="Joins (7 Tage)" value={stats?.joins7 ?? 0} color="success" />
|
|
||||||
<StatCard icon={<TrendingDown size={18} />} label="Leaves (7 Tage)" value={stats?.leaves7 ?? 0} color="danger" />
|
|
||||||
<StatCard icon={<Gem size={18} />} label="Booster" value={stats?.boosts ?? 0} color="accent" />
|
|
||||||
<StatCard icon={<Handshake size={18} />} label="Joins über Partner (30T)" value={stats?.partnerJoins30 ?? 0} color="warning" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Joins (letzte 7 Tage)</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-5">
|
|
||||||
{chartItems.length ? (
|
|
||||||
<BarComparisonChart items={chartItems} />
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<TrendingUp size={24} />
|
|
||||||
Noch keine Daten erfasst
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Bester Invite (30 Tage)</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-5">
|
|
||||||
{stats?.bestInvite ? (
|
|
||||||
<div className="bg-surface-tertiary flex items-center gap-3 rounded-xl px-4 py-3 text-sm">
|
|
||||||
<Link2 size={16} className="text-accent shrink-0" />
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-medium">discord.gg/{stats.bestInvite.code}</div>
|
|
||||||
<div className="text-xs text-muted">{stats.bestInvite.uses} Beitritte</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<Link2 size={20} />
|
|
||||||
Noch keine Invite-Nutzung erfasst
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-muted">
|
|
||||||
Joins (30 Tage): {stats?.joins30 ?? 0} · Leaves (30 Tage): {stats?.leaves30 ?? 0}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Invite-Übersicht (30 Tage)</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-2 p-5">
|
|
||||||
{inviteBreakdown.length ? inviteBreakdown.map((inv) => (
|
|
||||||
<div key={inv.code} className="bg-surface-tertiary flex items-center justify-between gap-2 rounded-xl px-4 py-3 text-sm">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-medium truncate">discord.gg/{inv.code}</div>
|
|
||||||
<div className="text-xs text-muted truncate">
|
|
||||||
{inv.inviterId ? `von <@${inv.inviterId}>` : 'Unbekannter Ersteller'} · {inv.uses} Nutzungen gesamt
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
|
||||||
<Chip size="sm" variant="soft" color="accent">{inv.joins30} Joins</Chip>
|
|
||||||
{inv.suspiciousJoins30 > 0 && (
|
|
||||||
<Chip size="sm" variant="soft" color="danger">{inv.suspiciousJoins30} auffällig</Chip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<Link2 size={20} />
|
|
||||||
Keine Invite-Nutzung in den letzten 30 Tagen
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Letzte Beitritte</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex max-h-96 flex-col gap-2 overflow-y-auto p-5">
|
|
||||||
{recentJoins.length ? recentJoins.map((j) => (
|
|
||||||
<div key={j.id} className="bg-surface-tertiary flex items-center gap-3 rounded-xl px-4 py-3 text-sm">
|
|
||||||
<Users size={14} className="text-muted shrink-0" />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="truncate">
|
|
||||||
User-ID {j.userId}{j.inviterId ? ` · eingeladen von ${j.inviterId}` : ''}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-muted">
|
|
||||||
{j.inviteCode ? `discord.gg/${j.inviteCode}` : 'Unbekannter Invite'} · {formatDate(j.createdAt)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{j.suspicious && <Chip size="sm" variant="soft" color="danger">Verdächtig</Chip>}
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<Users size={20} />
|
|
||||||
Noch keine Beitritte erfasst
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { Card, CardContent } from '@heroui/react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { guildIconUrl } from '../utils/formatters';
|
|
||||||
import { AppAvatar } from '../components/shared/AppAvatar';
|
|
||||||
|
|
||||||
export function GuildSelect() {
|
|
||||||
const { guilds, setCurrentGuildId } = useApp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto flex min-h-screen max-w-6xl flex-col items-center justify-center px-6 py-12">
|
|
||||||
<div className="mb-8 text-center">
|
|
||||||
<div className="bg-accent text-accent-foreground mx-auto mb-4 flex size-16 items-center justify-center rounded-2xl text-2xl font-black">
|
|
||||||
P
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Wähle einen Server</h1>
|
|
||||||
<p className="mt-2 text-muted">Wähle einen Discord-Server aus, um das Dashboard zu öffnen.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid w-full gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
||||||
{guilds.map((guild) => (
|
|
||||||
<Card
|
|
||||||
key={guild.id}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className="cursor-pointer border border-transparent transition-colors hover:border-accent"
|
|
||||||
onClick={() => setCurrentGuildId(guild.id)}
|
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setCurrentGuildId(guild.id); }}
|
|
||||||
>
|
|
||||||
<CardContent className="flex flex-row items-center gap-4 p-5">
|
|
||||||
<AppAvatar src={guildIconUrl(guild)} name={guild.name} size="lg" />
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="truncate text-lg font-semibold">{guild.name}</div>
|
|
||||||
<div className="mt-0.5 text-sm text-muted">ID: {guild.id}</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { Card, CardContent } from '@heroui/react';
|
|
||||||
import { Puzzle, CheckCircle, XCircle } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { AppSwitch } from '../components/shared/AppSwitch';
|
|
||||||
|
|
||||||
export function ModulesPage() {
|
|
||||||
const { modules, toggleModule } = useApp();
|
|
||||||
|
|
||||||
const activeModules = modules.filter((m) => m.enabled);
|
|
||||||
const inactiveModules = modules.filter((m) => !m.enabled);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Module" subtitle="Module direkt umschalten">
|
|
||||||
<div className="space-y-5">
|
|
||||||
{activeModules.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 flex items-center gap-2 text-base font-semibold">
|
|
||||||
<CheckCircle size={16} className="text-success" />
|
|
||||||
Aktive Module ({activeModules.length})
|
|
||||||
</h3>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
||||||
{activeModules.map((module) => (
|
|
||||||
<Card key={module.key} className="bg-surface-secondary">
|
|
||||||
<CardContent className="flex flex-row items-center justify-between gap-4 p-4">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-semibold">{module.name}</div>
|
|
||||||
{module.description && (
|
|
||||||
<div className="text-sm text-muted truncate">{module.description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<AppSwitch aria-label={module.name} isSelected={module.enabled} onChange={(v) => toggleModule(module.key, v)} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{inactiveModules.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 flex items-center gap-2 text-base font-semibold">
|
|
||||||
<XCircle size={16} className="text-muted" />
|
|
||||||
Deaktivierte Module ({inactiveModules.length})
|
|
||||||
</h3>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
||||||
{inactiveModules.map((module) => (
|
|
||||||
<Card key={module.key}>
|
|
||||||
<CardContent className="flex flex-row items-center justify-between gap-4 p-4">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-semibold">{module.name}</div>
|
|
||||||
{module.description && (
|
|
||||||
<div className="text-sm text-muted truncate">{module.description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<AppSwitch aria-label={module.name} isSelected={module.enabled} onChange={(v) => toggleModule(module.key, v)} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modules.length === 0 && (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<Puzzle size={24} />
|
|
||||||
Keine Module verfügbar
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Chip, Button } from '@heroui/react';
|
|
||||||
import { Music, Play, List, Repeat, ExternalLink } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { StatCard } from '../components/shared/StatCard';
|
|
||||||
|
|
||||||
export function MusicPage() {
|
|
||||||
const { musicStatus } = useApp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Musik-Status" subtitle="Aktuelle Wiedergabe und Queues pro Guild.">
|
|
||||||
<div className="space-y-5">
|
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
|
||||||
<StatCard icon={<Music size={18} />} label="Aktive Guilds" value={musicStatus.activeGuilds} color="accent" />
|
|
||||||
<StatCard icon={<Play size={18} />} label="Aktive Sessions" value={musicStatus.sessions.length} color="success" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="text-base font-semibold">Sessions ({musicStatus.sessions.length})</h3>
|
|
||||||
{musicStatus.sessions.length ? musicStatus.sessions.map((session) => (
|
|
||||||
<Card key={session.guildId}>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Music size={16} className="text-accent" />
|
|
||||||
<span className="font-semibold text-sm">{session.guildId}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Chip size="sm" variant="soft">
|
|
||||||
<Repeat size={12} /> {session.loop}
|
|
||||||
</Chip>
|
|
||||||
<Chip size="sm" variant="soft">
|
|
||||||
<List size={12} /> {session.queueLength} in Queue
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{session.nowPlaying ? (
|
|
||||||
<div className="bg-surface-tertiary rounded-xl px-4 py-3 text-sm">
|
|
||||||
<span className="text-muted">Jetzt läuft: </span>
|
|
||||||
<a href={session.nowPlaying.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-accent hover:underline">
|
|
||||||
{session.nowPlaying.title}
|
|
||||||
<ExternalLink size={12} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-surface-tertiary rounded-xl px-4 py-3 text-sm text-muted">
|
|
||||||
Keine aktive Wiedergabe
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<Music size={24} />
|
|
||||||
Keine aktiven Musik-Sessions
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Chip, Button, Input, TextArea, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { LayoutPanelTop, Trash2, Send, ScrollText, LifeBuoy, FileSignature, Handshake, Tags, CalendarDays, CircleHelp } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { EmptyState } from '../components/shared/EmptyState';
|
|
||||||
import type { InfoPanelType } from '../types';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
const TYPE_META: Record<InfoPanelType, { label: string; icon: React.ReactNode }> = {
|
|
||||||
rules: { label: 'Regeln', icon: <ScrollText size={14} /> },
|
|
||||||
support: { label: 'Support', icon: <LifeBuoy size={14} /> },
|
|
||||||
bewerbung: { label: 'Bewerbungen', icon: <FileSignature size={14} /> },
|
|
||||||
partner: { label: 'Partner', icon: <Handshake size={14} /> },
|
|
||||||
rollen: { label: 'Rollen', icon: <Tags size={14} /> },
|
|
||||||
events: { label: 'Events', icon: <CalendarDays size={14} /> },
|
|
||||||
faq: { label: 'FAQ', icon: <CircleHelp size={14} /> },
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Panels() {
|
|
||||||
const { currentGuildId, infoPanels, loadInfoPanels, panelDraft, setPanelDraft, createInfoPanel, deleteInfoPanel } = useApp();
|
|
||||||
const { channels } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentGuildId) loadInfoPanels();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentGuildId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Info-Panels" subtitle="Feste Panels für Regeln, Support, Bewerbungen, Partner, Rollen, Events und FAQ mit passenden Buttons.">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Bestehende Panels ({infoPanels.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{infoPanels.length ? infoPanels.map((p) => (
|
|
||||||
<Card key={p.id}>
|
|
||||||
<CardContent className="flex items-center justify-between gap-3 p-4">
|
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
|
||||||
<div className="bg-accent-soft text-accent-soft-foreground flex size-9 shrink-0 items-center justify-center rounded-lg">
|
|
||||||
{TYPE_META[p.type]?.icon || <LayoutPanelTop size={14} />}
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="truncate text-sm font-semibold">{p.title}</div>
|
|
||||||
<div className="text-xs text-muted">#{channels.find((c) => c.id === p.channelId)?.name || p.channelId}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
|
||||||
<Chip size="sm" variant="soft">{TYPE_META[p.type]?.label || p.type}</Chip>
|
|
||||||
<Button size="sm" variant="danger-soft" onPress={() => deleteInfoPanel(p.id)}>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : <EmptyState message="Noch keine Panels erstellt" icon={<LayoutPanelTop size={24} />} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Neues Panel erstellen</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Typ</Label>
|
|
||||||
<select
|
|
||||||
aria-label="Panel-Typ"
|
|
||||||
className="w-full rounded-xl text-sm"
|
|
||||||
value={panelDraft.type}
|
|
||||||
onChange={(e) => setPanelDraft((s: any) => ({ ...s, type: e.target.value as InfoPanelType }))}
|
|
||||||
>
|
|
||||||
{Object.entries(TYPE_META).map(([key, meta]) => (
|
|
||||||
<option key={key} value={key}>{meta.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Ziel-Kanal</Label>
|
|
||||||
<ChannelSelect options={channels} value={panelDraft.channelId} onChange={(id) => setPanelDraft((s: any) => ({ ...s, channelId: id }))} />
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Titel</Label>
|
|
||||||
<Input value={panelDraft.title} onChange={(e) => setPanelDraft((s: any) => ({ ...s, title: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
{panelDraft.type !== 'faq' && (
|
|
||||||
<TextField>
|
|
||||||
<Label>Beschreibung</Label>
|
|
||||||
<TextArea rows={4} value={panelDraft.description} onChange={(e) => setPanelDraft((s: any) => ({ ...s, description: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{panelDraft.type === 'faq' && (
|
|
||||||
<TextField>
|
|
||||||
<Label>Fragen (eine „Frage: Antwort“ pro Zeile)</Label>
|
|
||||||
<TextArea
|
|
||||||
rows={5}
|
|
||||||
placeholder="Wie erstelle ich ein Ticket?: Klicke auf den Support-Button."
|
|
||||||
value={panelDraft.items}
|
|
||||||
onChange={(e) => setPanelDraft((s: any) => ({ ...s, items: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<p className="text-xs text-muted">
|
|
||||||
{panelDraft.type === 'support' && 'Erhält automatisch einen Ticket-Button.'}
|
|
||||||
{panelDraft.type === 'bewerbung' && 'Erhält automatisch einen Bewerben-Button, falls ein aktives Formular existiert.'}
|
|
||||||
{panelDraft.type === 'partner' && 'Erhält automatisch einen Partner-werden-Button.'}
|
|
||||||
{panelDraft.type === 'faq' && 'Erhält ein Dropdown mit den hinterlegten Fragen.'}
|
|
||||||
{['rules', 'rollen', 'events'].includes(panelDraft.type) && 'Reines Info-Embed ohne Button.'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={createInfoPanel} isDisabled={!panelDraft.channelId || !panelDraft.title}>
|
|
||||||
<Send size={16} /> Panel posten
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, Chip, Button } from '@heroui/react';
|
|
||||||
import { ShieldAlert, ShieldX, Bot, Globe, MessageSquare, Users, Copy, Ban, RefreshCw, TriangleAlert } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { StatCard } from '../components/shared/StatCard';
|
|
||||||
import { EmptyState } from '../components/shared/EmptyState';
|
|
||||||
|
|
||||||
function EntryList({ entries, emptyMessage, icon }: { entries: { id: string; name: string }[]; emptyMessage: string; icon: React.ReactNode }) {
|
|
||||||
if (!entries.length) return <p className="py-3 text-center text-xs text-muted">{emptyMessage}</p>;
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
{entries.slice(0, 10).map((e) => (
|
|
||||||
<div key={e.id} className="bg-surface-tertiary flex items-center gap-2 rounded-lg px-3 py-2 text-sm">
|
|
||||||
{icon}
|
|
||||||
<span className="truncate">{e.name}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{entries.length > 10 && <p className="pl-1 text-xs text-muted">+{entries.length - 10} weitere</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Permissions() {
|
|
||||||
const { currentGuildId, permissionScan, permissionScanLoading, loadPermissionScan } = useApp();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentGuildId) loadPermissionScan();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentGuildId]);
|
|
||||||
|
|
||||||
const r = permissionScan;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard
|
|
||||||
title="Rechte-Scanner"
|
|
||||||
subtitle="Sicherheitsanalyse der Server-Rollen, Rechte und Kanäle."
|
|
||||||
action={
|
|
||||||
<Button variant="primary" onPress={() => loadPermissionScan()} isDisabled={permissionScanLoading}>
|
|
||||||
<RefreshCw size={16} className={permissionScanLoading ? 'animate-spin' : ''} /> Scan starten
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!r && !permissionScanLoading && (
|
|
||||||
<EmptyState message="Noch kein Scan durchgeführt. Klicke auf „Scan starten“." icon={<ShieldAlert size={24} />} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{r && (
|
|
||||||
<div className="flex flex-col gap-5">
|
|
||||||
{r.tooManyAdmins && (
|
|
||||||
<Card className="border-danger bg-danger-soft">
|
|
||||||
<CardContent className="flex items-center gap-3 p-4 text-sm">
|
|
||||||
<TriangleAlert size={18} className="shrink-0 text-danger" />
|
|
||||||
<span>Auffällig viele Administrator-Rechte vergeben ({r.adminRoles.length} Rollen, {r.adminMemberCount} Mitglieder mit Adminrechten). Prüfe, ob das notwendig ist.</span>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
<StatCard icon={<ShieldAlert size={18} />} label="Admin-Rollen" value={r.adminRoles.length} color={r.tooManyAdmins ? 'danger' : 'default'} />
|
|
||||||
<StatCard icon={<Ban size={18} />} label="Ban-Rollen" value={r.banRoles.length} />
|
|
||||||
<StatCard icon={<Bot size={18} />} label="Bots mit gefährlichen Rechten" value={r.dangerousBots.length} color={r.dangerousBots.length ? 'warning' : 'default'} />
|
|
||||||
<StatCard icon={<Globe size={18} />} label="Öffentliche Kanäle" value={r.publicChannels.length} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0"><h3 className="text-base font-semibold">Admin-Rollen</h3></CardHeader>
|
|
||||||
<CardContent className="p-5"><EntryList entries={r.adminRoles} emptyMessage="Keine Admin-Rollen" icon={<ShieldAlert size={14} className="text-danger shrink-0" />} /></CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0"><h3 className="text-base font-semibold">Rollen, die bannen können</h3></CardHeader>
|
|
||||||
<CardContent className="p-5"><EntryList entries={r.banRoles} emptyMessage="Keine Ban-Rollen" icon={<Ban size={14} className="text-warning shrink-0" />} /></CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0"><h3 className="text-base font-semibold">Bots mit gefährlichen Rechten</h3></CardHeader>
|
|
||||||
<CardContent className="p-5">
|
|
||||||
{r.dangerousBots.length ? (
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
{r.dangerousBots.slice(0, 10).map((b) => (
|
|
||||||
<div key={b.id} className="bg-surface-tertiary flex flex-col gap-1 rounded-lg px-3 py-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2"><Bot size={14} className="text-muted shrink-0" /><span className="truncate font-medium">{b.tag}</span></div>
|
|
||||||
<div className="flex flex-wrap gap-1 pl-5">
|
|
||||||
{b.perms.map((p) => <Chip key={p} size="sm" variant="soft" color="warning">{p}</Chip>)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : <p className="py-3 text-center text-xs text-muted">Keine Bots mit gefährlichen Rechten</p>}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0"><h3 className="text-base font-semibold">@everyone kann schreiben</h3></CardHeader>
|
|
||||||
<CardContent className="p-5"><EntryList entries={r.everyoneCanSendChannels} emptyMessage="Kein Kanal betroffen" icon={<MessageSquare size={14} className="text-muted shrink-0" />} /></CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0"><h3 className="text-base font-semibold">Leere Rollen</h3></CardHeader>
|
|
||||||
<CardContent className="p-5"><EntryList entries={r.emptyRoles} emptyMessage="Keine leeren Rollen" icon={<Users size={14} className="text-muted shrink-0" />} /></CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0"><h3 className="text-base font-semibold">Nutzlose Rollen</h3></CardHeader>
|
|
||||||
<CardContent className="p-5"><EntryList entries={r.uselessRoles} emptyMessage="Keine nutzlosen Rollen" icon={<ShieldX size={14} className="text-muted shrink-0" />} /></CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="xl:col-span-2">
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0"><h3 className="text-base font-semibold">Doppelte Rollen (identische Rechte)</h3></CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-2 p-5">
|
|
||||||
{r.duplicateRoleGroups.length ? r.duplicateRoleGroups.map((group, i) => (
|
|
||||||
<div key={i} className="bg-surface-tertiary flex flex-wrap items-center gap-2 rounded-lg px-3 py-2 text-sm">
|
|
||||||
<Copy size={14} className="text-muted shrink-0" />
|
|
||||||
{group.map((role) => <Chip key={role.id} size="sm" variant="soft">{role.name}</Chip>)}
|
|
||||||
</div>
|
|
||||||
)) : <p className="py-3 text-center text-xs text-muted">Keine doppelten Rollen gefunden</p>}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, Input, Button, Chip, TextField, Label } from '@heroui/react';
|
|
||||||
import { Tag, Save, Plus, X } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { RoleSelect } from '../components/shared/RoleSelect';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
export function ReactionRoles() {
|
|
||||||
const { reactionRoles, reactionDraft, setReactionDraft, saveReactionRole, currentGuildId } = useApp();
|
|
||||||
const { channels, roles } = useGuildResources(currentGuildId);
|
|
||||||
const [entryDraft, setEntryDraft] = useState({ emoji: '', roleId: '', label: '' });
|
|
||||||
|
|
||||||
const addEntry = () => {
|
|
||||||
if (!entryDraft.emoji.trim() || !entryDraft.roleId) return;
|
|
||||||
setReactionDraft((s) => ({
|
|
||||||
...s,
|
|
||||||
entries: [...s.entries, { emoji: entryDraft.emoji.trim(), roleId: entryDraft.roleId, label: entryDraft.label.trim(), description: '' }]
|
|
||||||
}));
|
|
||||||
setEntryDraft({ emoji: '', roleId: '', label: '' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeEntry = (index: number) => {
|
|
||||||
setReactionDraft((s) => ({ ...s, entries: s.entries.filter((_: any, i: number) => i !== index) }));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Reaction Roles" subtitle="Sets anzeigen und neue Zuordnungen anlegen">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Bestehende Sets ({reactionRoles.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{reactionRoles.length ? reactionRoles.map((set, i) => (
|
|
||||||
<Card key={set.id || i}>
|
|
||||||
<CardContent className="flex items-center gap-3 p-4">
|
|
||||||
<div className="flex size-10 items-center justify-center rounded-xl bg-accent-soft text-accent-soft-foreground">
|
|
||||||
<Tag size={18} />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="font-semibold text-sm truncate">{set.title || 'Reaction Role'}</div>
|
|
||||||
<div className="text-xs text-muted truncate">Channel: {set.channelId || '-'}</div>
|
|
||||||
</div>
|
|
||||||
<Chip size="sm" variant="soft">{(set.entries?.length || 0)} Einträge</Chip>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<Tag size={24} />
|
|
||||||
Keine Sets
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Neues Set</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Titel</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Rollenauswahl"
|
|
||||||
value={reactionDraft.title}
|
|
||||||
onChange={(e) => setReactionDraft((s) => ({ ...s, title: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={reactionDraft.channelId}
|
|
||||||
onChange={(id) => setReactionDraft((s) => ({ ...s, channelId: id }))}
|
|
||||||
placeholder="Channel für die Nachricht wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label>Einträge</Label>
|
|
||||||
<div className="mt-2 flex flex-col gap-2">
|
|
||||||
{reactionDraft.entries.map((entry, i) => {
|
|
||||||
const role = roles.find((r) => r.id === entry.roleId);
|
|
||||||
return (
|
|
||||||
<div key={i} className="bg-default-soft flex items-center gap-2 rounded-xl py-1.5 pl-3 pr-1.5 text-sm">
|
|
||||||
<span>{entry.emoji}</span>
|
|
||||||
{role?.color && <span className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: role.color }} />}
|
|
||||||
<span className="min-w-0 flex-1 truncate">{entry.label || role?.name || entry.roleId}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex size-5 shrink-0 items-center justify-center rounded-full text-muted hover:bg-danger-soft hover:text-danger"
|
|
||||||
onClick={() => removeEntry(i)}
|
|
||||||
>
|
|
||||||
<X size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!reactionDraft.entries.length && <p className="text-xs text-muted">Noch keine Einträge hinzugefügt.</p>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 flex flex-col gap-2 rounded-xl border border-border p-3">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
className="w-16 shrink-0"
|
|
||||||
placeholder="😀"
|
|
||||||
value={entryDraft.emoji}
|
|
||||||
onChange={(e) => setEntryDraft((s) => ({ ...s, emoji: e.target.value }))}
|
|
||||||
/>
|
|
||||||
<RoleSelect
|
|
||||||
options={roles}
|
|
||||||
value={entryDraft.roleId}
|
|
||||||
onChange={(id) => setEntryDraft((s) => ({ ...s, roleId: id }))}
|
|
||||||
placeholder="Rolle wählen"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder="Label (optional, z.B. Gamer)"
|
|
||||||
value={entryDraft.label}
|
|
||||||
onChange={(e) => setEntryDraft((s) => ({ ...s, label: e.target.value }))}
|
|
||||||
/>
|
|
||||||
<Button size="sm" variant="tertiary" onPress={addEntry} isDisabled={!entryDraft.emoji.trim() || !entryDraft.roleId}>
|
|
||||||
<Plus size={14} /> Eintrag hinzufügen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={saveReactionRole} isDisabled={!reactionDraft.entries.length || !reactionDraft.channelId}>
|
|
||||||
<Save size={16} /> Reaction Role speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Tabs, Tab, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { ClipboardList, Pencil, Trash2, Send, Plus, FileText, History, MessageSquare } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { formatDate } from '../utils/formatters';
|
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, string> = {
|
|
||||||
pending: 'Ausstehend',
|
|
||||||
accepted: 'Akzeptiert',
|
|
||||||
invited: 'Zum Gespräch eingeladen',
|
|
||||||
rejected: 'Abgelehnt',
|
|
||||||
};
|
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, 'success' | 'danger' | 'warning' | 'accent' | 'default'> = {
|
|
||||||
pending: 'warning',
|
|
||||||
accepted: 'success',
|
|
||||||
invited: 'accent',
|
|
||||||
rejected: 'danger',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Register() {
|
|
||||||
const {
|
|
||||||
registerForms, registerApps, registerTab, setRegisterTab,
|
|
||||||
formDraft, setFormDraft, editingFormId, setEditingFormId,
|
|
||||||
saveForm, deleteForm, sendFormPanel,
|
|
||||||
registerStatusFilter, registerFormFilter, setRegisterStatusFilter, setRegisterFormFilter, loadRegisterApps,
|
|
||||||
selectedAppId, openAppDetail, appHistory, appNotes, noteDraft, setNoteDraft, addAppNote
|
|
||||||
} = useApp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Registrierungsformulare" subtitle="Bewerbungs-Formulare und eingegangene Anträge verwalten.">
|
|
||||||
<Tabs selectedKey={registerTab} variant="primary" onSelectionChange={(key) => setRegisterTab(String(key))}>
|
|
||||||
<Tabs.ListContainer>
|
|
||||||
<Tabs.List aria-label="Register Tabs">
|
|
||||||
<Tab id="forms">Formulare</Tab>
|
|
||||||
<Tab id="apps">Anträge</Tab>
|
|
||||||
</Tabs.List>
|
|
||||||
</Tabs.ListContainer>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
{registerTab === 'forms' && (
|
|
||||||
<div className="mt-5 grid gap-5 xl:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Bestehende Formulare ({registerForms.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{registerForms.length ? registerForms.map((f) => (
|
|
||||||
<Card key={f.id}>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<FileText size={16} className="text-accent shrink-0" />
|
|
||||||
<span className="font-semibold text-sm truncate">{f.name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button isIconOnly size="sm" variant="ghost" onPress={() => {
|
|
||||||
setFormDraft({
|
|
||||||
name: f.name, description: f.description || '',
|
|
||||||
reviewChannelId: f.reviewChannelId || '',
|
|
||||||
notifyRoleIds: (f.notifyRoleIds || []).join(', '),
|
|
||||||
fields: (f.fields || []).map((fd) =>
|
|
||||||
fd.label + '|' + fd.type + (fd.required ? '|required' : '') + (fd.options ? '|' + fd.options.join(',') : '')
|
|
||||||
).join('\n')
|
|
||||||
});
|
|
||||||
setEditingFormId(f.id);
|
|
||||||
}}>
|
|
||||||
<Pencil size={14} />
|
|
||||||
</Button>
|
|
||||||
<Button isIconOnly size="sm" variant="danger-soft" onPress={() => deleteForm(f.id)}>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted">{f.description || 'Keine Beschreibung'}</p>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<Chip size="sm" variant="soft" color={f.isActive ? 'success' : 'default'}>
|
|
||||||
{f.isActive ? 'Aktiv' : 'Inaktiv'}
|
|
||||||
</Chip>
|
|
||||||
<span className="text-muted">{f.fields?.length || 0} Felder</span>
|
|
||||||
<Button size="sm" variant="tertiary" onPress={() => sendFormPanel(f.id)}>
|
|
||||||
<Send size={12} /> Panel senden
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<ClipboardList size={24} />
|
|
||||||
Keine Formulare
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">{editingFormId ? 'Formular bearbeiten' : 'Neues Formular'}</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Name</Label>
|
|
||||||
<Input value={formDraft.name} onChange={(e) => setFormDraft((s) => ({ ...s, name: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Beschreibung</Label>
|
|
||||||
<Input value={formDraft.description} onChange={(e) => setFormDraft((s) => ({ ...s, description: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Review Channel ID</Label>
|
|
||||||
<Input value={formDraft.reviewChannelId} onChange={(e) => setFormDraft((s) => ({ ...s, reviewChannelId: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Benachrichtigungs-Rollen (Komma-getrennt)</Label>
|
|
||||||
<Input value={formDraft.notifyRoleIds} onChange={(e) => setFormDraft((s) => ({ ...s, notifyRoleIds: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Felder (label|type|required|options)</Label>
|
|
||||||
<TextArea rows={6} value={formDraft.fields} onChange={(e) => setFormDraft((s) => ({ ...s, fields: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<p className="text-xs text-muted">
|
|
||||||
Pro Zeile: label | type (text/paragraph/select/multi) | required | option1,option2
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="primary" onPress={saveForm}>{editingFormId ? 'Aktualisieren' : 'Erstellen'}</Button>
|
|
||||||
{editingFormId && (
|
|
||||||
<Button variant="tertiary" onPress={() => { setEditingFormId(null); setFormDraft({ name: '', description: '', reviewChannelId: '', notifyRoleIds: '', fields: '' }); }}>
|
|
||||||
Abbrechen
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{registerTab === 'apps' && (
|
|
||||||
<div className="mt-5">
|
|
||||||
<div className="mb-4 flex flex-wrap items-end gap-2">
|
|
||||||
<TextField className="w-56">
|
|
||||||
<Label className="text-xs">Formular/Position</Label>
|
|
||||||
<select
|
|
||||||
className="w-full rounded-xl px-3 py-2 text-sm"
|
|
||||||
value={registerFormFilter}
|
|
||||||
onChange={(e) => { setRegisterFormFilter(e.target.value); loadRegisterApps({ formId: e.target.value }); }}
|
|
||||||
>
|
|
||||||
<option value="">Alle</option>
|
|
||||||
{registerForms.map((f) => <option key={f.id} value={f.id}>{f.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</TextField>
|
|
||||||
<TextField className="w-48">
|
|
||||||
<Label className="text-xs">Status</Label>
|
|
||||||
<select
|
|
||||||
className="w-full rounded-xl px-3 py-2 text-sm"
|
|
||||||
value={registerStatusFilter}
|
|
||||||
onChange={(e) => { setRegisterStatusFilter(e.target.value); loadRegisterApps({ status: e.target.value }); }}
|
|
||||||
>
|
|
||||||
<option value="">Alle</option>
|
|
||||||
{Object.entries(STATUS_LABELS).map(([value, label]) => (
|
|
||||||
<option key={value} value={value}>{label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</TextField>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Eingegangene Anträge ({registerApps.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{registerApps.length ? registerApps.map((app) => (
|
|
||||||
<Card key={app.id}>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-4">
|
|
||||||
<button type="button" className="flex items-center justify-between gap-2 text-left" onClick={() => openAppDetail(app.id)}>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-semibold truncate">{app.username || app.userId}</div>
|
|
||||||
<div className="text-xs text-muted truncate">{app.form?.name || 'Formular'}</div>
|
|
||||||
</div>
|
|
||||||
<Chip size="sm" variant="soft" color={STATUS_COLORS[app.status] || 'default'}>
|
|
||||||
{STATUS_LABELS[app.status] || app.status}
|
|
||||||
</Chip>
|
|
||||||
</button>
|
|
||||||
<div className="text-xs text-muted">{formatDate(app.createdAt)}</div>
|
|
||||||
{app.answers?.length ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{app.answers.map((a, i) => (
|
|
||||||
<div key={i} className="text-sm">
|
|
||||||
<span className="text-muted">{a.label || 'Frage'}: </span>
|
|
||||||
{a.value}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{selectedAppId === app.id && (
|
|
||||||
<div className="mt-2 grid gap-4 border-t border-border pt-3 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="mb-2 flex items-center gap-1.5 text-sm font-semibold"><History size={14} /> Bisherige Bewerbungen</h4>
|
|
||||||
{appHistory.length ? (
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{appHistory.map((h) => (
|
|
||||||
<div key={h.id} className="flex items-center justify-between gap-2 text-xs">
|
|
||||||
<span className="truncate">{h.form?.name || 'Formular'} · {formatDate(h.createdAt)}</span>
|
|
||||||
<Chip size="sm" variant="soft" color={STATUS_COLORS[h.status] || 'default'}>{STATUS_LABELS[h.status] || h.status}</Chip>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : <p className="text-xs text-muted">Keine weiteren Bewerbungen dieses Nutzers</p>}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="mb-2 flex items-center gap-1.5 text-sm font-semibold"><MessageSquare size={14} /> Interne Notizen</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{appNotes.length ? appNotes.map((n) => (
|
|
||||||
<div key={n.id} className="rounded-lg bg-surface-secondary p-2 text-xs">
|
|
||||||
<div className="mb-1 flex items-center justify-between text-muted">
|
|
||||||
<span className="font-medium text-default">{n.authorTag}</span>
|
|
||||||
<span>{formatDate(n.createdAt)}</span>
|
|
||||||
</div>
|
|
||||||
{n.body}
|
|
||||||
</div>
|
|
||||||
)) : <p className="text-xs text-muted">Keine Notizen</p>}
|
|
||||||
</div>
|
|
||||||
<TextArea
|
|
||||||
className="mt-2" rows={2} placeholder="Interne Notiz hinzufügen..."
|
|
||||||
value={noteDraft} onChange={(e) => setNoteDraft(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button size="sm" variant="tertiary" className="mt-2" isDisabled={!noteDraft.trim()} onPress={addAppNote}>
|
|
||||||
Notiz speichern
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<ClipboardList size={24} />
|
|
||||||
Keine Anträge
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Input, Button, Chip, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { Activity, Save, Trash2, Plus, BarChart3 } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
|
|
||||||
|
|
||||||
export function ServerStats() {
|
|
||||||
const { statsDraft, setStatsDraft, saveServerStats, statsItemDraft, setStatsItemDraft, addStatsItem, deleteStatsItem } = useApp();
|
|
||||||
|
|
||||||
const items = (statsDraft?.items || []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Server Stats" subtitle="Counter und Refresh-Intervall steuern">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[420px_1fr]">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Konfiguration</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<ModuleActiveToggle
|
|
||||||
icon={<BarChart3 size={16} />}
|
|
||||||
title="Server Stats aktiv"
|
|
||||||
description="Zeigt Mitglieder-/Channel-Zahlen als Voice-Statistiken an."
|
|
||||||
isSelected={statsDraft?.enabled === true}
|
|
||||||
onChange={(v) => setStatsDraft((s) => ({ ...(s || {}), enabled: v }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Kategorie-Name</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Server Stats"
|
|
||||||
value={statsDraft?.categoryName || ''}
|
|
||||||
onChange={(e) => setStatsDraft((s) => ({ ...(s || {}), categoryName: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Refresh (Minuten)</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={String(statsDraft?.refreshMinutes || 10)}
|
|
||||||
onChange={(e) => setStatsDraft((s) => ({ ...(s || {}), refreshMinutes: Number(e.target.value || 10) }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={saveServerStats}>
|
|
||||||
<Save size={16} /> Server Stats speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Items ({items.length})</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-5">
|
|
||||||
{items.length ? items.map((item, i) => (
|
|
||||||
<div key={i} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Activity size={14} className="text-accent" />
|
|
||||||
<span className="font-medium">{item.label || item.key}</span>
|
|
||||||
<Chip size="sm" variant="soft">{item.type || '-'}</Chip>
|
|
||||||
</div>
|
|
||||||
<Button isIconOnly size="sm" variant="danger-soft" onPress={() => deleteStatsItem(i)}>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<BarChart3 size={20} />
|
|
||||||
Keine Items
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-semibold mb-2">Item hinzufügen</h4>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Input placeholder="Label" value={statsItemDraft.label} onChange={(e) => setStatsItemDraft((s) => ({ ...s, label: e.target.value }))} />
|
|
||||||
<select
|
|
||||||
className="w-full rounded-xl px-3 py-2 text-sm"
|
|
||||||
value={statsItemDraft.type}
|
|
||||||
onChange={(e) => setStatsItemDraft((s) => ({ ...s, type: e.target.value }))}
|
|
||||||
>
|
|
||||||
<option value="members">Mitglieder</option>
|
|
||||||
<option value="channels">Channels</option>
|
|
||||||
<option value="roles">Rollen</option>
|
|
||||||
<option value="boosts">Boosts</option>
|
|
||||||
<option value="online">Online</option>
|
|
||||||
<option value="custom">Custom</option>
|
|
||||||
</select>
|
|
||||||
<Button size="sm" variant="primary" onPress={addStatsItem}>
|
|
||||||
<Plus size={14} /> Hinzufügen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Button, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { Settings, Save, Logs, Bell, Shield, Edit3, Trash2, ImageIcon, QrCode } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { RoleSelect } from '../components/shared/RoleSelect';
|
|
||||||
import { AppSwitch } from '../components/shared/AppSwitch';
|
|
||||||
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
export function SettingsPage() {
|
|
||||||
const { settings, setSettings, saveSettingsPayload, currentGuildId } = useApp();
|
|
||||||
const { channels, roles } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Einstellungen & Logging" subtitle="Globale Guild-Settings und Log-Kategorien">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Allgemein</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Welcome Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={settings.welcomeChannelId}
|
|
||||||
onChange={(id) => setSettings((s) => ({ ...s, welcomeChannelId: id }))}
|
|
||||||
placeholder="Channel wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Log Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={settings.logChannelId}
|
|
||||||
onChange={(id) => setSettings((s) => ({ ...s, logChannelId: id }))}
|
|
||||||
placeholder="Channel wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Support Rolle</Label>
|
|
||||||
<RoleSelect
|
|
||||||
options={roles}
|
|
||||||
value={settings.supportRoleId}
|
|
||||||
onChange={(id) => setSettings((s) => ({ ...s, supportRoleId: id }))}
|
|
||||||
placeholder="Rolle wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={() => saveSettingsPayload(settings, 'Settings gespeichert')}>
|
|
||||||
<Save size={16} /> Speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Logging Kategorien</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<AppSwitch
|
|
||||||
isSelected={settings.loggingConfig?.categories?.joinLeave !== false}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, loggingConfig: { ...(s.loggingConfig || {}), categories: { ...(s.loggingConfig?.categories || {}), joinLeave: v } } }))}
|
|
||||||
label={<div className="flex items-center gap-2"><Logs size={14} /> Join / Leave loggen</div>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AppSwitch
|
|
||||||
isSelected={settings.loggingConfig?.categories?.messageEdit !== false}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, loggingConfig: { ...(s.loggingConfig || {}), categories: { ...(s.loggingConfig?.categories || {}), messageEdit: v } } }))}
|
|
||||||
label={<div className="flex items-center gap-2"><Edit3 size={14} /> Message Edit loggen</div>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AppSwitch
|
|
||||||
isSelected={settings.loggingConfig?.categories?.messageDelete !== false}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, loggingConfig: { ...(s.loggingConfig || {}), categories: { ...(s.loggingConfig?.categories || {}), messageDelete: v } } }))}
|
|
||||||
label={<div className="flex items-center gap-2"><Trash2 size={14} /> Message Delete loggen</div>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AppSwitch
|
|
||||||
isSelected={settings.loggingConfig?.categories?.automodActions !== false}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, loggingConfig: { ...(s.loggingConfig || {}), categories: { ...(s.loggingConfig?.categories || {}), automodActions: v } } }))}
|
|
||||||
label={<div className="flex items-center gap-2"><Shield size={14} /> Automod Actions loggen</div>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AppSwitch
|
|
||||||
isSelected={settings.loggingConfig?.categories?.ticketActions !== false}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, loggingConfig: { ...(s.loggingConfig || {}), categories: { ...(s.loggingConfig?.categories || {}), ticketActions: v } } }))}
|
|
||||||
label={<div className="flex items-center gap-2"><Bell size={14} /> Ticket Actions loggen</div>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={() => saveSettingsPayload(settings, 'Settings gespeichert')}>
|
|
||||||
<Save size={16} /> Speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Bild-Moderation</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<ModuleActiveToggle
|
|
||||||
icon={<ImageIcon size={16} />}
|
|
||||||
title="Bild-Moderation aktiv"
|
|
||||||
description="Scannt Bild-Anhänge auf QR-Codes und gleicht sie mit bekannten Scam-Mustern ab."
|
|
||||||
isSelected={settings.imageModerationConfig?.enabled === true}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, imageModerationConfig: { ...(s.imageModerationConfig || {}), enabled: v } }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AppSwitch
|
|
||||||
isSelected={settings.imageModerationConfig?.alertOnly === true}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, imageModerationConfig: { ...(s.imageModerationConfig || {}), alertOnly: v } }))}
|
|
||||||
label={<div className="flex items-center gap-2"><QrCode size={14} /> Nur bei erkanntem Scam-Verdacht alarmieren</div>}
|
|
||||||
description="Wenn aus, wird bei jedem gefundenen QR-Code alarmiert, nicht nur bei verdächtigen."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={() => saveSettingsPayload(settings, 'Settings gespeichert')}>
|
|
||||||
<Save size={16} /> Speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Input, Button, Chip, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { RadioTower, Save, Trash2, Plus, Activity as ActivityIcon } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import type { StatusService } from '../types';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
export function Statuspage() {
|
|
||||||
const { statusDraft, setStatusDraft, saveStatuspage, statusServiceDraft, setStatusServiceDraft, addStatusService, deleteStatusService, currentGuildId } = useApp();
|
|
||||||
const { channels } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
const services = ((statusDraft?.services || []) as StatusService[]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Statuspage" subtitle="Statusseite und Service-Liste verwalten">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[420px_1fr]">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Konfiguration</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<ModuleActiveToggle
|
|
||||||
icon={<RadioTower size={16} />}
|
|
||||||
title="Statuspage aktiv"
|
|
||||||
description="Postet und aktualisiert die Statusseite mit deinen Services."
|
|
||||||
isSelected={statusDraft?.enabled !== false}
|
|
||||||
onChange={(v) => setStatusDraft((s) => ({ ...(s || {}), enabled: v }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={statusDraft?.channelId}
|
|
||||||
onChange={(id) => setStatusDraft((s) => ({ ...(s || {}), channelId: id }))}
|
|
||||||
placeholder="Channel für Status-Updates wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Intervall (ms)</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={String(statusDraft?.intervalMs || 60000)}
|
|
||||||
onChange={(e) => setStatusDraft((s) => ({ ...(s || {}), intervalMs: Number(e.target.value || 60000) }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Button variant="primary" onPress={saveStatuspage}>
|
|
||||||
<Save size={16} /> Statuspage speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Services ({services.length})</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-5">
|
|
||||||
{services.length ? services.map((service) => (
|
|
||||||
<div key={service.id} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
|
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
|
||||||
<div className="size-2 rounded-full shrink-0" />
|
|
||||||
<div className="min-w-0">
|
|
||||||
<span className="font-medium">{service.name || 'Service'}</span>
|
|
||||||
<span className="ml-2 text-muted text-xs truncate">{service.url || ''}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Chip size="sm" variant="soft" color={
|
|
||||||
service.status === 'operational' ? 'success' :
|
|
||||||
service.status === 'degraded' ? 'warning' :
|
|
||||||
service.status === 'down' ? 'danger' : 'default'
|
|
||||||
}>{service.status || 'unknown'}</Chip>
|
|
||||||
<Button isIconOnly size="sm" variant="danger-soft" onPress={() => deleteStatusService(service.id)}>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<ActivityIcon size={20} />
|
|
||||||
Keine Services
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-semibold mb-2">Service hinzufügen</h4>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Input placeholder="Name" value={statusServiceDraft.name} onChange={(e) => setStatusServiceDraft((s) => ({ ...s, name: e.target.value }))} />
|
|
||||||
<Input placeholder="URL (optional)" value={statusServiceDraft.url} onChange={(e) => setStatusServiceDraft((s) => ({ ...s, url: e.target.value }))} />
|
|
||||||
<Button size="sm" variant="primary" onPress={addStatusService}>
|
|
||||||
<Plus size={14} /> Hinzufügen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, TextArea, Button, Chip, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { UserRound, Save, Send } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { AppAvatar } from '../components/shared/AppAvatar';
|
|
||||||
import { DiscordPreview, DiscordButton } from '../components/shared/DiscordPreview';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { AppSwitch } from '../components/shared/AppSwitch';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
export function SupportLogin() {
|
|
||||||
const { supportLogin, setSupportLogin, saveSupportLogin, currentGuildId } = useApp();
|
|
||||||
const { channels } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Support Login" subtitle="Login-Panel fuer Supporter konfigurieren">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[1fr_400px]">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<CardTitle>Panel-Konfiguration</CardTitle>
|
|
||||||
<CardDescription>Texte und Zielkanal mit normalen HeroUI-Feldern pflegen.</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<AppSwitch
|
|
||||||
isSelected={supportLogin?.config?.autoRefresh !== false}
|
|
||||||
onChange={(v) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, autoRefresh: v } } : s)}
|
|
||||||
label="Auto-Refresh aktiv"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Panel Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={supportLogin?.config?.panelChannelId}
|
|
||||||
onChange={(id) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, panelChannelId: id } } : s)}
|
|
||||||
placeholder="Channel für das Panel wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Titel</Label>
|
|
||||||
<Input
|
|
||||||
value={supportLogin?.config?.title || 'Support Login'}
|
|
||||||
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, title: e.target.value } } : s)}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Beschreibung</Label>
|
|
||||||
<TextArea
|
|
||||||
value={supportLogin?.config?.description || ''}
|
|
||||||
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, description: e.target.value } } : s)}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Login Button Label</Label>
|
|
||||||
<Input
|
|
||||||
value={supportLogin?.config?.loginLabel || 'Ich bin jetzt im Support'}
|
|
||||||
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, loginLabel: e.target.value } } : s)}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Logout Button Label</Label>
|
|
||||||
<Input
|
|
||||||
value={supportLogin?.config?.logoutLabel || 'Ich bin nicht mehr im Support'}
|
|
||||||
onChange={(e) => setSupportLogin((s) => s ? { ...s, config: { ...s.config, logoutLabel: e.target.value } } : s)}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="primary" onPress={saveSupportLogin}>
|
|
||||||
<Save size={16} /> Speichern & Panel senden
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost">
|
|
||||||
<Send size={16} /> Panel manuell senden
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<CardTitle>Live Vorschau</CardTitle>
|
|
||||||
<CardDescription>So sieht das Panel auf Discord aus.</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<DiscordPreview
|
|
||||||
title={supportLogin?.config?.title || 'Support Login'}
|
|
||||||
description={supportLogin?.config?.description || 'Melde dich als Support an/ab.'}
|
|
||||||
>
|
|
||||||
<DiscordButton variant="primary">{supportLogin?.config?.loginLabel || 'Ich bin jetzt im Support'}</DiscordButton>
|
|
||||||
<DiscordButton variant="secondary">{supportLogin?.config?.logoutLabel || 'Ich bin nicht mehr im Support'}</DiscordButton>
|
|
||||||
</DiscordPreview>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<CardTitle>Aktive Supporter</CardTitle>
|
|
||||||
<CardDescription>Aktueller Status aus der Guild.</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-2">
|
|
||||||
{supportLogin?.status?.active?.length ? supportLogin.status.active.map((s, i) => (
|
|
||||||
<Card key={i} className="bg-surface-tertiary">
|
|
||||||
<CardContent className="flex items-center gap-3">
|
|
||||||
<AppAvatar name={s.username} size="sm" className="size-6" />
|
|
||||||
<span className="font-medium">{s.username || s.userId}</span>
|
|
||||||
<Chip size="sm" variant="soft" color="success" className="ml-auto">Online</Chip>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
|
|
||||||
<UserRound size={20} />
|
|
||||||
Keine aktiven Supporter
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="mt-2 text-xs text-muted">
|
|
||||||
Support Role ID: {supportLogin?.supportRoleId || 'Nicht gesetzt'}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, TextField, Label } from '@heroui/react';
|
|
||||||
import { ListChecks, Trash2, Plus } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { formatDate } from '../utils/formatters';
|
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, string> = { open: 'Offen', in_progress: 'In Bearbeitung', done: 'Erledigt' };
|
|
||||||
const STATUS_COLORS: Record<string, 'warning' | 'accent' | 'success'> = { open: 'warning', in_progress: 'accent', done: 'success' };
|
|
||||||
|
|
||||||
export function Tasks() {
|
|
||||||
const { tasks, taskDraft, setTaskDraft, createTask, updateTaskStatus, deleteTask } = useApp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Team-Aufgaben" subtitle="Aufgaben für dein Team erstellen und Status pflegen.">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-[1fr_360px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Aufgaben ({tasks.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{tasks.length ? tasks.map((t) => (
|
|
||||||
<Card key={t.id}>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-4">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-semibold truncate">{t.title}</div>
|
|
||||||
<div className="text-xs text-muted">von {t.createdByTag} · {formatDate(t.createdAt)}</div>
|
|
||||||
</div>
|
|
||||||
<Button isIconOnly size="sm" variant="danger-soft" onPress={() => deleteTask(t.id)}>
|
|
||||||
<Trash2 size={14} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{t.description && <p className="text-sm text-muted">{t.description}</p>}
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<select
|
|
||||||
className="rounded-xl px-3 py-2 text-sm"
|
|
||||||
value={t.status}
|
|
||||||
onChange={(e) => updateTaskStatus(t.id, e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="open">Offen</option>
|
|
||||||
<option value="in_progress">In Bearbeitung</option>
|
|
||||||
<option value="done">Erledigt</option>
|
|
||||||
</select>
|
|
||||||
<Chip size="sm" variant="soft" color={STATUS_COLORS[t.status] || 'warning'}>
|
|
||||||
{STATUS_LABELS[t.status] || t.status}
|
|
||||||
</Chip>
|
|
||||||
{t.assigneeId && <span className="text-xs text-muted">Zugewiesen an User-ID {t.assigneeId}</span>}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<ListChecks size={24} />
|
|
||||||
Keine Aufgaben
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Neue Aufgabe</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Titel</Label>
|
|
||||||
<Input value={taskDraft.title} onChange={(e) => setTaskDraft((s) => ({ ...s, title: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Beschreibung</Label>
|
|
||||||
<TextArea rows={4} value={taskDraft.description} onChange={(e) => setTaskDraft((s) => ({ ...s, description: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<p className="text-xs text-muted">Eine zuständige Person lässt sich aktuell nur über <code>/task create</code> in Discord zuweisen.</p>
|
|
||||||
<Button variant="primary" onPress={createTask} isDisabled={!taskDraft.title.trim()}>
|
|
||||||
<Plus size={16} /> Aufgabe erstellen
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,445 +0,0 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
|
||||||
import { Card, CardContent, CardHeader, Chip, Button, Tabs, Tab, Input, TextArea, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { Ticket, Clock, UserRound, CheckCircle, MessageSquare, FileText, Pencil, Trash2, ChevronRight, Tag, Users } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { formatDate } from '../utils/formatters';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { StatCard } from '../components/shared/StatCard';
|
|
||||||
import { EmptyState } from '../components/shared/EmptyState';
|
|
||||||
import { TicketKanban } from '../components/shared/TicketKanban';
|
|
||||||
import { RoleSelect } from '../components/shared/RoleSelect';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
export function Tickets() {
|
|
||||||
const {
|
|
||||||
currentGuildId, tickets, pipeline, sla, automations, kbArticles, ticketTab, setTicketTab,
|
|
||||||
ticketDetail, setTicketDetail, ticketMessages, loadTicketMessages,
|
|
||||||
updateTicketStatus, closeTicket, automationDraft, setAutomationDraft,
|
|
||||||
saveAutomation, kbDraft, setKbDraft, saveKbArticle, kbEditDraft, setKbEditDraft,
|
|
||||||
updateKbArticle, deleteKbArticle, automationEditDraft, setAutomationEditDraft,
|
|
||||||
updateAutomation, deleteAutomation, overview,
|
|
||||||
ticketTopics, loadTicketTopics, ticketTopicDraft, setTicketTopicDraft, saveTicketTopic
|
|
||||||
} = useApp();
|
|
||||||
const { roles } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentGuildId) loadTicketTopics();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentGuildId]);
|
|
||||||
|
|
||||||
const openTickets = useMemo(() => tickets.filter((t) => t.status !== 'closed'), [tickets]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Ticketsystem" subtitle="Ticket-Übersicht, Pipeline, SLA, Automationen und Knowledge Base.">
|
|
||||||
<Tabs selectedKey={ticketTab} variant="primary" onSelectionChange={(key) => setTicketTab(String(key))}>
|
|
||||||
<Tabs.ListContainer>
|
|
||||||
<Tabs.List aria-label="Ticket Tabs">
|
|
||||||
<Tab id="overview">Übersicht</Tab>
|
|
||||||
<Tab id="pipeline">Pipeline</Tab>
|
|
||||||
<Tab id="sla">SLA</Tab>
|
|
||||||
<Tab id="automations">Automationen</Tab>
|
|
||||||
<Tab id="kb">Knowledge Base</Tab>
|
|
||||||
<Tab id="kategorien">Kategorien</Tab>
|
|
||||||
</Tabs.List>
|
|
||||||
</Tabs.ListContainer>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
{ticketTab === 'overview' && (
|
|
||||||
<div className="mt-5 space-y-5">
|
|
||||||
<div className="grid gap-4 sm:grid-cols-4">
|
|
||||||
<StatCard icon={<Ticket size={18} />} label="Offen" value={overview?.tickets?.open ?? 0} color="warning" />
|
|
||||||
<StatCard icon={<Clock size={18} />} label="In Bearbeitung" value={overview?.tickets?.inProgress ?? 0} color="accent" />
|
|
||||||
<StatCard icon={<CheckCircle size={18} />} label="Geschlossen" value={overview?.tickets?.closed ?? 0} color="default" />
|
|
||||||
<StatCard icon={<UserRound size={18} />} label="Gesamt" value={tickets.length} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Offene Tickets ({openTickets.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{openTickets.length ? openTickets.map((t) => (
|
|
||||||
<Card key={t.id}>
|
|
||||||
<CardContent className="flex flex-col gap-3 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<div className={`size-2 rounded-full shrink-0 ${
|
|
||||||
t.status === 'neu' ? 'bg-warning' :
|
|
||||||
t.status === 'in_bearbeitung' ? 'bg-accent' :
|
|
||||||
t.status === 'warten_auf_user' ? 'bg-default' : 'bg-success'
|
|
||||||
}`} />
|
|
||||||
<span className="font-semibold text-sm truncate">{t.topic || 'Ticket'}</span>
|
|
||||||
</div>
|
|
||||||
<Chip size="sm" variant="soft" color={t.status === 'neu' ? 'warning' : t.status === 'erledigt' ? 'default' : 'accent'}>
|
|
||||||
{t.status || 'neu'}
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs text-muted">
|
|
||||||
<span>{t.category || 'Allgemein'}</span>
|
|
||||||
<span>·</span>
|
|
||||||
<span>{formatDate(t.createdAt)}</span>
|
|
||||||
{t.claimedById && (
|
|
||||||
<>
|
|
||||||
<span>·</span>
|
|
||||||
<Chip size="sm" variant="soft" color="success" className="h-5">Claimed</Chip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<select
|
|
||||||
aria-label="Status"
|
|
||||||
className="w-40 rounded-xl text-sm"
|
|
||||||
value=""
|
|
||||||
onChange={(e) => { if (e.target.value) updateTicketStatus(t.id, e.target.value); }}
|
|
||||||
>
|
|
||||||
<option value="">Status ändern</option>
|
|
||||||
<option value="neu">Neu</option>
|
|
||||||
<option value="in_bearbeitung">In Bearbeitung</option>
|
|
||||||
<option value="warten_auf_user">Warten auf User</option>
|
|
||||||
<option value="erledigt">Erledigt</option>
|
|
||||||
</select>
|
|
||||||
<Button size="sm" variant="danger" onPress={() => closeTicket(t.id)}>Schließen</Button>
|
|
||||||
<Button size="sm" variant="tertiary" onPress={() => { setTicketDetail(t); loadTicketMessages(t.id); }}>Details</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : <EmptyState message="Keine offenen Tickets" icon={<Ticket size={24} />} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Alle Tickets ({tickets.length})</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{tickets.length ? tickets.map((t) => (
|
|
||||||
<div key={t.id} className="bg-surface flex items-center justify-between rounded-xl px-4 py-3">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="text-sm font-medium truncate">{t.topic || t.id}</div>
|
|
||||||
<div className="text-xs text-muted">{t.category || '-'} · {formatDate(t.createdAt)}</div>
|
|
||||||
</div>
|
|
||||||
<Chip size="sm" variant="soft" color={t.status === 'neu' ? 'warning' : t.status === 'erledigt' ? 'default' : 'accent'}>
|
|
||||||
{t.status}
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
)) : <EmptyState message="Keine Tickets" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ticketDetail && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex items-center justify-between px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-lg font-bold">{ticketDetail.topic || 'Ticket-Details'}</h3>
|
|
||||||
<Button size="sm" variant="ghost" onPress={() => setTicketDetail(null)}>Schließen</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
|
||||||
<div className="rounded-lg bg-default-soft px-3 py-2">
|
|
||||||
<span className="text-muted">Status:</span> {ticketDetail.status}
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg bg-default-soft px-3 py-2">
|
|
||||||
<span className="text-muted">Priorität:</span> {ticketDetail.priority || 'normal'}
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg bg-default-soft px-3 py-2">
|
|
||||||
<span className="text-muted">Kategorie:</span> {ticketDetail.category || '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<div>
|
|
||||||
<h4 className="mb-2 text-sm font-semibold">Nachrichten ({ticketMessages.length})</h4>
|
|
||||||
<div className="max-h-60 space-y-2 overflow-auto">
|
|
||||||
{ticketMessages.length ? ticketMessages.map((msg, i) => (
|
|
||||||
<div key={i} className="rounded-lg bg-default-soft px-3 py-2 text-sm">
|
|
||||||
<span className="text-xs text-muted">{msg.author?.tag || msg.authorId}: </span>
|
|
||||||
{msg.content || '(Embed)'}
|
|
||||||
</div>
|
|
||||||
)) : <p className="text-xs text-muted">Keine Nachrichten</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ticketTab === 'pipeline' && (
|
|
||||||
<TicketKanban pipeline={pipeline} updateTicketStatus={updateTicketStatus} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ticketTab === 'sla' && (
|
|
||||||
<div className="mt-5 grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">SLA pro Supporter</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-2 p-5">
|
|
||||||
{(sla.supporters || []).length ? sla.supporters.map((row: any, i: number) => (
|
|
||||||
<div key={i} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-3 text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<UserRound size={14} className="text-muted" />
|
|
||||||
<span className="font-medium">{row.supporterId || '-'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 text-xs text-muted">
|
|
||||||
<span>{row.tickets || 0} Tickets</span>
|
|
||||||
<span>TTC: {row.avgTTC ? `${Math.round(row.avgTTC / 1000)}s` : '-'}</span>
|
|
||||||
<span>TTFR: {row.avgTTFR ? `${Math.round(row.avgTTFR / 1000)}s` : '-'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : <p className="text-xs text-muted text-center py-4">Keine Daten</p>}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">SLA pro Tag</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-2 p-5">
|
|
||||||
{(sla.days || []).length ? sla.days.slice(-14).map((row: any, i: number) => (
|
|
||||||
<div key={i} className="bg-surface-tertiary flex items-center justify-between rounded-xl px-4 py-2 text-xs">
|
|
||||||
<span className="font-medium">{row.date || '-'}</span>
|
|
||||||
<div className="flex gap-2 text-muted">
|
|
||||||
<span>{row.tickets || 0}</span>
|
|
||||||
<span>TTC: {row.avgTTC ? `${Math.round(row.avgTTC / 1000)}s` : '-'}</span>
|
|
||||||
<span>TTFR: {row.avgTTFR ? `${Math.round(row.avgTTFR / 1000)}s` : '-'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)) : <p className="text-xs text-muted text-center py-4">Keine Daten</p>}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ticketTab === 'automations' && (
|
|
||||||
<div className="mt-5 grid gap-5 xl:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Regeln ({automations.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{automations.length ? automations.map((rule) => (
|
|
||||||
<Card key={rule.id}>
|
|
||||||
<CardContent className="flex flex-col gap-2 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="font-semibold text-sm">{rule.name || 'Automation'}</div>
|
|
||||||
<Chip size="sm" variant="soft" color={rule.active !== false ? 'success' : 'default'}>
|
|
||||||
{rule.active !== false ? 'Aktiv' : 'Inaktiv'}
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button size="sm" variant="tertiary" onPress={() => setAutomationEditDraft({ id: rule.id, name: rule.name || '', conditionValue: rule.condition?.category || '', actionValue: rule.action?.message || '' })}>
|
|
||||||
<Pencil size={14} /> Bearbeiten
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="danger-soft" onPress={() => deleteAutomation(rule.id)}>
|
|
||||||
<Trash2 size={14} /> Löschen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : <EmptyState message="Keine Regeln" icon={<FileText size={24} />} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{automationEditDraft ? (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Automation bearbeiten</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Name</Label>
|
|
||||||
<Input value={automationEditDraft.name} onChange={(e) => setAutomationEditDraft((s) => ({ ...s, name: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Kategorie / Zustand</Label>
|
|
||||||
<Input value={automationEditDraft.conditionValue} onChange={(e) => setAutomationEditDraft((s) => ({ ...s, conditionValue: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Aktion / Nachricht</Label>
|
|
||||||
<TextArea value={automationEditDraft.actionValue} onChange={(e) => setAutomationEditDraft((s) => ({ ...s, actionValue: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="primary" onPress={() => updateAutomation(automationEditDraft.id)}>Aktualisieren</Button>
|
|
||||||
<Button variant="tertiary" onPress={() => setAutomationEditDraft(null)}>Abbrechen</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Neue Automation</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Name</Label>
|
|
||||||
<Input value={automationDraft.name} onChange={(e) => setAutomationDraft((s) => ({ ...s, name: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Kategorie / Zustand</Label>
|
|
||||||
<Input value={automationDraft.conditionValue} onChange={(e) => setAutomationDraft((s) => ({ ...s, conditionValue: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Aktion / Nachricht</Label>
|
|
||||||
<TextArea value={automationDraft.actionValue} onChange={(e) => setAutomationDraft((s) => ({ ...s, actionValue: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<Button variant="primary" onPress={saveAutomation}>Automation speichern</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ticketTab === 'kb' && (
|
|
||||||
<div className="mt-5 grid gap-5 xl:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Artikel ({kbArticles.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{kbArticles.length ? kbArticles.map((article) => (
|
|
||||||
<Card key={article.id}>
|
|
||||||
<CardContent className="flex flex-col gap-2 p-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="font-semibold text-sm truncate">{article.title || 'Artikel'}</div>
|
|
||||||
<Chip size="sm" variant="soft">{(article.keywords?.length || 0)} Keywords</Chip>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted line-clamp-2">{article.content || '-'}</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button size="sm" variant="tertiary" onPress={() => setKbEditDraft({ id: article.id, title: article.title || '', keywords: (article.keywords || []).join(', '), content: article.content || '' })}>
|
|
||||||
<Pencil size={14} /> Bearbeiten
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="danger-soft" onPress={() => deleteKbArticle(article.id)}>
|
|
||||||
<Trash2 size={14} /> Löschen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : <EmptyState message="Keine Artikel" icon={<FileText size={24} />} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{kbEditDraft ? (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Artikel bearbeiten</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Titel</Label>
|
|
||||||
<Input value={kbEditDraft.title} onChange={(e) => setKbEditDraft((s) => ({ ...s, title: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Keywords</Label>
|
|
||||||
<Input value={kbEditDraft.keywords} onChange={(e) => setKbEditDraft((s) => ({ ...s, keywords: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Inhalt</Label>
|
|
||||||
<TextArea rows={5} value={kbEditDraft.content} onChange={(e) => setKbEditDraft((s) => ({ ...s, content: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="primary" onPress={() => updateKbArticle(kbEditDraft.id)}>Aktualisieren</Button>
|
|
||||||
<Button variant="tertiary" onPress={() => setKbEditDraft(null)}>Abbrechen</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Neuer KB-Artikel</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Titel</Label>
|
|
||||||
<Input value={kbDraft.title} onChange={(e) => setKbDraft((s) => ({ ...s, title: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Keywords</Label>
|
|
||||||
<Input value={kbDraft.keywords} onChange={(e) => setKbDraft((s) => ({ ...s, keywords: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Inhalt</Label>
|
|
||||||
<TextArea rows={5} value={kbDraft.content} onChange={(e) => setKbDraft((s) => ({ ...s, content: e.target.value }))} />
|
|
||||||
</TextField>
|
|
||||||
<Button variant="primary" onPress={saveKbArticle}>Artikel speichern</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ticketTab === 'kategorien' && (
|
|
||||||
<div className="mt-5 grid gap-5 xl:grid-cols-[1fr_420px]">
|
|
||||||
<div>
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Konfigurierte Kategorien ({Object.keys(ticketTopics).length})</h3>
|
|
||||||
<p className="mb-3 text-xs text-muted">
|
|
||||||
Der Kategorie-Slug entspricht dem Ticket-Grund (z.B. <code>ban</code>, <code>help</code>, <code>feedback</code>, <code>other</code>). Ohne Konfiguration bleibt das bisherige Verhalten unverändert.
|
|
||||||
</p>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{Object.keys(ticketTopics).length ? Object.entries(ticketTopics).map(([topic, cfg]) => (
|
|
||||||
<Card key={topic}>
|
|
||||||
<CardContent className="flex items-center justify-between gap-3 p-4">
|
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
|
||||||
<div className="bg-accent-soft text-accent-soft-foreground flex size-9 shrink-0 items-center justify-center rounded-lg">
|
|
||||||
<Tag size={14} />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="truncate text-sm font-semibold">{topic}</div>
|
|
||||||
<div className="flex items-center gap-1 text-xs text-muted">
|
|
||||||
<Users size={12} />
|
|
||||||
{cfg.roleId ? roles.find((r) => r.id === cfg.roleId)?.name || cfg.roleId : 'Standard-Support-Rolle'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
|
||||||
<Chip size="sm" variant="soft">{cfg.questions?.length ?? 0} Fragen</Chip>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="tertiary"
|
|
||||||
onPress={() => setTicketTopicDraft({ topic, roleId: cfg.roleId || '', questions: (cfg.questions || []).join('\n') })}
|
|
||||||
>
|
|
||||||
<Pencil size={14} /> Bearbeiten
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : <EmptyState message="Noch keine Kategorien konfiguriert" icon={<Tag size={24} />} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="px-5 pt-5 pb-0">
|
|
||||||
<h3 className="text-base font-semibold">Kategorie {ticketTopicDraft.topic ? 'bearbeiten' : 'anlegen'}</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4 p-5">
|
|
||||||
<TextField>
|
|
||||||
<Label>Kategorie-Slug</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="z.B. help, ban, feedback, other"
|
|
||||||
value={ticketTopicDraft.topic}
|
|
||||||
onChange={(e) => setTicketTopicDraft((s: any) => ({ ...s, topic: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Rolle, die gepingt wird</Label>
|
|
||||||
<RoleSelect options={roles} value={ticketTopicDraft.roleId} onChange={(id) => setTicketTopicDraft((s: any) => ({ ...s, roleId: id }))} placeholder="Standard-Support-Rolle" />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Fragen-Vorlage (eine pro Zeile, max. 5)</Label>
|
|
||||||
<TextArea
|
|
||||||
rows={5}
|
|
||||||
placeholder={'Was ist passiert?\nWann ist es aufgetreten?\nHast du einen Screenshot?'}
|
|
||||||
value={ticketTopicDraft.questions}
|
|
||||||
onChange={(e) => setTicketTopicDraft((s: any) => ({ ...s, questions: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
<p className="text-xs text-muted">Wenn Fragen hinterlegt sind, öffnet sich beim Ticket-Erstellen ein Formular statt sofort ein Kanal zu erstellen.</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="primary" onPress={saveTicketTopic} isDisabled={!ticketTopicDraft.topic.trim()}>Speichern</Button>
|
|
||||||
{ticketTopicDraft.topic && (
|
|
||||||
<Button variant="tertiary" onPress={() => setTicketTopicDraft({ topic: '', roleId: '', questions: '' })}>Abbrechen</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Card, CardContent, Button } from '@heroui/react';
|
|
||||||
import { Eye, UserMinus } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { formatDate } from '../utils/formatters';
|
|
||||||
|
|
||||||
export function Watchlist() {
|
|
||||||
const { watchlistEntries, removeFromWatchlist } = useApp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Watchlist" subtitle="Beobachtete Nutzer. Papo meldet auffälliges Verhalten im Log-Kanal.">
|
|
||||||
<h3 className="mb-3 text-base font-semibold">Beobachtete Nutzer ({watchlistEntries.length})</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{watchlistEntries.length ? watchlistEntries.map((w) => (
|
|
||||||
<Card key={w.id}>
|
|
||||||
<CardContent className="flex items-center justify-between gap-3 p-4">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<div className="font-semibold truncate">User-ID {w.userId}</div>
|
|
||||||
<div className="text-xs text-muted truncate">{w.reason || 'Kein Grund angegeben'}</div>
|
|
||||||
<div className="text-xs text-muted">hinzugefügt von {w.addedBy} · {formatDate(w.addedAt)}</div>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" variant="danger-soft" onPress={() => removeFromWatchlist(w.userId)}>
|
|
||||||
<UserMinus size={14} /> Entfernen
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)) : (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-8 text-center text-sm text-muted">
|
|
||||||
<Eye size={24} />
|
|
||||||
Aktuell wird niemand beobachtet.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, TextArea, Button, Separator, TextField, Label } from '@heroui/react';
|
|
||||||
import { Sparkles, Save, X } from 'lucide-react';
|
|
||||||
import { useApp } from '../context/AppContext';
|
|
||||||
import { SectionCard } from '../components/shared/SectionCard';
|
|
||||||
import { DiscordPreview } from '../components/shared/DiscordPreview';
|
|
||||||
import { ChannelSelect } from '../components/shared/ChannelSelect';
|
|
||||||
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
|
|
||||||
import { useGuildResources } from '../hooks/useGuildResources';
|
|
||||||
|
|
||||||
const MAX_IMAGE_BYTES = 3 * 1024 * 1024;
|
|
||||||
|
|
||||||
export function Welcome() {
|
|
||||||
const { settings, setSettings, saveSettingsPayload, currentGuildId } = useApp();
|
|
||||||
const { channels } = useGuildResources(currentGuildId);
|
|
||||||
|
|
||||||
const imagePreview = settings.welcomeConfig?.embedImageData || settings.welcomeConfig?.embedImage;
|
|
||||||
|
|
||||||
const handleImageUpload = (file: File) => {
|
|
||||||
if (file.size > MAX_IMAGE_BYTES) {
|
|
||||||
alert('Bild ist zu groß (max. 3 MB).');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedImageData: reader.result as string, embedImage: undefined } }));
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearImage = () => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedImage: undefined, embedImageData: undefined } }));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionCard title="Willkommen" subtitle="Welcome-Embeds und Join-Nachrichten">
|
|
||||||
<div className="grid gap-5 xl:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<CardTitle>Welcome konfigurieren</CardTitle>
|
|
||||||
<CardDescription>Standardfelder fuer das Welcome-Embed.</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<ModuleActiveToggle
|
|
||||||
icon={<Sparkles size={16} />}
|
|
||||||
title="Welcome aktiv"
|
|
||||||
description="Sendet ein Begrüßungs-Embed, wenn neue Mitglieder beitreten."
|
|
||||||
isSelected={settings.welcomeConfig?.enabled !== false}
|
|
||||||
onChange={(v) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), enabled: v } }))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Channel</Label>
|
|
||||||
<ChannelSelect
|
|
||||||
options={channels}
|
|
||||||
value={settings.welcomeConfig?.channelId || settings.welcomeChannelId}
|
|
||||||
onChange={(id) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), channelId: id } }))}
|
|
||||||
placeholder="Channel für Willkommensnachrichten wählen"
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Titel</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Willkommen {user}!"
|
|
||||||
value={settings.welcomeConfig?.embedTitle || ''}
|
|
||||||
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedTitle: e.target.value } }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Beschreibung</Label>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Beschreibung des Embeds"
|
|
||||||
value={settings.welcomeConfig?.embedDescription || ''}
|
|
||||||
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedDescription: e.target.value } }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Footer</Label>
|
|
||||||
<Input
|
|
||||||
placeholder={new Date().getFullYear().toString()}
|
|
||||||
value={settings.welcomeConfig?.embedFooter || ''}
|
|
||||||
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedFooter: e.target.value } }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>Bild-URL</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="https://..."
|
|
||||||
value={settings.welcomeConfig?.embedImage || ''}
|
|
||||||
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedImage: e.target.value, embedImageData: undefined } }))}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
<TextField>
|
|
||||||
<Label>oder Bild hochladen</Label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
|
||||||
className="text-sm text-muted"
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) handleImageUpload(file);
|
|
||||||
e.target.value = '';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
|
|
||||||
{imagePreview && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<img src={imagePreview} alt="Vorschau" className="h-12 w-20 rounded object-cover" />
|
|
||||||
<Button size="sm" variant="danger-soft" onPress={clearImage}>
|
|
||||||
<X size={14} /> Bild entfernen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<Button size="lg" variant="primary" onPress={() => saveSettingsPayload({ welcomeConfig: settings.welcomeConfig || {} }, 'Welcome gespeichert')}>
|
|
||||||
<Save size={16} /> Speichern
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<div>
|
|
||||||
<CardTitle>Live Vorschau</CardTitle>
|
|
||||||
<CardDescription>So sieht die Nachricht auf Discord aus.</CardDescription>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<DiscordPreview
|
|
||||||
title={settings.welcomeConfig?.embedTitle || 'Willkommen!'}
|
|
||||||
description={settings.welcomeConfig?.embedDescription || 'Willkommen auf dem Server!'}
|
|
||||||
footer={settings.welcomeConfig?.embedFooter}
|
|
||||||
image={imagePreview}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-sm text-muted">
|
|
||||||
Nutze {'{user}'} fuer den Benutzernamen und {'{server}'} fuer den Servernamen.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</SectionCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
export type AppConfig = {
|
|
||||||
baseRoot?: string;
|
|
||||||
baseApi?: string;
|
|
||||||
baseAuth?: string;
|
|
||||||
baseDashboard?: string;
|
|
||||||
initialGuildId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type User = {
|
|
||||||
username: string;
|
|
||||||
discriminator?: string;
|
|
||||||
isAdmin?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Guild = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
icon?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NavKey =
|
|
||||||
| 'overview'
|
|
||||||
| 'tickets'
|
|
||||||
| 'supportlogin'
|
|
||||||
| 'automod'
|
|
||||||
| 'welcome'
|
|
||||||
| 'dynamicvoice'
|
|
||||||
| 'birthday'
|
|
||||||
| 'reactionroles'
|
|
||||||
| 'statuspage'
|
|
||||||
| 'serverstats'
|
|
||||||
| 'register'
|
|
||||||
| 'music'
|
|
||||||
| 'settings'
|
|
||||||
| 'modules'
|
|
||||||
| 'events'
|
|
||||||
| 'tasks'
|
|
||||||
| 'watchlist'
|
|
||||||
| 'branding'
|
|
||||||
| 'growth'
|
|
||||||
| 'permissions'
|
|
||||||
| 'panels'
|
|
||||||
| 'admin';
|
|
||||||
|
|
||||||
export type TicketRecord = {
|
|
||||||
id: string;
|
|
||||||
topic?: string;
|
|
||||||
status?: string;
|
|
||||||
category?: string;
|
|
||||||
priority?: string;
|
|
||||||
createdAt?: string;
|
|
||||||
claimedById?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StatusService = {
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
url?: string;
|
|
||||||
status?: string;
|
|
||||||
uptimePct?: number;
|
|
||||||
lastCheckedAt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EventItem = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
startsAt?: string;
|
|
||||||
reminderMinutes?: number;
|
|
||||||
channelId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ReactionRoleSet = {
|
|
||||||
id: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
channelId?: string;
|
|
||||||
messageId?: string;
|
|
||||||
entries?: Array<{ emoji: string; roleId: string; label?: string; description?: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ModuleItem = {
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
enabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LogEntry = {
|
|
||||||
level?: string;
|
|
||||||
category?: string;
|
|
||||||
message?: string;
|
|
||||||
timestamp?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SettingsState = Record<string, any>;
|
|
||||||
|
|
||||||
export type NavItem = {
|
|
||||||
key: NavKey;
|
|
||||||
label: string;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SupportLoginConfig = {
|
|
||||||
panelChannelId?: string;
|
|
||||||
panelMessageId?: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
loginLabel?: string;
|
|
||||||
logoutLabel?: string;
|
|
||||||
autoRefresh?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SupportLoginStatus = {
|
|
||||||
active: { userId: string; username?: string; loggedInAt?: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RegisterFormField = {
|
|
||||||
id?: string;
|
|
||||||
label: string;
|
|
||||||
type: 'text' | 'paragraph' | 'select' | 'multi';
|
|
||||||
required?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
options?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RegisterForm = {
|
|
||||||
id: string;
|
|
||||||
guildId: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
reviewChannelId?: string;
|
|
||||||
notifyRoleIds?: string[];
|
|
||||||
isActive: boolean;
|
|
||||||
fields: RegisterFormField[];
|
|
||||||
createdAt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RegisterApplication = {
|
|
||||||
id: string;
|
|
||||||
formId: string;
|
|
||||||
userId: string;
|
|
||||||
username?: string;
|
|
||||||
status: 'pending' | 'accepted' | 'invited' | 'rejected';
|
|
||||||
answers: { fieldId?: string; label?: string; value: string }[];
|
|
||||||
form?: RegisterForm;
|
|
||||||
createdAt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type StaffTask = {
|
|
||||||
id: string;
|
|
||||||
guildId: string;
|
|
||||||
title: string;
|
|
||||||
description?: string | null;
|
|
||||||
status: 'open' | 'in_progress' | 'done';
|
|
||||||
assigneeId?: string | null;
|
|
||||||
createdBy: string;
|
|
||||||
createdByTag: string;
|
|
||||||
createdAt?: string;
|
|
||||||
updatedAt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WatchlistEntry = {
|
|
||||||
id: string;
|
|
||||||
guildId: string;
|
|
||||||
userId: string;
|
|
||||||
reason?: string | null;
|
|
||||||
addedBy: string;
|
|
||||||
addedAt?: string;
|
|
||||||
active: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GrowthStats = {
|
|
||||||
joins7: number;
|
|
||||||
leaves7: number;
|
|
||||||
joins30: number;
|
|
||||||
leaves30: number;
|
|
||||||
bestInvite: { code: string; uses: number } | null;
|
|
||||||
partnerJoins30: number;
|
|
||||||
boosts: number;
|
|
||||||
dailyJoins: { day: string; count: number }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InviteBreakdownEntry = {
|
|
||||||
code: string;
|
|
||||||
inviterId?: string;
|
|
||||||
uses: number;
|
|
||||||
joins30: number;
|
|
||||||
suspiciousJoins30: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RecentJoinEntry = {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
inviterId?: string | null;
|
|
||||||
inviteCode?: string | null;
|
|
||||||
suspicious: boolean;
|
|
||||||
createdAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PermissionScanResult = {
|
|
||||||
adminRoles: { id: string; name: string }[];
|
|
||||||
banRoles: { id: string; name: string }[];
|
|
||||||
dangerousBots: { id: string; tag: string; perms: string[] }[];
|
|
||||||
publicChannels: { id: string; name: string }[];
|
|
||||||
everyoneCanSendChannels: { id: string; name: string }[];
|
|
||||||
emptyRoles: { id: string; name: string }[];
|
|
||||||
duplicateRoleGroups: { id: string; name: string }[][];
|
|
||||||
uselessRoles: { id: string; name: string }[];
|
|
||||||
tooManyAdmins: boolean;
|
|
||||||
adminMemberCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InfoPanelType = 'rules' | 'support' | 'bewerbung' | 'partner' | 'rollen' | 'events' | 'faq';
|
|
||||||
|
|
||||||
export type InfoPanel = {
|
|
||||||
id: string;
|
|
||||||
guildId: string;
|
|
||||||
channelId: string;
|
|
||||||
messageId?: string | null;
|
|
||||||
type: InfoPanelType;
|
|
||||||
title: string;
|
|
||||||
description?: string | null;
|
|
||||||
items?: { question: string; answer: string }[] | null;
|
|
||||||
createdAt?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TicketTopicConfig = {
|
|
||||||
roleId?: string;
|
|
||||||
questions?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MusicSession = {
|
|
||||||
guildId: string;
|
|
||||||
nowPlaying?: { title: string; url: string } | null;
|
|
||||||
queueLength: number;
|
|
||||||
loop: 'off' | 'song' | 'queue';
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { AppConfig } from '../types';
|
|
||||||
|
|
||||||
const config: AppConfig = (window as any).__PAPO__ || {};
|
|
||||||
|
|
||||||
export function apiUrl(path: string) {
|
|
||||||
const base = config.baseApi || '/api';
|
|
||||||
return `${base}${path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
||||||
const response = await fetch(apiUrl(path), {
|
|
||||||
...init,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(init?.headers || {})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
window.location.href = `${config.baseAuth || '/auth'}/discord`;
|
|
||||||
throw new Error('unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`request failed: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json() as Promise<T>;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Activity, AudioLines, CalendarDays, ClipboardList, Home,
|
|
||||||
LogIn, Music, Puzzle, RadioTower, Settings, Shield, Sparkles,
|
|
||||||
Tag, Ticket, Wrench
|
|
||||||
} from 'lucide-react';
|
|
||||||
import type { NavItem } from '../types';
|
|
||||||
|
|
||||||
export const navItems: NavItem[] = [
|
|
||||||
{ key: 'overview', label: 'Übersicht', icon: <Home size={20} /> },
|
|
||||||
{ key: 'tickets', label: 'Ticketsystem', icon: <Ticket size={20} /> },
|
|
||||||
{ key: 'supportlogin', label: 'Support Login', icon: <LogIn size={20} /> },
|
|
||||||
{ key: 'automod', label: 'Automod', icon: <Shield size={20} /> },
|
|
||||||
{ key: 'welcome', label: 'Willkommen', icon: <Sparkles size={20} /> },
|
|
||||||
{ key: 'dynamicvoice', label: 'Dynamic Voice', icon: <AudioLines size={20} /> },
|
|
||||||
{ key: 'birthday', label: 'Birthday', icon: <CalendarDays size={20} /> },
|
|
||||||
{ key: 'reactionroles', label: 'Reaction Roles', icon: <Tag size={20} /> },
|
|
||||||
{ key: 'statuspage', label: 'Statuspage', icon: <RadioTower size={20} /> },
|
|
||||||
{ key: 'serverstats', label: 'Server Stats', icon: <Activity size={20} /> },
|
|
||||||
{ key: 'register', label: 'Registrierung', icon: <ClipboardList size={20} /> },
|
|
||||||
{ key: 'music', label: 'Musik', icon: <Music size={20} /> },
|
|
||||||
{ key: 'settings', label: 'Einstellungen', icon: <Settings size={20} /> },
|
|
||||||
{ key: 'modules', label: 'Module', icon: <Puzzle size={20} /> },
|
|
||||||
{ key: 'events', label: 'Events', icon: <CalendarDays size={20} /> },
|
|
||||||
{ key: 'admin', label: 'Admin', icon: <Wrench size={20} /> }
|
|
||||||
];
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { Guild } from '../types';
|
|
||||||
|
|
||||||
export function formatDate(value?: string | number | null) {
|
|
||||||
if (!value) return '-';
|
|
||||||
const date = new Date(value);
|
|
||||||
if (Number.isNaN(date.getTime())) return String(value);
|
|
||||||
return `${date.toLocaleDateString('de-DE')} ${date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDuration(ms?: number | null) {
|
|
||||||
if (!ms || ms < 0) return '-';
|
|
||||||
const totalMinutes = Math.floor(ms / 60000);
|
|
||||||
const days = Math.floor(totalMinutes / (60 * 24));
|
|
||||||
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
|
|
||||||
const minutes = totalMinutes % 60;
|
|
||||||
const parts: string[] = [];
|
|
||||||
if (days) parts.push(`${days}d`);
|
|
||||||
if (hours) parts.push(`${hours}h`);
|
|
||||||
if (minutes || !parts.length) parts.push(`${minutes}m`);
|
|
||||||
return parts.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function guildIconUrl(guild?: Guild | null) {
|
|
||||||
if (!guild) return undefined;
|
|
||||||
if (guild.icon) return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png`;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"allowJs": false,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
base: './',
|
|
||||||
plugins: [react(), tailwindcss()]
|
|
||||||
});
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -Eeuo pipefail
|
|
||||||
|
|
||||||
RUNNER_VERSION="${RUNNER_VERSION:-2.0.0}"
|
|
||||||
RUNNER_USER="${RUNNER_USER:-act_runner}"
|
|
||||||
RUNNER_HOME="${RUNNER_HOME:-/var/lib/act_runner}"
|
|
||||||
RUNNER_CONFIG_DIR="${RUNNER_CONFIG_DIR:-/etc/act_runner}"
|
|
||||||
RUNNER_CONFIG_FILE="${RUNNER_CONFIG_FILE:-$RUNNER_CONFIG_DIR/config.yaml}"
|
|
||||||
RUNNER_BINARY_PATH="${RUNNER_BINARY_PATH:-/usr/local/bin/gitea-runner}"
|
|
||||||
RUNNER_COMPAT_SYMLINK_PATH="${RUNNER_COMPAT_SYMLINK_PATH:-/usr/local/bin/act_runner}"
|
|
||||||
RUNNER_NAME="${RUNNER_NAME:-$(hostname)}"
|
|
||||||
RUNNER_LABELS="${RUNNER_LABELS:-linux_amd64:host,ubuntu-latest:docker://node:20-bookworm}"
|
|
||||||
GITEA_INSTANCE_URL="${GITEA_INSTANCE_URL:-}"
|
|
||||||
GITEA_RUNNER_TOKEN="${GITEA_RUNNER_TOKEN:-}"
|
|
||||||
REGISTER_RUNNER="${REGISTER_RUNNER:-true}"
|
|
||||||
|
|
||||||
detect_runner_arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64)
|
|
||||||
echo "amd64"
|
|
||||||
;;
|
|
||||||
aarch64|arm64)
|
|
||||||
echo "arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Nicht unterstützte Architektur: $(uname -m)" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[RUNNER] %s\n' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
require_root() {
|
|
||||||
if [ "${EUID}" -ne 0 ]; then
|
|
||||||
echo "Dieses Script muss als root laufen." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_packages() {
|
|
||||||
log "Installiere Systempakete"
|
|
||||||
apt update
|
|
||||||
apt install -y docker.io curl wget unzip git ca-certificates python3
|
|
||||||
systemctl enable --now docker
|
|
||||||
}
|
|
||||||
|
|
||||||
create_runner_user() {
|
|
||||||
log "Erstelle Runner-User und Verzeichnisse"
|
|
||||||
|
|
||||||
if ! id -u "$RUNNER_USER" >/dev/null 2>&1; then
|
|
||||||
useradd --system --create-home --shell /bin/bash "$RUNNER_USER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
usermod -aG docker "$RUNNER_USER"
|
|
||||||
mkdir -p "$RUNNER_HOME" "$RUNNER_CONFIG_DIR"
|
|
||||||
chown -R "$RUNNER_USER:$RUNNER_USER" "$RUNNER_HOME" "$RUNNER_CONFIG_DIR"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_runner_binary() {
|
|
||||||
local tmp_dir runner_arch asset_name download_url
|
|
||||||
|
|
||||||
log "Installiere act_runner ${RUNNER_VERSION}"
|
|
||||||
tmp_dir="$(mktemp -d)"
|
|
||||||
runner_arch="$(detect_runner_arch)"
|
|
||||||
asset_name="gitea-runner-${RUNNER_VERSION}-linux-${runner_arch}"
|
|
||||||
download_url="https://dl.gitea.com/gitea-runner/${RUNNER_VERSION}/${asset_name}"
|
|
||||||
|
|
||||||
curl -fsSL "$download_url" -o "${tmp_dir}/gitea-runner"
|
|
||||||
install -m 0755 "${tmp_dir}/gitea-runner" "$RUNNER_BINARY_PATH"
|
|
||||||
ln -sf "$RUNNER_BINARY_PATH" "$RUNNER_COMPAT_SYMLINK_PATH"
|
|
||||||
rm -rf "$tmp_dir"
|
|
||||||
|
|
||||||
"$RUNNER_BINARY_PATH" --version
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_config() {
|
|
||||||
log "Erzeuge Runner-Konfiguration"
|
|
||||||
sudo -u "$RUNNER_USER" -H bash -lc "\"$RUNNER_BINARY_PATH\" generate-config > \"$RUNNER_CONFIG_FILE\""
|
|
||||||
|
|
||||||
python3 - "$RUNNER_CONFIG_FILE" "$RUNNER_LABELS" <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
config_path = Path(sys.argv[1])
|
|
||||||
labels = [label.strip() for label in sys.argv[2].split(",") if label.strip()]
|
|
||||||
content = config_path.read_text(encoding="utf-8")
|
|
||||||
lines = content.splitlines()
|
|
||||||
out = []
|
|
||||||
in_runner = False
|
|
||||||
labels_written = False
|
|
||||||
skip_existing_labels = False
|
|
||||||
|
|
||||||
for idx, line in enumerate(lines):
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped == "runner:":
|
|
||||||
in_runner = True
|
|
||||||
out.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if in_runner and line and not line.startswith(" "):
|
|
||||||
if not labels_written:
|
|
||||||
out.append(" labels:")
|
|
||||||
for label in labels:
|
|
||||||
out.append(f" - \"{label}\"")
|
|
||||||
labels_written = True
|
|
||||||
in_runner = False
|
|
||||||
|
|
||||||
if in_runner and stripped.startswith("labels:"):
|
|
||||||
skip_existing_labels = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
if skip_existing_labels:
|
|
||||||
if line.startswith(" - ") or stripped == "":
|
|
||||||
continue
|
|
||||||
skip_existing_labels = False
|
|
||||||
|
|
||||||
out.append(line)
|
|
||||||
|
|
||||||
if in_runner and not labels_written:
|
|
||||||
out.append(" labels:")
|
|
||||||
for label in labels:
|
|
||||||
out.append(f" - \"{label}\"")
|
|
||||||
|
|
||||||
config_path.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
||||||
PY
|
|
||||||
|
|
||||||
chown "$RUNNER_USER:$RUNNER_USER" "$RUNNER_CONFIG_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
register_runner() {
|
|
||||||
if [ "$REGISTER_RUNNER" != "true" ]; then
|
|
||||||
log "Runner-Registrierung übersprungen"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$GITEA_INSTANCE_URL" ] || [ -z "$GITEA_RUNNER_TOKEN" ]; then
|
|
||||||
echo "Für die Registrierung werden GITEA_INSTANCE_URL und GITEA_RUNNER_TOKEN benötigt." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Registriere Runner bei ${GITEA_INSTANCE_URL}"
|
|
||||||
sudo -u "$RUNNER_USER" -H bash -lc "cd \"$RUNNER_HOME\" && rm -f .runner && \"$RUNNER_BINARY_PATH\" register --no-interactive --instance \"$GITEA_INSTANCE_URL\" --token \"$GITEA_RUNNER_TOKEN\" --name \"$RUNNER_NAME\" --labels \"$RUNNER_LABELS\""
|
|
||||||
}
|
|
||||||
|
|
||||||
install_service() {
|
|
||||||
log "Installiere systemd-Service"
|
|
||||||
cat >/etc/systemd/system/act_runner.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=Gitea Actions runner
|
|
||||||
Documentation=https://docs.gitea.com/usage/actions/act-runner
|
|
||||||
After=docker.service
|
|
||||||
Requires=docker.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=${RUNNER_USER}
|
|
||||||
WorkingDirectory=${RUNNER_HOME}
|
|
||||||
ExecStart=${RUNNER_BINARY_PATH} daemon --config ${RUNNER_CONFIG_FILE}
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable --now act_runner
|
|
||||||
}
|
|
||||||
|
|
||||||
print_summary() {
|
|
||||||
log "Fertig"
|
|
||||||
echo
|
|
||||||
echo "Service-Status:"
|
|
||||||
systemctl --no-pager --full status act_runner || true
|
|
||||||
echo
|
|
||||||
echo "Wichtige Pfade:"
|
|
||||||
echo " Binary: ${RUNNER_BINARY_PATH}"
|
|
||||||
echo " Symlink: ${RUNNER_COMPAT_SYMLINK_PATH}"
|
|
||||||
echo " Config: ${RUNNER_CONFIG_FILE}"
|
|
||||||
echo " Home: ${RUNNER_HOME}"
|
|
||||||
echo
|
|
||||||
echo "Beispiel mit direkter Registrierung:"
|
|
||||||
echo " sudo GITEA_INSTANCE_URL=https://gitea.example.tld GITEA_RUNNER_TOKEN=TOKEN ./install-gitea-runner.sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
require_root
|
|
||||||
install_packages
|
|
||||||
create_runner_user
|
|
||||||
install_runner_binary
|
|
||||||
generate_config
|
|
||||||
register_runner
|
|
||||||
install_service
|
|
||||||
print_summary
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
17
node_modules/.bin/acorn
generated
vendored
17
node_modules/.bin/acorn
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../acorn/bin/acorn
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../acorn/bin/acorn" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../acorn/bin/acorn" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/mime
generated
vendored
17
node_modules/.bin/mime
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../mime/cli.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../mime/cli.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../mime/cli.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/mkdirp
generated
vendored
17
node_modules/.bin/mkdirp
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../mkdirp/bin/cmd.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../mkdirp/bin/cmd.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/prisma
generated
vendored
17
node_modules/.bin/prisma
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../prisma/build/index.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../prisma/build/index.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../prisma/build/index.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/resolve
generated
vendored
17
node_modules/.bin/resolve
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../resolve/bin/resolve
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../resolve/bin/resolve" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../resolve/bin/resolve" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/rimraf
generated
vendored
17
node_modules/.bin/rimraf
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../rimraf/bin.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../rimraf/bin.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../rimraf/bin.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/tree-kill
generated
vendored
17
node_modules/.bin/tree-kill
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../tree-kill/cli.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../tree-kill/cli.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../tree-kill/cli.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/ts-node
generated
vendored
17
node_modules/.bin/ts-node
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../ts-node/dist/bin.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../ts-node/dist/bin.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../ts-node/dist/bin.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/ts-node-cwd
generated
vendored
17
node_modules/.bin/ts-node-cwd
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../ts-node/dist/bin-cwd.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../ts-node/dist/bin-cwd.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../ts-node/dist/bin-cwd.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/ts-node-dev
generated
vendored
17
node_modules/.bin/ts-node-dev
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../ts-node-dev/lib/bin.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../ts-node-dev/lib/bin.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../ts-node-dev/lib/bin.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/ts-node-esm
generated
vendored
17
node_modules/.bin/ts-node-esm
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../ts-node/dist/bin-esm.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../ts-node/dist/bin-esm.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../ts-node/dist/bin-esm.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/ts-node-script
generated
vendored
17
node_modules/.bin/ts-node-script
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../ts-node/dist/bin-script.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../ts-node/dist/bin-script.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../ts-node/dist/bin-script.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/ts-node-transpile-only
generated
vendored
17
node_modules/.bin/ts-node-transpile-only
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../ts-node/dist/bin-transpile.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../ts-node/dist/bin-transpile.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../ts-node/dist/bin-transpile.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/ts-script
generated
vendored
17
node_modules/.bin/ts-script
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../ts-node/dist/bin-script-deprecated.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../ts-node/dist/bin-script-deprecated.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../ts-node/dist/bin-script-deprecated.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/tsc
generated
vendored
17
node_modules/.bin/tsc
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../typescript/bin/tsc
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../typescript/bin/tsc" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/tsnd
generated
vendored
17
node_modules/.bin/tsnd
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../ts-node-dev/lib/bin.js
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../ts-node-dev/lib/bin.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../ts-node-dev/lib/bin.js" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
17
node_modules/.bin/tsserver
generated
vendored
17
node_modules/.bin/tsserver
generated
vendored
@@ -1 +1,16 @@
|
|||||||
../typescript/bin/tsserver
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
||||||
|
fi
|
||||||
|
|||||||
823
node_modules/.package-lock.json
generated
vendored
823
node_modules/.package-lock.json
generated
vendored
@@ -4,16 +4,6 @@
|
|||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"node_modules/@borewit/text-codec": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
|
|
||||||
"integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -274,430 +264,6 @@
|
|||||||
"scripts/actions/documentation"
|
"scripts/actions/documentation"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@jimp/core": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/file-ops": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"await-to-js": "^3.0.0",
|
|
||||||
"exif-parser": "^0.1.12",
|
|
||||||
"file-type": "^21.3.3",
|
|
||||||
"mime": "3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/core/node_modules/mime": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"mime": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/diff": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"pixelmatch": "^5.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/file-ops": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-bmp": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"bmp-ts": "^1.0.9"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-gif": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"gifwrap": "^0.10.1",
|
|
||||||
"omggif": "^1.0.10"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-jpeg": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"jpeg-js": "^0.4.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-png": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"pngjs": "^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-tiff": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"utif2": "^4.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-blit": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-blur": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-circle": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-color": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"tinycolor2": "^1.6.0",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-contain": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/plugin-blit": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-cover": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/plugin-crop": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-crop": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-displace": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-dither": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-fisheye": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-flip": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-hash": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/js-bmp": "1.6.1",
|
|
||||||
"@jimp/js-jpeg": "1.6.1",
|
|
||||||
"@jimp/js-png": "1.6.1",
|
|
||||||
"@jimp/js-tiff": "1.6.1",
|
|
||||||
"@jimp/plugin-color": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"any-base": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-mask": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-print": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/js-jpeg": "1.6.1",
|
|
||||||
"@jimp/js-png": "1.6.1",
|
|
||||||
"@jimp/plugin-blit": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"parse-bmfont-ascii": "^1.0.6",
|
|
||||||
"parse-bmfont-binary": "^1.0.6",
|
|
||||||
"parse-bmfont-xml": "^1.1.6",
|
|
||||||
"simple-xml-to-json": "^1.2.2",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-quantize": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"image-q": "^4.0.0",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-resize": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-rotate": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/plugin-crop": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-threshold": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/plugin-color": "1.6.1",
|
|
||||||
"@jimp/plugin-hash": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/types": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/utils": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"tinycolor2": "^1.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
@@ -855,84 +421,22 @@
|
|||||||
"@snazzah/davey-win32-x64-msvc": "0.1.8"
|
"@snazzah/davey-win32-x64-msvc": "0.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@snazzah/davey-linux-x64-gnu": {
|
"node_modules/@snazzah/davey-win32-x64-msvc": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@snazzah/davey-linux-x64-gnu/-/davey-linux-x64-gnu-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@snazzah/davey-win32-x64-msvc/-/davey-win32-x64-msvc-0.1.8.tgz",
|
||||||
"integrity": "sha512-yghgG7iXZUHy734Cq3PcgrbRnLhhB233JNTX5VPRxRqdwFAg2MzAJ2iSWpP12K6hSqKq9hw0sdt8CNpr0mEXjQ==",
|
"integrity": "sha512-JKIco1miwtM4NgVwU/H9TJdUaSlJ+kdtydy3+tiV9cmJv0u1SM2NpwjV85H44xAQWW2zcRBHP0ZDriciHw09qQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"win32"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@snazzah/davey-linux-x64-musl": {
|
|
||||||
"version": "0.1.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@snazzah/davey-linux-x64-musl/-/davey-linux-x64-musl-0.1.8.tgz",
|
|
||||||
"integrity": "sha512-WwCiAge27ZOEu7NRx5NFjpCAkGcUGHGtoROBY4ElRYE0Tp3DfuzWU06qSu6JBQPlzhTTBN29X2/kGP8iRUwqnQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tokenizer/inflate": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "^4.4.3",
|
|
||||||
"token-types": "^6.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tokenizer/inflate/node_modules/debug": {
|
|
||||||
"version": "4.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tokenizer/inflate/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@tokenizer/token": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
@@ -1220,12 +724,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/any-base": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
@@ -1273,15 +771,6 @@
|
|||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/await-to-js": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -1342,12 +831,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bmp-ts": {
|
|
||||||
"version": "1.0.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz",
|
|
||||||
"integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
@@ -1796,11 +1279,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exif-parser": {
|
|
||||||
"version": "0.1.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
|
|
||||||
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="
|
|
||||||
},
|
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
@@ -1903,24 +1381,6 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/file-type": {
|
|
||||||
"version": "21.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz",
|
|
||||||
"integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@tokenizer/inflate": "^0.4.1",
|
|
||||||
"strtok3": "^10.3.4",
|
|
||||||
"token-types": "^6.1.1",
|
|
||||||
"uint8array-extras": "^1.4.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -2067,16 +1527,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gifwrap": {
|
|
||||||
"version": "0.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz",
|
|
||||||
"integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"image-q": "^4.0.0",
|
|
||||||
"omggif": "^1.0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
@@ -2232,41 +1682,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ieee754": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/image-q": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "16.9.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/image-q/node_modules/@types/node": {
|
|
||||||
"version": "16.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
|
|
||||||
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
@@ -2364,56 +1779,6 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jimp": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/diff": "1.6.1",
|
|
||||||
"@jimp/js-bmp": "1.6.1",
|
|
||||||
"@jimp/js-gif": "1.6.1",
|
|
||||||
"@jimp/js-jpeg": "1.6.1",
|
|
||||||
"@jimp/js-png": "1.6.1",
|
|
||||||
"@jimp/js-tiff": "1.6.1",
|
|
||||||
"@jimp/plugin-blit": "1.6.1",
|
|
||||||
"@jimp/plugin-blur": "1.6.1",
|
|
||||||
"@jimp/plugin-circle": "1.6.1",
|
|
||||||
"@jimp/plugin-color": "1.6.1",
|
|
||||||
"@jimp/plugin-contain": "1.6.1",
|
|
||||||
"@jimp/plugin-cover": "1.6.1",
|
|
||||||
"@jimp/plugin-crop": "1.6.1",
|
|
||||||
"@jimp/plugin-displace": "1.6.1",
|
|
||||||
"@jimp/plugin-dither": "1.6.1",
|
|
||||||
"@jimp/plugin-fisheye": "1.6.1",
|
|
||||||
"@jimp/plugin-flip": "1.6.1",
|
|
||||||
"@jimp/plugin-hash": "1.6.1",
|
|
||||||
"@jimp/plugin-mask": "1.6.1",
|
|
||||||
"@jimp/plugin-print": "1.6.1",
|
|
||||||
"@jimp/plugin-quantize": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/plugin-rotate": "1.6.1",
|
|
||||||
"@jimp/plugin-threshold": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jpeg-js": {
|
|
||||||
"version": "0.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
|
||||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/jsqr": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/libsodium": {
|
"node_modules/libsodium": {
|
||||||
"version": "0.7.15",
|
"version": "0.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz",
|
||||||
@@ -2715,12 +2080,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/omggif": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@@ -2751,34 +2110,6 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pako": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
|
||||||
"license": "(MIT AND Zlib)"
|
|
||||||
},
|
|
||||||
"node_modules/parse-bmfont-ascii": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/parse-bmfont-binary": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/parse-bmfont-xml": {
|
|
||||||
"version": "1.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz",
|
|
||||||
"integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"xml-parse-from-string": "^1.0.0",
|
|
||||||
"xml2js": "^0.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parse-cache-control": {
|
"node_modules/parse-cache-control": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
||||||
@@ -2828,27 +2159,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pixelmatch": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"pngjs": "^6.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"pixelmatch": "bin/pixelmatch"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pixelmatch/node_modules/pngjs": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/play-audio": {
|
"node_modules/play-audio": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz",
|
||||||
@@ -2867,15 +2177,6 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pngjs": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prism-media": {
|
"node_modules/prism-media": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
|
||||||
@@ -3092,15 +2393,6 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sax": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
@@ -3257,15 +2549,6 @@
|
|||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/simple-xml-to-json": {
|
|
||||||
"version": "1.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz",
|
|
||||||
"integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.12.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sodium-native": {
|
"node_modules/sodium-native": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
|
||||||
@@ -3360,22 +2643,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strtok3": {
|
|
||||||
"version": "10.3.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
|
|
||||||
"integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@tokenizer/token": "^0.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/supports-preserve-symlinks-flag": {
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
@@ -3406,12 +2673,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinycolor2": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -3434,24 +2695,6 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/token-types": {
|
|
||||||
"version": "6.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
|
|
||||||
"integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@borewit/text-codec": "^0.2.1",
|
|
||||||
"@tokenizer/token": "^0.3.0",
|
|
||||||
"ieee754": "^1.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tr46": {
|
"node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
@@ -3617,18 +2860,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uint8array-extras": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.21.3",
|
"version": "6.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||||
@@ -3653,15 +2884,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/utif2": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pako": "^1.0.11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -3745,34 +2967,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/xml-parse-from-string": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/xml2js": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"sax": ">=0.6.0",
|
|
||||||
"xmlbuilder": "~11.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xmlbuilder": {
|
|
||||||
"version": "11.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
|
||||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
@@ -3798,15 +2992,6 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/zod": {
|
|
||||||
"version": "3.25.76",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
301
node_modules/.prisma/client/edge.js
generated
vendored
301
node_modules/.prisma/client/edge.js
generated
vendored
File diff suppressed because one or more lines are too long
286
node_modules/.prisma/client/index-browser.js
generated
vendored
286
node_modules/.prisma/client/index-browser.js
generated
vendored
@@ -141,18 +141,6 @@ exports.Prisma.GuildSettingsScalarFieldEnum = {
|
|||||||
reactionRolesEnabled: 'reactionRolesEnabled',
|
reactionRolesEnabled: 'reactionRolesEnabled',
|
||||||
reactionRolesConfig: 'reactionRolesConfig',
|
reactionRolesConfig: 'reactionRolesConfig',
|
||||||
eventsEnabled: 'eventsEnabled',
|
eventsEnabled: 'eventsEnabled',
|
||||||
registerEnabled: 'registerEnabled',
|
|
||||||
registerConfig: 'registerConfig',
|
|
||||||
serverStatsEnabled: 'serverStatsEnabled',
|
|
||||||
serverStatsConfig: 'serverStatsConfig',
|
|
||||||
lockdownConfig: 'lockdownConfig',
|
|
||||||
tasksEnabled: 'tasksEnabled',
|
|
||||||
badgesEnabled: 'badgesEnabled',
|
|
||||||
brandingConfig: 'brandingConfig',
|
|
||||||
partnerConfig: 'partnerConfig',
|
|
||||||
galleryConfig: 'galleryConfig',
|
|
||||||
imageModerationConfig: 'imageModerationConfig',
|
|
||||||
ticketConfig: 'ticketConfig',
|
|
||||||
supportRoleId: 'supportRoleId',
|
supportRoleId: 'supportRoleId',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdAt: 'createdAt'
|
createdAt: 'createdAt'
|
||||||
@@ -169,30 +157,6 @@ exports.Prisma.TicketScalarFieldEnum = {
|
|||||||
status: 'status',
|
status: 'status',
|
||||||
claimedBy: 'claimedBy',
|
claimedBy: 'claimedBy',
|
||||||
transcript: 'transcript',
|
transcript: 'transcript',
|
||||||
firstClaimAt: 'firstClaimAt',
|
|
||||||
firstResponseAt: 'firstResponseAt',
|
|
||||||
kbSuggestionSentAt: 'kbSuggestionSentAt',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.TicketAutomationRuleScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
name: 'name',
|
|
||||||
condition: 'condition',
|
|
||||||
action: 'action',
|
|
||||||
active: 'active',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.KnowledgeBaseArticleScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
title: 'title',
|
|
||||||
keywords: 'keywords',
|
|
||||||
content: 'content',
|
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
@@ -264,230 +228,6 @@ exports.Prisma.EventSignupScalarFieldEnum = {
|
|||||||
canceledAt: 'canceledAt'
|
canceledAt: 'canceledAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.RegisterFormScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
name: 'name',
|
|
||||||
description: 'description',
|
|
||||||
reviewChannelId: 'reviewChannelId',
|
|
||||||
notifyRoleIds: 'notifyRoleIds',
|
|
||||||
isActive: 'isActive',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.RegisterFormFieldScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
formId: 'formId',
|
|
||||||
label: 'label',
|
|
||||||
type: 'type',
|
|
||||||
required: 'required',
|
|
||||||
order: 'order'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.RegisterApplicationScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
formId: 'formId',
|
|
||||||
status: 'status',
|
|
||||||
reviewedBy: 'reviewedBy',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.RegisterApplicationAnswerScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
applicationId: 'applicationId',
|
|
||||||
fieldId: 'fieldId',
|
|
||||||
value: 'value'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.RegisterApplicationNoteScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
applicationId: 'applicationId',
|
|
||||||
authorId: 'authorId',
|
|
||||||
authorTag: 'authorTag',
|
|
||||||
body: 'body',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.AutomodStrikeScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
filterKey: 'filterKey',
|
|
||||||
weight: 'weight',
|
|
||||||
reason: 'reason',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.StaffTaskScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
title: 'title',
|
|
||||||
description: 'description',
|
|
||||||
status: 'status',
|
|
||||||
assigneeId: 'assigneeId',
|
|
||||||
createdBy: 'createdBy',
|
|
||||||
createdByTag: 'createdByTag',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.ModCaseScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
type: 'type',
|
|
||||||
reason: 'reason',
|
|
||||||
moderatorId: 'moderatorId',
|
|
||||||
moderatorTag: 'moderatorTag',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.WatchlistScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
reason: 'reason',
|
|
||||||
addedBy: 'addedBy',
|
|
||||||
addedAt: 'addedAt',
|
|
||||||
active: 'active'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.UserBadgeScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
badgeKey: 'badgeKey',
|
|
||||||
awardedAt: 'awardedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.UserActivityScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
messageCount: 'messageCount',
|
|
||||||
activeDays: 'activeDays',
|
|
||||||
lastActiveDay: 'lastActiveDay'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.WeeklyPlanScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
title: 'title',
|
|
||||||
entries: 'entries',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.WeeklyPlanRsvpScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
planId: 'planId',
|
|
||||||
entryId: 'entryId',
|
|
||||||
userId: 'userId',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.PollScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
question: 'question',
|
|
||||||
options: 'options',
|
|
||||||
anonymous: 'anonymous',
|
|
||||||
closesAt: 'closesAt',
|
|
||||||
closed: 'closed',
|
|
||||||
createdBy: 'createdBy',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.PollVoteScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
pollId: 'pollId',
|
|
||||||
userId: 'userId',
|
|
||||||
optionId: 'optionId',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.SuggestionScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
userId: 'userId',
|
|
||||||
userTag: 'userTag',
|
|
||||||
content: 'content',
|
|
||||||
status: 'status',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.SuggestionVoteScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
suggestionId: 'suggestionId',
|
|
||||||
userId: 'userId',
|
|
||||||
value: 'value'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.PartnerRequestScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
userTag: 'userTag',
|
|
||||||
serverName: 'serverName',
|
|
||||||
inviteCode: 'inviteCode',
|
|
||||||
memberCount: 'memberCount',
|
|
||||||
description: 'description',
|
|
||||||
status: 'status',
|
|
||||||
reviewedBy: 'reviewedBy',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.GalleryPostScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
userId: 'userId',
|
|
||||||
userTag: 'userTag',
|
|
||||||
imageUrl: 'imageUrl',
|
|
||||||
caption: 'caption',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.GalleryVoteScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
postId: 'postId',
|
|
||||||
userId: 'userId'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.GuildGrowthEventScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
type: 'type',
|
|
||||||
userId: 'userId',
|
|
||||||
inviteCode: 'inviteCode',
|
|
||||||
inviterId: 'inviterId',
|
|
||||||
suspicious: 'suspicious',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.InfoPanelScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
type: 'type',
|
|
||||||
title: 'title',
|
|
||||||
description: 'description',
|
|
||||||
items: 'items',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.SortOrder = {
|
exports.Prisma.SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -522,36 +262,12 @@ exports.Prisma.NullsOrder = {
|
|||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
GuildSettings: 'GuildSettings',
|
GuildSettings: 'GuildSettings',
|
||||||
Ticket: 'Ticket',
|
Ticket: 'Ticket',
|
||||||
TicketAutomationRule: 'TicketAutomationRule',
|
|
||||||
KnowledgeBaseArticle: 'KnowledgeBaseArticle',
|
|
||||||
Level: 'Level',
|
Level: 'Level',
|
||||||
TicketSupportSession: 'TicketSupportSession',
|
TicketSupportSession: 'TicketSupportSession',
|
||||||
Birthday: 'Birthday',
|
Birthday: 'Birthday',
|
||||||
ReactionRoleSet: 'ReactionRoleSet',
|
ReactionRoleSet: 'ReactionRoleSet',
|
||||||
Event: 'Event',
|
Event: 'Event',
|
||||||
EventSignup: 'EventSignup',
|
EventSignup: 'EventSignup'
|
||||||
RegisterForm: 'RegisterForm',
|
|
||||||
RegisterFormField: 'RegisterFormField',
|
|
||||||
RegisterApplication: 'RegisterApplication',
|
|
||||||
RegisterApplicationAnswer: 'RegisterApplicationAnswer',
|
|
||||||
RegisterApplicationNote: 'RegisterApplicationNote',
|
|
||||||
AutomodStrike: 'AutomodStrike',
|
|
||||||
StaffTask: 'StaffTask',
|
|
||||||
ModCase: 'ModCase',
|
|
||||||
Watchlist: 'Watchlist',
|
|
||||||
UserBadge: 'UserBadge',
|
|
||||||
UserActivity: 'UserActivity',
|
|
||||||
WeeklyPlan: 'WeeklyPlan',
|
|
||||||
WeeklyPlanRsvp: 'WeeklyPlanRsvp',
|
|
||||||
Poll: 'Poll',
|
|
||||||
PollVote: 'PollVote',
|
|
||||||
Suggestion: 'Suggestion',
|
|
||||||
SuggestionVote: 'SuggestionVote',
|
|
||||||
PartnerRequest: 'PartnerRequest',
|
|
||||||
GalleryPost: 'GalleryPost',
|
|
||||||
GalleryVote: 'GalleryVote',
|
|
||||||
GuildGrowthEvent: 'GuildGrowthEvent',
|
|
||||||
InfoPanel: 'InfoPanel'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
31568
node_modules/.prisma/client/index.d.ts
generated
vendored
31568
node_modules/.prisma/client/index.d.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
305
node_modules/.prisma/client/index.js
generated
vendored
305
node_modules/.prisma/client/index.js
generated
vendored
File diff suppressed because one or more lines are too long
286
node_modules/.prisma/client/wasm.js
generated
vendored
286
node_modules/.prisma/client/wasm.js
generated
vendored
@@ -141,18 +141,6 @@ exports.Prisma.GuildSettingsScalarFieldEnum = {
|
|||||||
reactionRolesEnabled: 'reactionRolesEnabled',
|
reactionRolesEnabled: 'reactionRolesEnabled',
|
||||||
reactionRolesConfig: 'reactionRolesConfig',
|
reactionRolesConfig: 'reactionRolesConfig',
|
||||||
eventsEnabled: 'eventsEnabled',
|
eventsEnabled: 'eventsEnabled',
|
||||||
registerEnabled: 'registerEnabled',
|
|
||||||
registerConfig: 'registerConfig',
|
|
||||||
serverStatsEnabled: 'serverStatsEnabled',
|
|
||||||
serverStatsConfig: 'serverStatsConfig',
|
|
||||||
lockdownConfig: 'lockdownConfig',
|
|
||||||
tasksEnabled: 'tasksEnabled',
|
|
||||||
badgesEnabled: 'badgesEnabled',
|
|
||||||
brandingConfig: 'brandingConfig',
|
|
||||||
partnerConfig: 'partnerConfig',
|
|
||||||
galleryConfig: 'galleryConfig',
|
|
||||||
imageModerationConfig: 'imageModerationConfig',
|
|
||||||
ticketConfig: 'ticketConfig',
|
|
||||||
supportRoleId: 'supportRoleId',
|
supportRoleId: 'supportRoleId',
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: 'updatedAt',
|
||||||
createdAt: 'createdAt'
|
createdAt: 'createdAt'
|
||||||
@@ -169,30 +157,6 @@ exports.Prisma.TicketScalarFieldEnum = {
|
|||||||
status: 'status',
|
status: 'status',
|
||||||
claimedBy: 'claimedBy',
|
claimedBy: 'claimedBy',
|
||||||
transcript: 'transcript',
|
transcript: 'transcript',
|
||||||
firstClaimAt: 'firstClaimAt',
|
|
||||||
firstResponseAt: 'firstResponseAt',
|
|
||||||
kbSuggestionSentAt: 'kbSuggestionSentAt',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.TicketAutomationRuleScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
name: 'name',
|
|
||||||
condition: 'condition',
|
|
||||||
action: 'action',
|
|
||||||
active: 'active',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.KnowledgeBaseArticleScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
title: 'title',
|
|
||||||
keywords: 'keywords',
|
|
||||||
content: 'content',
|
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
@@ -264,230 +228,6 @@ exports.Prisma.EventSignupScalarFieldEnum = {
|
|||||||
canceledAt: 'canceledAt'
|
canceledAt: 'canceledAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.RegisterFormScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
name: 'name',
|
|
||||||
description: 'description',
|
|
||||||
reviewChannelId: 'reviewChannelId',
|
|
||||||
notifyRoleIds: 'notifyRoleIds',
|
|
||||||
isActive: 'isActive',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.RegisterFormFieldScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
formId: 'formId',
|
|
||||||
label: 'label',
|
|
||||||
type: 'type',
|
|
||||||
required: 'required',
|
|
||||||
order: 'order'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.RegisterApplicationScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
formId: 'formId',
|
|
||||||
status: 'status',
|
|
||||||
reviewedBy: 'reviewedBy',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.RegisterApplicationAnswerScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
applicationId: 'applicationId',
|
|
||||||
fieldId: 'fieldId',
|
|
||||||
value: 'value'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.RegisterApplicationNoteScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
applicationId: 'applicationId',
|
|
||||||
authorId: 'authorId',
|
|
||||||
authorTag: 'authorTag',
|
|
||||||
body: 'body',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.AutomodStrikeScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
filterKey: 'filterKey',
|
|
||||||
weight: 'weight',
|
|
||||||
reason: 'reason',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.StaffTaskScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
title: 'title',
|
|
||||||
description: 'description',
|
|
||||||
status: 'status',
|
|
||||||
assigneeId: 'assigneeId',
|
|
||||||
createdBy: 'createdBy',
|
|
||||||
createdByTag: 'createdByTag',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.ModCaseScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
type: 'type',
|
|
||||||
reason: 'reason',
|
|
||||||
moderatorId: 'moderatorId',
|
|
||||||
moderatorTag: 'moderatorTag',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.WatchlistScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
reason: 'reason',
|
|
||||||
addedBy: 'addedBy',
|
|
||||||
addedAt: 'addedAt',
|
|
||||||
active: 'active'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.UserBadgeScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
badgeKey: 'badgeKey',
|
|
||||||
awardedAt: 'awardedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.UserActivityScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
messageCount: 'messageCount',
|
|
||||||
activeDays: 'activeDays',
|
|
||||||
lastActiveDay: 'lastActiveDay'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.WeeklyPlanScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
title: 'title',
|
|
||||||
entries: 'entries',
|
|
||||||
createdAt: 'createdAt',
|
|
||||||
updatedAt: 'updatedAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.WeeklyPlanRsvpScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
planId: 'planId',
|
|
||||||
entryId: 'entryId',
|
|
||||||
userId: 'userId',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.PollScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
question: 'question',
|
|
||||||
options: 'options',
|
|
||||||
anonymous: 'anonymous',
|
|
||||||
closesAt: 'closesAt',
|
|
||||||
closed: 'closed',
|
|
||||||
createdBy: 'createdBy',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.PollVoteScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
pollId: 'pollId',
|
|
||||||
userId: 'userId',
|
|
||||||
optionId: 'optionId',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.SuggestionScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
userId: 'userId',
|
|
||||||
userTag: 'userTag',
|
|
||||||
content: 'content',
|
|
||||||
status: 'status',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.SuggestionVoteScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
suggestionId: 'suggestionId',
|
|
||||||
userId: 'userId',
|
|
||||||
value: 'value'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.PartnerRequestScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
userId: 'userId',
|
|
||||||
userTag: 'userTag',
|
|
||||||
serverName: 'serverName',
|
|
||||||
inviteCode: 'inviteCode',
|
|
||||||
memberCount: 'memberCount',
|
|
||||||
description: 'description',
|
|
||||||
status: 'status',
|
|
||||||
reviewedBy: 'reviewedBy',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.GalleryPostScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
userId: 'userId',
|
|
||||||
userTag: 'userTag',
|
|
||||||
imageUrl: 'imageUrl',
|
|
||||||
caption: 'caption',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.GalleryVoteScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
postId: 'postId',
|
|
||||||
userId: 'userId'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.GuildGrowthEventScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
type: 'type',
|
|
||||||
userId: 'userId',
|
|
||||||
inviteCode: 'inviteCode',
|
|
||||||
inviterId: 'inviterId',
|
|
||||||
suspicious: 'suspicious',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.InfoPanelScalarFieldEnum = {
|
|
||||||
id: 'id',
|
|
||||||
guildId: 'guildId',
|
|
||||||
channelId: 'channelId',
|
|
||||||
messageId: 'messageId',
|
|
||||||
type: 'type',
|
|
||||||
title: 'title',
|
|
||||||
description: 'description',
|
|
||||||
items: 'items',
|
|
||||||
createdAt: 'createdAt'
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.Prisma.SortOrder = {
|
exports.Prisma.SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -522,36 +262,12 @@ exports.Prisma.NullsOrder = {
|
|||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
GuildSettings: 'GuildSettings',
|
GuildSettings: 'GuildSettings',
|
||||||
Ticket: 'Ticket',
|
Ticket: 'Ticket',
|
||||||
TicketAutomationRule: 'TicketAutomationRule',
|
|
||||||
KnowledgeBaseArticle: 'KnowledgeBaseArticle',
|
|
||||||
Level: 'Level',
|
Level: 'Level',
|
||||||
TicketSupportSession: 'TicketSupportSession',
|
TicketSupportSession: 'TicketSupportSession',
|
||||||
Birthday: 'Birthday',
|
Birthday: 'Birthday',
|
||||||
ReactionRoleSet: 'ReactionRoleSet',
|
ReactionRoleSet: 'ReactionRoleSet',
|
||||||
Event: 'Event',
|
Event: 'Event',
|
||||||
EventSignup: 'EventSignup',
|
EventSignup: 'EventSignup'
|
||||||
RegisterForm: 'RegisterForm',
|
|
||||||
RegisterFormField: 'RegisterFormField',
|
|
||||||
RegisterApplication: 'RegisterApplication',
|
|
||||||
RegisterApplicationAnswer: 'RegisterApplicationAnswer',
|
|
||||||
RegisterApplicationNote: 'RegisterApplicationNote',
|
|
||||||
AutomodStrike: 'AutomodStrike',
|
|
||||||
StaffTask: 'StaffTask',
|
|
||||||
ModCase: 'ModCase',
|
|
||||||
Watchlist: 'Watchlist',
|
|
||||||
UserBadge: 'UserBadge',
|
|
||||||
UserActivity: 'UserActivity',
|
|
||||||
WeeklyPlan: 'WeeklyPlan',
|
|
||||||
WeeklyPlanRsvp: 'WeeklyPlanRsvp',
|
|
||||||
Poll: 'Poll',
|
|
||||||
PollVote: 'PollVote',
|
|
||||||
Suggestion: 'Suggestion',
|
|
||||||
SuggestionVote: 'SuggestionVote',
|
|
||||||
PartnerRequest: 'PartnerRequest',
|
|
||||||
GalleryPost: 'GalleryPost',
|
|
||||||
GalleryVote: 'GalleryVote',
|
|
||||||
GuildGrowthEvent: 'GuildGrowthEvent',
|
|
||||||
InfoPanel: 'InfoPanel'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
802
package-lock.json
generated
802
package-lock.json
generated
@@ -20,8 +20,6 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
"jimp": "^1.6.1",
|
|
||||||
"jsqr": "^1.4.0",
|
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"play-dl": "^1.9.7",
|
"play-dl": "^1.9.7",
|
||||||
"sodium-native": "^4.0.4"
|
"sodium-native": "^4.0.4"
|
||||||
@@ -36,16 +34,6 @@
|
|||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@borewit/text-codec": {
|
|
||||||
"version": "0.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
|
|
||||||
"integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -337,430 +325,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jimp/core": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/file-ops": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"await-to-js": "^3.0.0",
|
|
||||||
"exif-parser": "^0.1.12",
|
|
||||||
"file-type": "^21.3.3",
|
|
||||||
"mime": "3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/core/node_modules/mime": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"mime": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/diff": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"pixelmatch": "^5.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/file-ops": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-bmp": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"bmp-ts": "^1.0.9"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-gif": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"gifwrap": "^0.10.1",
|
|
||||||
"omggif": "^1.0.10"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-jpeg": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"jpeg-js": "^0.4.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-png": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"pngjs": "^7.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/js-tiff": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"utif2": "^4.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-blit": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-blur": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-circle": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-color": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"tinycolor2": "^1.6.0",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-contain": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/plugin-blit": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-cover": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/plugin-crop": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-crop": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-displace": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-dither": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-fisheye": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-flip": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-hash": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/js-bmp": "1.6.1",
|
|
||||||
"@jimp/js-jpeg": "1.6.1",
|
|
||||||
"@jimp/js-png": "1.6.1",
|
|
||||||
"@jimp/js-tiff": "1.6.1",
|
|
||||||
"@jimp/plugin-color": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"any-base": "^1.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-mask": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-print": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/js-jpeg": "1.6.1",
|
|
||||||
"@jimp/js-png": "1.6.1",
|
|
||||||
"@jimp/plugin-blit": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"parse-bmfont-ascii": "^1.0.6",
|
|
||||||
"parse-bmfont-binary": "^1.0.6",
|
|
||||||
"parse-bmfont-xml": "^1.1.6",
|
|
||||||
"simple-xml-to-json": "^1.2.2",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-quantize": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"image-q": "^4.0.0",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-resize": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-rotate": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/plugin-crop": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/plugin-threshold": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/plugin-color": "1.6.1",
|
|
||||||
"@jimp/plugin-hash": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1",
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/types": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"zod": "^3.23.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jimp/utils": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"tinycolor2": "^1.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
@@ -1154,52 +718,6 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tokenizer/inflate": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
|
||||||
"integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "^4.4.3",
|
|
||||||
"token-types": "^6.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tokenizer/inflate/node_modules/debug": {
|
|
||||||
"version": "4.4.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tokenizer/inflate/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@tokenizer/token": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
@@ -1497,12 +1015,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/any-base": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
@@ -1550,15 +1062,6 @@
|
|||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/await-to-js": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -1619,12 +1122,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bmp-ts": {
|
|
||||||
"version": "1.0.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz",
|
|
||||||
"integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||||
@@ -2073,11 +1570,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exif-parser": {
|
|
||||||
"version": "0.1.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
|
|
||||||
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="
|
|
||||||
},
|
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||||
@@ -2180,24 +1672,6 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/file-type": {
|
|
||||||
"version": "21.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz",
|
|
||||||
"integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@tokenizer/inflate": "^0.4.1",
|
|
||||||
"strtok3": "^10.3.4",
|
|
||||||
"token-types": "^6.1.1",
|
|
||||||
"uint8array-extras": "^1.4.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -2281,6 +1755,7 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -2358,16 +1833,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/gifwrap": {
|
|
||||||
"version": "0.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz",
|
|
||||||
"integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"image-q": "^4.0.0",
|
|
||||||
"omggif": "^1.0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
@@ -2523,41 +1988,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ieee754": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/image-q": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "16.9.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/image-q/node_modules/@types/node": {
|
|
||||||
"version": "16.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
|
|
||||||
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
@@ -2655,56 +2085,6 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jimp": {
|
|
||||||
"version": "1.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz",
|
|
||||||
"integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@jimp/core": "1.6.1",
|
|
||||||
"@jimp/diff": "1.6.1",
|
|
||||||
"@jimp/js-bmp": "1.6.1",
|
|
||||||
"@jimp/js-gif": "1.6.1",
|
|
||||||
"@jimp/js-jpeg": "1.6.1",
|
|
||||||
"@jimp/js-png": "1.6.1",
|
|
||||||
"@jimp/js-tiff": "1.6.1",
|
|
||||||
"@jimp/plugin-blit": "1.6.1",
|
|
||||||
"@jimp/plugin-blur": "1.6.1",
|
|
||||||
"@jimp/plugin-circle": "1.6.1",
|
|
||||||
"@jimp/plugin-color": "1.6.1",
|
|
||||||
"@jimp/plugin-contain": "1.6.1",
|
|
||||||
"@jimp/plugin-cover": "1.6.1",
|
|
||||||
"@jimp/plugin-crop": "1.6.1",
|
|
||||||
"@jimp/plugin-displace": "1.6.1",
|
|
||||||
"@jimp/plugin-dither": "1.6.1",
|
|
||||||
"@jimp/plugin-fisheye": "1.6.1",
|
|
||||||
"@jimp/plugin-flip": "1.6.1",
|
|
||||||
"@jimp/plugin-hash": "1.6.1",
|
|
||||||
"@jimp/plugin-mask": "1.6.1",
|
|
||||||
"@jimp/plugin-print": "1.6.1",
|
|
||||||
"@jimp/plugin-quantize": "1.6.1",
|
|
||||||
"@jimp/plugin-resize": "1.6.1",
|
|
||||||
"@jimp/plugin-rotate": "1.6.1",
|
|
||||||
"@jimp/plugin-threshold": "1.6.1",
|
|
||||||
"@jimp/types": "1.6.1",
|
|
||||||
"@jimp/utils": "1.6.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jpeg-js": {
|
|
||||||
"version": "0.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
|
||||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/jsqr": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/libsodium": {
|
"node_modules/libsodium": {
|
||||||
"version": "0.7.15",
|
"version": "0.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz",
|
||||||
@@ -3006,12 +2386,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/omggif": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@@ -3042,34 +2416,6 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pako": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
|
||||||
"license": "(MIT AND Zlib)"
|
|
||||||
},
|
|
||||||
"node_modules/parse-bmfont-ascii": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/parse-bmfont-binary": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/parse-bmfont-xml": {
|
|
||||||
"version": "1.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz",
|
|
||||||
"integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"xml-parse-from-string": "^1.0.0",
|
|
||||||
"xml2js": "^0.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parse-cache-control": {
|
"node_modules/parse-cache-control": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
|
||||||
@@ -3119,27 +2465,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pixelmatch": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"pngjs": "^6.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"pixelmatch": "bin/pixelmatch"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pixelmatch/node_modules/pngjs": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/play-audio": {
|
"node_modules/play-audio": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz",
|
||||||
@@ -3158,15 +2483,6 @@
|
|||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pngjs": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prism-media": {
|
"node_modules/prism-media": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
|
||||||
@@ -3383,15 +2699,6 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sax": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
@@ -3548,15 +2855,6 @@
|
|||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/simple-xml-to-json": {
|
|
||||||
"version": "1.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz",
|
|
||||||
"integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.12.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sodium-native": {
|
"node_modules/sodium-native": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
|
||||||
@@ -3651,22 +2949,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strtok3": {
|
|
||||||
"version": "10.3.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
|
|
||||||
"integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@tokenizer/token": "^0.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/supports-preserve-symlinks-flag": {
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
@@ -3697,12 +2979,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinycolor2": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -3725,24 +3001,6 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/token-types": {
|
|
||||||
"version": "6.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
|
|
||||||
"integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@borewit/text-codec": "^0.2.1",
|
|
||||||
"@tokenizer/token": "^0.3.0",
|
|
||||||
"ieee754": "^1.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Borewit"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tr46": {
|
"node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
@@ -3908,18 +3166,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uint8array-extras": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.21.3",
|
"version": "6.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||||
@@ -3944,15 +3190,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/utif2": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pako": "^1.0.11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -4036,34 +3273,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/xml-parse-from-string": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/xml2js": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"sax": ">=0.6.0",
|
|
||||||
"xmlbuilder": "~11.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xmlbuilder": {
|
|
||||||
"version": "11.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
|
||||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
@@ -4089,15 +3298,6 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/zod": {
|
|
||||||
"version": "3.25.76",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -7,15 +7,14 @@
|
|||||||
"schema": "src/database/schema.prisma"
|
"schema": "src/database/schema.prisma"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:web": "npm --prefix frontend run build",
|
|
||||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||||
"build": "npm run build:web && tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js"
|
"start": "node dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/opus": "^0.9.0",
|
|
||||||
"@discordjs/rest": "^2.2.0",
|
"@discordjs/rest": "^2.2.0",
|
||||||
"@discordjs/voice": "^0.19.0",
|
"@discordjs/voice": "^0.19.0",
|
||||||
|
"@discordjs/opus": "^0.9.0",
|
||||||
"@prisma/client": "^5.4.2",
|
"@prisma/client": "^5.4.2",
|
||||||
"@snazzah/davey": "^0.1.8",
|
"@snazzah/davey": "^0.1.8",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
@@ -24,12 +23,10 @@
|
|||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"ffmpeg-static": "^5.2.0",
|
|
||||||
"jimp": "^1.6.1",
|
|
||||||
"jsqr": "^1.4.0",
|
|
||||||
"libsodium-wrappers": "^0.7.13",
|
"libsodium-wrappers": "^0.7.13",
|
||||||
"play-dl": "^1.9.7",
|
"play-dl": "^1.9.7",
|
||||||
"sodium-native": "^4.0.4"
|
"sodium-native": "^4.0.4",
|
||||||
|
"ffmpeg-static": "^5.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
-- Placeholder recreated because migration was already applied in the database.
|
|
||||||
-- Schema changes are already present; this file keeps the migration timeline consistent.
|
|
||||||
@@ -26,12 +26,7 @@ model GuildSettings {
|
|||||||
birthdayConfig Json?
|
birthdayConfig Json?
|
||||||
reactionRolesEnabled Boolean?
|
reactionRolesEnabled Boolean?
|
||||||
reactionRolesConfig Json?
|
reactionRolesConfig Json?
|
||||||
eventsEnabled Boolean?
|
eventsEnabled Boolean?\n registerEnabled Boolean?\n registerConfig Json?\n supportRoleId String?
|
||||||
registerEnabled Boolean?
|
|
||||||
registerConfig Json?
|
|
||||||
serverStatsEnabled Boolean?
|
|
||||||
serverStatsConfig Json?
|
|
||||||
supportRoleId String?
|
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
@@ -177,6 +172,7 @@ model RegisterForm {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
fields RegisterFormField[]
|
fields RegisterFormField[]
|
||||||
|
applications RegisterApplication[]
|
||||||
|
|
||||||
@@index([guildId, isActive])
|
@@index([guildId, isActive])
|
||||||
}
|
}
|
||||||
@@ -187,7 +183,7 @@ model RegisterFormField {
|
|||||||
label String
|
label String
|
||||||
type String
|
type String
|
||||||
required Boolean @default(false)
|
required Boolean @default(false)
|
||||||
order Int @default(0)
|
sortOrder Int @default(0)
|
||||||
|
|
||||||
form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|||||||
347
readme.md
347
readme.md
@@ -1,274 +1,73 @@
|
|||||||
# 🚀 Papo Discord Bot
|
# Papo Discord Bot
|
||||||
|
|
||||||
<p align="center">
|
Discord-Bot (discord.js 14, TypeScript) mit Web-Dashboard, Prisma/PostgreSQL und Docker-Support.
|
||||||
<img src="https://img.shields.io/badge/Discord.js-v14-5865F2?style=for-the-badge&logo=discord&logoColor=white" />
|
|
||||||
<img src="https://img.shields.io/badge/TypeScript-5.x-3178C6?style=for-the-badge&logo=typescript&logoColor=white" />
|
## Was drin ist
|
||||||
<img src="https://img.shields.io/badge/Node.js-20-339933?style=for-the-badge&logo=node.js&logoColor=white" />
|
- Ticketsystem: Slash-Commands (/ticket, /claim, /close, /ticketpriority, /ticketstatus, /transcript, /ticketpanel), Panels, Transcripts unter `./transcripts`, Support-Login-Panel mit Rollen-Vergabe/On-Duty-Logging.
|
||||||
<img src="https://img.shields.io/badge/PostgreSQL-Prisma-2D3748?style=for-the-badge&logo=postgresql&logoColor=white" />
|
- Automod: Link-Filter (Whitelist), Spam/Caps-Erkennung, Bad-Word-Listen (Custom), Timeouts, Logging.
|
||||||
<img src="https://img.shields.io/badge/Docker-Ready-2496ED?style=for-the-badge&logo=docker&logoColor=white" />
|
- Musik: play/skip/stop/pause/resume/loop, Queue, aktivierbar/deaktivierbar pro Guild.
|
||||||
</p>
|
- Welcome: konfigurierbare Embeds (Channel, Farbe, Texte, Bilder/Uploads), Preview im Dashboard, Text-Fallback.
|
||||||
|
- Logging: Join/Leave, Message Edit/Delete, Automod/Ticket/Musik-Events mit konfigurierbarem Log-Channel/Kategorien.
|
||||||
A modern, feature-rich Discord bot built with **discord.js v14**, **TypeScript**, **Prisma**, and **PostgreSQL**, featuring a powerful web dashboard and modular architecture.
|
- Leveling: XP/Level pro Nachricht, /rank, toggelbar.
|
||||||
|
- Dynamische Voice: Lobby erzeugt private Voice-Channels mit Template/Userlimit.
|
||||||
---
|
- Birthday: /birthday + geplante Glueckwuensche mit Template/Channel.
|
||||||
|
- Reaction Roles: Verwaltung im Dashboard, Sync/Loeschen/Erstellen.
|
||||||
# ✨ Features
|
- Events: Einmalig/recurring, Reminder, Signups, Buttons.
|
||||||
|
- Statuspage-Modul vorhanden (Config/API), plus Modul-Toggles im Dashboard.
|
||||||
## 🎫 Ticket System
|
- Dashboard: OAuth2 (Scopes identify, guilds), zeigt nur Guilds, die der Nutzer besitzt oder mit Manage Guild/Admin-Rechten verwalten darf **und** in denen der Bot ist. Modulabhaengige Navigation.
|
||||||
- Interactive ticket panels
|
- Rich Presence: rotiert mit `/help`, Dashboard-URL und Guild-Zaehler.
|
||||||
- Ticket claiming
|
|
||||||
- Ticket transcripts
|
## Tech-Stack
|
||||||
- Support login sessions
|
- Node.js 20 (Docker-Basis), TypeScript (CommonJS)
|
||||||
- Slash commands (`/ticket`, `/claim`, `/close`, ...)
|
- discord.js 14, play-dl, @discordjs/voice
|
||||||
|
- Express + OAuth2-Login, Prisma ORM (PostgreSQL)
|
||||||
## 🛡️ Moderation
|
- Dockerfile + docker-compose (App + Postgres)
|
||||||
- Link whitelist
|
|
||||||
- Anti-Spam
|
## Setup (lokal, Entwicklung)
|
||||||
- Anti-Caps
|
1. Repo klonen, in das Verzeichnis wechseln.
|
||||||
- Bad word filtering
|
2. `cp .env.example .env` und Variablen setzen (siehe unten).
|
||||||
- Comprehensive server logging
|
3. Dependencies installieren: `npm ci` (oder `npm install`).
|
||||||
|
4. Prisma: `npx prisma generate --schema=src/database/schema.prisma` und `npx prisma migrate dev --name init`.
|
||||||
## 🎵 Music
|
5. Start Dev: `npm run dev` (ts-node-dev). Dashboard und Bot laufen auf `PORT` (default 3000).
|
||||||
- Play music from multiple sources
|
6. Slash-Commands werden beim Start fuer die IDs in `DISCORD_GUILD_IDS` (oder `DISCORD_GUILD_ID`) registriert.
|
||||||
- Queue support
|
|
||||||
- Pause / Resume
|
## Setup mit Docker
|
||||||
- Skip
|
- `.dockerignore` blendet lokale node_modules/.env aus.
|
||||||
- Stop
|
- Dev-Stack: `docker-compose up --build` (nutzt `Dockerfile`, Postgres 15, env aus `.env`, `npm run dev` im Container).
|
||||||
- Loop
|
- Eigenes Image: `docker build .` (Prisma-Generate laeuft im Build).
|
||||||
- Enable/Disable per server
|
|
||||||
|
## Environment-Variablen
|
||||||
## 👋 Community Features
|
- `DISCORD_TOKEN` (Pflicht, Bot Token)
|
||||||
- Welcome messages
|
- `DISCORD_CLIENT_ID` / `DISCORD_CLIENT_SECRET` (Pflicht fuer Dashboard-OAuth)
|
||||||
- Leveling system
|
- `DISCORD_GUILD_ID` (optional Einzel-Guild fuer Commands)
|
||||||
- Birthday reminders
|
- `DISCORD_GUILD_IDS` (kommagetrennt, mehrere Guilds)
|
||||||
- Reaction Roles
|
- `DATABASE_URL` (Pflicht, Postgres)
|
||||||
- Dynamic Voice Channels
|
- `PORT` (Webserver/Dashboard, default 3000)
|
||||||
- Event system with reminders
|
- `SESSION_SECRET` (Express Session Secret, default `papo_dev_secret`)
|
||||||
|
- `DASHBOARD_BASE_URL` (Public Base URL, fuer OAuth Redirect)
|
||||||
## 📊 Dashboard
|
- `WEB_BASE_PATH` (Default `/ucp`, ohne Slash am Ende)
|
||||||
- Discord OAuth2 Login
|
- `OWNER_IDS` (kommagetrennte Owner fuer Admin-UI)
|
||||||
- Guild management
|
- `SUPPORT_ROLE_ID` (optional Ticket/Support-Login Rolle)
|
||||||
- Modular settings
|
|
||||||
- Status Page integration
|
## Datenbank / Prisma
|
||||||
- Rich Presence management
|
- Schema: `src/database/schema.prisma` (zweites Schema in `prisma/schema.prisma` fuer Binary Targets).
|
||||||
- Modern responsive interface
|
- Migrationen: `npx prisma migrate dev --name <name>`; danach `npx prisma generate --schema=src/database/schema.prisma`.
|
||||||
|
- Kern-Tabellen: GuildSettings (Module/Config), Ticket, TicketSupportSession, Event/EventSignup, Birthday, ReactionRoleSet, Level.
|
||||||
---
|
|
||||||
|
## Kommandos & Scripts
|
||||||
# 🛠️ Tech Stack
|
- `npm run dev` – Entwicklung (ts-node-dev)
|
||||||
|
- `npm run build` – TypeScript build
|
||||||
| Technology | Version |
|
- `npm start` – Start aus `dist`
|
||||||
|------------|---------|
|
- Prisma-CLI: `npx prisma ...` (nutzt Schema aus `src/database/schema.prisma`)
|
||||||
| Node.js | 20+ |
|
|
||||||
| TypeScript | Latest |
|
## Dashboard / API Kurzinfo
|
||||||
| discord.js | v14 |
|
- Auth-Gate (`/api/*`), Login `/auth/discord`, Callback `/auth/callback`, Logout `/auth/logout`.
|
||||||
| Express | Latest |
|
- `/api/guilds` filtert auf Guilds, die der eingeloggte User besitzt oder managen darf und in denen der Bot ist.
|
||||||
| Prisma ORM | Latest |
|
- Module/Settings ueber `/api/settings`, `/api/modules`, Tickets unter `/api/tickets*`, weitere Endpoints fuer Events, Reaction Roles, Birthday, Statuspage.
|
||||||
| PostgreSQL | 15+ |
|
|
||||||
| Docker | Supported |
|
## Deployment-Hinweise
|
||||||
|
- Produktion: `npm run build` + `npm start` oder Docker-Image nutzen.
|
||||||
---
|
- Transcripts werden unter `./transcripts` abgelegt (Volume mounten, falls Container).
|
||||||
|
|
||||||
# 📦 Installation
|
## Credits/Lizenz
|
||||||
|
- Autoren/Lizenz nicht hinterlegt. Bitte vor Nutzung pruefen.
|
||||||
## Local Development
|
|
||||||
|
|
||||||
Clone the repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/yourname/papo-discord-bot.git
|
|
||||||
cd papo-discord-bot
|
|
||||||
```
|
|
||||||
|
|
||||||
Create your environment file
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Install dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate Prisma Client
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx prisma generate --schema=src/database/schema.prisma
|
|
||||||
```
|
|
||||||
|
|
||||||
Run database migrations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx prisma migrate dev --name init
|
|
||||||
```
|
|
||||||
|
|
||||||
Start the development server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
The bot and dashboard will start on the configured **PORT** (default: `3000`).
|
|
||||||
|
|
||||||
Slash commands are automatically registered for the guilds defined in:
|
|
||||||
|
|
||||||
- `DISCORD_GUILD_IDS`
|
|
||||||
- or `DISCORD_GUILD_ID`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🐳 Docker
|
|
||||||
|
|
||||||
Start the complete development stack
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Build the Docker image manually
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t papo-discord-bot .
|
|
||||||
```
|
|
||||||
|
|
||||||
The Docker image automatically generates the Prisma Client during the build process.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# ⚙️ Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|-----------|-------------|
|
|
||||||
| `DISCORD_TOKEN` | Discord Bot Token |
|
|
||||||
| `DISCORD_CLIENT_ID` | Discord OAuth Client ID |
|
|
||||||
| `DISCORD_CLIENT_SECRET` | Discord OAuth Secret |
|
|
||||||
| `DATABASE_URL` | PostgreSQL Connection String |
|
|
||||||
| `PORT` | Dashboard Port (default: 3000) |
|
|
||||||
| `SESSION_SECRET` | Express Session Secret |
|
|
||||||
| `DASHBOARD_BASE_URL` | Public Dashboard URL |
|
|
||||||
| `WEB_BASE_PATH` | Base path (default: `/ucp`) |
|
|
||||||
| `OWNER_IDS` | Comma-separated Bot Owners |
|
|
||||||
| `SUPPORT_ROLE_ID` | Support Role ID |
|
|
||||||
| `DISCORD_GUILD_ID(S)` | Guild(s) for command registration |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🗄️ Database
|
|
||||||
|
|
||||||
Main Prisma schema
|
|
||||||
|
|
||||||
```
|
|
||||||
src/database/schema.prisma
|
|
||||||
```
|
|
||||||
|
|
||||||
Generate Prisma Client
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx prisma generate --schema=src/database/schema.prisma
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a migration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx prisma migrate dev --name your-migration
|
|
||||||
```
|
|
||||||
|
|
||||||
### Core Models
|
|
||||||
|
|
||||||
- GuildSettings
|
|
||||||
- Ticket
|
|
||||||
- TicketSupportSession
|
|
||||||
- Event
|
|
||||||
- EventSignup
|
|
||||||
- RegisterForm
|
|
||||||
- RegisterApplication
|
|
||||||
- Birthday
|
|
||||||
- ReactionRoleSet
|
|
||||||
- Level
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 📜 Available Scripts
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `npm run dev` | Development Mode |
|
|
||||||
| `npm run build` | Compile TypeScript |
|
|
||||||
| `npm start` | Run Production Build |
|
|
||||||
| `npx prisma ...` | Prisma CLI |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🌐 Dashboard API
|
|
||||||
|
|
||||||
Authentication
|
|
||||||
|
|
||||||
```
|
|
||||||
/auth/discord
|
|
||||||
/auth/callback
|
|
||||||
/auth/logout
|
|
||||||
```
|
|
||||||
|
|
||||||
Protected API
|
|
||||||
|
|
||||||
```
|
|
||||||
/api/*
|
|
||||||
```
|
|
||||||
|
|
||||||
Main Endpoints
|
|
||||||
|
|
||||||
- `/api/guilds`
|
|
||||||
- `/api/settings`
|
|
||||||
- `/api/modules`
|
|
||||||
- `/api/tickets`
|
|
||||||
- `/api/events`
|
|
||||||
- `/api/reactionroles`
|
|
||||||
- `/api/birthday`
|
|
||||||
- `/api/statuspage`
|
|
||||||
|
|
||||||
Only guilds where the authenticated user has **Manage Server** permissions and where the bot is present are accessible.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 🚀 Deployment
|
|
||||||
|
|
||||||
Production build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
or simply use Docker.
|
|
||||||
|
|
||||||
Ticket transcripts are stored in
|
|
||||||
|
|
||||||
```
|
|
||||||
./transcripts
|
|
||||||
```
|
|
||||||
|
|
||||||
When running inside Docker, mount this directory as a volume to persist transcripts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# ❤️ Contributing
|
|
||||||
|
|
||||||
Contributions, feature requests, and bug reports are always welcome!
|
|
||||||
|
|
||||||
Feel free to open an Issue or submit a Pull Request.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 📄 License
|
|
||||||
|
|
||||||
No license has been specified yet.
|
|
||||||
|
|
||||||
Please add an appropriate open-source license before publishing this project.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
Built with ❤️ using TypeScript, Discord.js, Prisma & PostgreSQL
|
|
||||||
</p>
|
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ const command: SlashCommand = {
|
|||||||
|
|
||||||
await member.ban({ reason }).catch(() => null);
|
await member.ban({ reason }).catch(() => null);
|
||||||
await interaction.reply({ content: `${user.tag} wurde gebannt. Grund: ${reason}` });
|
await interaction.reply({ content: `${user.tag} wurde gebannt. Grund: ${reason}` });
|
||||||
context.logging.logAction(user, 'Ban', reason, interaction.guild);
|
context.logging.logAction(user, 'Ban', reason);
|
||||||
context.modCases.recordCase(interaction.guild.id, user.id, 'ban', reason, interaction.user.id, interaction.user.tag);
|
|
||||||
context.watchlist.notifyIfWatched(interaction.guild, user.id, 'Gebannt', reason);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { EmbedBuilder, SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
|
|
||||||
import { SlashCommand } from '../../utils/types';
|
|
||||||
import { context } from '../../config/context';
|
|
||||||
|
|
||||||
const TYPE_LABELS: Record<string, string> = {
|
|
||||||
warn: 'Warnungen',
|
|
||||||
mute: 'Mutes',
|
|
||||||
timeout: 'Timeouts',
|
|
||||||
kick: 'Kicks',
|
|
||||||
ban: 'Bans',
|
|
||||||
tempban: 'Tempbans',
|
|
||||||
note: 'Notizen',
|
|
||||||
watchlist_add: 'Watchlist (hinzugefügt)',
|
|
||||||
watchlist_remove: 'Watchlist (entfernt)'
|
|
||||||
};
|
|
||||||
|
|
||||||
const command: SlashCommand = {
|
|
||||||
guildOnly: true,
|
|
||||||
data: new SlashCommandBuilder()
|
|
||||||
.setName('case')
|
|
||||||
.setDescription('Zeigt oder ergänzt die Mod-Akte eines Nutzers.')
|
|
||||||
.addSubcommand((sub) =>
|
|
||||||
sub
|
|
||||||
.setName('view')
|
|
||||||
.setDescription('Zeigt die Mod-Akte eines Nutzers.')
|
|
||||||
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer').setRequired(true))
|
|
||||||
)
|
|
||||||
.addSubcommand((sub) =>
|
|
||||||
sub
|
|
||||||
.setName('note')
|
|
||||||
.setDescription('Fügt eine interne Notiz zur Akte hinzu.')
|
|
||||||
.addUserOption((opt) => opt.setName('user').setDescription('Nutzer').setRequired(true))
|
|
||||||
.addStringOption((opt) => opt.setName('body').setDescription('Notiz').setRequired(true))
|
|
||||||
)
|
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
|
||||||
async execute(interaction: ChatInputCommandInteraction) {
|
|
||||||
if (!interaction.guild) return;
|
|
||||||
const sub = interaction.options.getSubcommand();
|
|
||||||
const user = interaction.options.getUser('user', true);
|
|
||||||
|
|
||||||
if (sub === 'note') {
|
|
||||||
const body = interaction.options.getString('body', true);
|
|
||||||
await context.modCases.addNote(interaction.guild.id, user.id, interaction.user.id, interaction.user.tag, body);
|
|
||||||
await interaction.reply({ content: `Notiz zur Akte von ${user.tag} hinzugefügt.`, ephemeral: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await context.modCases.getCase(interaction.guild.id, user.id);
|
|
||||||
const watched = context.watchlist.isWatched(interaction.guild.id, user.id);
|
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setTitle(`Mod-Akte: ${user.tag}`)
|
|
||||||
.setColor(watched ? 0xeab308 : 0x7289da)
|
|
||||||
.addFields(
|
|
||||||
...Object.entries(TYPE_LABELS).map(([type, label]) => ({ name: label, value: String(data.counts[type] || 0), inline: true })),
|
|
||||||
{ name: 'Tickets erstellt', value: String(data.ticketCount), inline: true },
|
|
||||||
{ name: 'Automod-Verstöße', value: String(data.automodStrikes), inline: true },
|
|
||||||
{ name: 'Beobachtungsliste', value: watched ? 'Ja' : 'Nein', inline: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const recent = data.cases.slice(0, 10);
|
|
||||||
if (recent.length) {
|
|
||||||
embed.addFields({
|
|
||||||
name: 'Letzte Einträge',
|
|
||||||
value: recent
|
|
||||||
.map((c) => `**${TYPE_LABELS[c.type] || c.type}** — ${c.reason || 'Kein Grund'} (${c.moderatorTag}, ${c.createdAt.toLocaleDateString('de-DE')})`)
|
|
||||||
.join('\n')
|
|
||||||
.slice(0, 1024)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default command;
|
|
||||||
@@ -10,12 +10,11 @@ const command: SlashCommand = {
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
||||||
async execute(interaction: ChatInputCommandInteraction) {
|
async execute(interaction: ChatInputCommandInteraction) {
|
||||||
const amount = interaction.options.getInteger('amount', true);
|
const amount = interaction.options.getInteger('amount', true);
|
||||||
const channel = interaction.channel;
|
if (!interaction.channel || amount < 1 || amount > 100) {
|
||||||
if (!channel || !('bulkDelete' in channel) || amount < 1 || amount > 100) {
|
|
||||||
await interaction.reply({ content: 'Anzahl muss zwischen 1 und 100 liegen.', ephemeral: true });
|
await interaction.reply({ content: 'Anzahl muss zwischen 1 und 100 liegen.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const messages = await channel.bulkDelete(amount, true);
|
const messages = await interaction.channel.bulkDelete(amount, true);
|
||||||
await interaction.reply({ content: `Gelöschte Nachrichten: ${messages.size}`, ephemeral: true });
|
await interaction.reply({ content: `Gelöschte Nachrichten: ${messages.size}`, ephemeral: true });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ const command: SlashCommand = {
|
|||||||
}
|
}
|
||||||
await member.kick(reason);
|
await member.kick(reason);
|
||||||
await interaction.reply({ content: `${user.tag} wurde gekickt.` });
|
await interaction.reply({ content: `${user.tag} wurde gekickt.` });
|
||||||
context.logging.logAction(user, 'Kick', reason, interaction.guild);
|
context.logging.logAction(user, 'Kick', reason);
|
||||||
context.modCases.recordCase(interaction.guild.id, user.id, 'kick', reason, interaction.user.id, interaction.user.tag);
|
|
||||||
context.watchlist.notifyIfWatched(interaction.guild, user.id, 'Gekickt', reason);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
|
||||||
import { SlashCommand } from '../../utils/types';
|
|
||||||
import { context } from '../../config/context';
|
|
||||||
|
|
||||||
const command: SlashCommand = {
|
|
||||||
guildOnly: true,
|
|
||||||
data: new SlashCommandBuilder()
|
|
||||||
.setName('lockdown')
|
|
||||||
.setDescription('Aktiviert oder deaktiviert den Raid-Schutz/Lockdown-Modus.')
|
|
||||||
.addSubcommand((sub) =>
|
|
||||||
sub
|
|
||||||
.setName('enable')
|
|
||||||
.setDescription('Aktiviert den Lockdown-Modus.')
|
|
||||||
.addStringOption((opt) => opt.setName('reason').setDescription('Grund für den Lockdown'))
|
|
||||||
.addRoleOption((opt) => opt.setName('staff_role').setDescription('Rolle, die im Raid-Log gepingt wird'))
|
|
||||||
)
|
|
||||||
.addSubcommand((sub) => sub.setName('disable').setDescription('Hebt den Lockdown-Modus wieder auf.'))
|
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
|
||||||
async execute(interaction: ChatInputCommandInteraction) {
|
|
||||||
if (!interaction.guild) return;
|
|
||||||
const sub = interaction.options.getSubcommand();
|
|
||||||
|
|
||||||
if (sub === 'enable') {
|
|
||||||
if (context.lockdown.isActive(interaction.guildId!)) {
|
|
||||||
await interaction.reply({ content: 'Lockdown ist bereits aktiv.', ephemeral: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
const reason = interaction.options.getString('reason') ?? undefined;
|
|
||||||
const staffRole = interaction.options.getRole('staff_role');
|
|
||||||
await context.lockdown.enable(interaction.guild, interaction.user.id, reason, staffRole?.id);
|
|
||||||
await interaction.editReply({ content: 'Lockdown wurde aktiviert. Alle Textkanäle sind für @everyone gesperrt.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sub === 'disable') {
|
|
||||||
if (!context.lockdown.isActive(interaction.guildId!)) {
|
|
||||||
await interaction.reply({ content: 'Lockdown ist aktuell nicht aktiv.', ephemeral: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
await context.lockdown.disable(interaction.guild);
|
|
||||||
await interaction.editReply({ content: 'Lockdown wurde aufgehoben.' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default command;
|
|
||||||
@@ -23,9 +23,7 @@ const command: SlashCommand = {
|
|||||||
}
|
}
|
||||||
await member.timeout(minutes * 60 * 1000, reason).catch(() => null);
|
await member.timeout(minutes * 60 * 1000, reason).catch(() => null);
|
||||||
await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten gemutet.` });
|
await interaction.reply({ content: `${user.tag} wurde für ${minutes} Minuten gemutet.` });
|
||||||
context.logging.logAction(user, 'Mute', reason, interaction.guild);
|
context.logging.logAction(user, 'Mute', reason);
|
||||||
context.modCases.recordCase(interaction.guild.id, user.id, 'mute', reason, interaction.user.id, interaction.user.tag);
|
|
||||||
context.watchlist.notifyIfWatched(interaction.guild, user.id, 'Gemutet', reason);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
|
||||||
import { SlashCommand } from '../../utils/types';
|
|
||||||
import { context } from '../../config/context';
|
|
||||||
|
|
||||||
function formatList(items: { id: string; name: string }[], max = 10): string {
|
|
||||||
if (!items.length) return 'Keine';
|
|
||||||
const shown = items.slice(0, max).map((i) => i.name);
|
|
||||||
const rest = items.length - shown.length;
|
|
||||||
return shown.join(', ') + (rest > 0 ? ` (+${rest} weitere)` : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const command: SlashCommand = {
|
|
||||||
guildOnly: true,
|
|
||||||
data: new SlashCommandBuilder()
|
|
||||||
.setName('permissions')
|
|
||||||
.setDescription('Analysiert Rollen und Berechtigungen auf Sicherheitsrisiken.')
|
|
||||||
.addSubcommand((sub) => sub.setName('scan').setDescription('Startet die Rechte-Analyse.'))
|
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
|
||||||
async execute(interaction: ChatInputCommandInteraction) {
|
|
||||||
if (!interaction.guild) return;
|
|
||||||
await interaction.deferReply({ ephemeral: true });
|
|
||||||
|
|
||||||
const result = await context.permissionScan.scan(interaction.guild);
|
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
|
||||||
.setTitle('🔐 Rechte-Scan')
|
|
||||||
.setColor(result.tooManyAdmins ? 0xdc2626 : 0xf97316)
|
|
||||||
.setDescription(`Analyse für **${interaction.guild.name}**`)
|
|
||||||
.addFields(
|
|
||||||
{ name: 'Admin-Rollen', value: formatList(result.adminRoles), inline: false },
|
|
||||||
{ name: 'Rollen, die bannen können', value: formatList(result.banRoles), inline: false },
|
|
||||||
{
|
|
||||||
name: 'Bots mit gefährlichen Rechten',
|
|
||||||
value: result.dangerousBots.length
|
|
||||||
? result.dangerousBots.slice(0, 10).map((b) => `${b.tag}: ${b.perms.join(', ')}`).join('\n')
|
|
||||||
: 'Keine',
|
|
||||||
inline: false
|
|
||||||
},
|
|
||||||
{ name: 'Öffentliche Kanäle', value: formatList(result.publicChannels), inline: false },
|
|
||||||
{ name: '@everyone kann schreiben', value: formatList(result.everyoneCanSendChannels), inline: false },
|
|
||||||
{ name: 'Leere Rollen', value: formatList(result.emptyRoles), inline: false },
|
|
||||||
{
|
|
||||||
name: 'Doppelte Rollen (gleiche Rechte)',
|
|
||||||
value: result.duplicateRoleGroups.length
|
|
||||||
? result.duplicateRoleGroups.slice(0, 5).map((g) => g.map((r) => r.name).join(' = ')).join('\n')
|
|
||||||
: 'Keine',
|
|
||||||
inline: false
|
|
||||||
},
|
|
||||||
{ name: 'Nutzlose Rollen (keine Rechte, keine Mitglieder)', value: formatList(result.uselessRoles), inline: false },
|
|
||||||
{
|
|
||||||
name: '⚠️ Zu viele Admin-Rechte?',
|
|
||||||
value: result.tooManyAdmins
|
|
||||||
? `Ja — ${result.adminRoles.length} Admin-Rolle(n), ${result.adminMemberCount} Mitglied(er) mit Administrator.`
|
|
||||||
: 'Nein, sieht unauffällig aus.',
|
|
||||||
inline: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.setTimestamp();
|
|
||||||
context.branding.applyFooter(embed, interaction.guild.id);
|
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default command;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user