Remove management panel (will be reimplemented)
This commit is contained in:
parent
2c851516b2
commit
0587d08437
27 changed files with 0 additions and 4627 deletions
|
@ -49,17 +49,6 @@ services:
|
|||
- 0.0.0.0:9000:9000
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./web/Dockerfile
|
||||
args:
|
||||
- VITE_API_URL=${PUBLIC_API_URL}
|
||||
- VITE_BOT_PREFIX=${BOT_PREFIX}
|
||||
ports:
|
||||
- 0.0.0.0:8080:80
|
||||
restart: unless-stopped
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
environment:
|
||||
|
|
13
web/.gitignore
vendored
13
web/.gitignore
vendored
|
@ -1,13 +0,0 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
894
web/.yarn/releases/yarn-4.3.1.cjs
vendored
894
web/.yarn/releases/yarn-4.3.1.cjs
vendored
File diff suppressed because one or more lines are too long
|
@ -1,7 +0,0 @@
|
|||
compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.3.1.cjs
|
|
@ -1,22 +0,0 @@
|
|||
FROM node:18 as build
|
||||
ARG VITE_API_URL
|
||||
ARG VITE_BOT_PREFIX
|
||||
WORKDIR /build/app
|
||||
COPY web/package.json web/yarn.lock web/.yarnrc.yml ./
|
||||
COPY web/.yarn ./.yarn
|
||||
COPY lib ../lib
|
||||
RUN yarn --cwd ../lib --immutable
|
||||
RUN yarn --cwd ../lib build
|
||||
RUN yarn install --immutable
|
||||
COPY web .
|
||||
RUN yarn build
|
||||
|
||||
FROM node:18 as prod
|
||||
WORKDIR /app/web
|
||||
COPY --from=build /build/app/package.json /build/app/yarn.lock /build/app/.yarnrc.yml ./
|
||||
COPY --from=build /build/app/.yarn ./.yarn
|
||||
COPY --from=build /build/app/dist ./dist
|
||||
COPY --from=build /build/lib ../lib
|
||||
RUN yarn add vite
|
||||
# Running this with bash -c because it won't exit on ctrl+c otherwise
|
||||
CMD ["bash", "-c", "yarn preview --port=80 --strictPort=true --clearScreen=false --host"]
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Automod Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<noscript>
|
||||
<h1 style="color: #ffffff;">This application requires JavaScript.</h1>
|
||||
</noscript>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/core-js": "^2.5.8",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"automod": "^0.1.0",
|
||||
"axios": "^1.7.2",
|
||||
"core-js": "^3.37.1",
|
||||
"katex": "^0.16.11",
|
||||
"localforage": "^1.10.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"styled-components": "^6.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.3"
|
||||
},
|
||||
"packageManager": "yarn@4.3.1",
|
||||
"resolutions": {
|
||||
"automod": "portal:../lib"
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import { Route, BrowserRouter, Routes } from 'react-router-dom';
|
||||
import Home from './pages/Home';
|
||||
import './App.css';
|
||||
import RequireAuth from './components/RequireAuth';
|
||||
import DashboardHome from './pages/DashboardHome';
|
||||
import ServerDashboard from './pages/ServerDashboard/ServerDashboard';
|
||||
import localforage from 'localforage';
|
||||
import TexPage from './pages/Tex';
|
||||
|
||||
const API_URL = import.meta.env['VITE_API_URL']?.toString()
|
||||
|| 'http://localhost:9000';
|
||||
|
||||
const BOT_PREFIX = import.meta.env['VITE_BOT_PREFIX']?.toString()
|
||||
|| '/';
|
||||
|
||||
function App() {
|
||||
const authConfig = new URLSearchParams(window.location.search).get('setAuth');
|
||||
|
||||
if (authConfig) {
|
||||
console.log('Using provided auth data');
|
||||
|
||||
const [ user, token ] = authConfig.split(':');
|
||||
localforage.setItem('auth', {
|
||||
user: decodeURIComponent(user),
|
||||
token: decodeURIComponent(token),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path='/' element={<Home />} />
|
||||
<Route path='/dashboard' element={<RequireAuth><DashboardHome /></RequireAuth>} />
|
||||
<Route path='/dashboard/:serverid' element={<RequireAuth><ServerDashboard /></RequireAuth>} />
|
||||
<Route path='/tex' element={<TexPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export { API_URL, BOT_PREFIX }
|
|
@ -1 +0,0 @@
|
|||
<svg viewBox="0 0 24 24" height="24" width="24" aria-hidden="true" focusable="false" fill="#848484" xmlns="http://www.w3.org/2000/svg" class="StyledIconBase-ea9ulj-0 bWRyML"><path d="M16.018 3.815 15.232 8h-4.966l.716-3.815-1.964-.37L8.232 8H4v2h3.857l-.751 4H3v2h3.731l-.714 3.805 1.965.369L8.766 16h4.966l-.714 3.805 1.965.369.783-4.174H20v-2h-3.859l.751-4H21V8h-3.733l.716-3.815-1.965-.37zM14.106 14H9.141l.751-4h4.966l-.752 4z"></path></svg>
|
Before Width: | Height: | Size: 445 B |
|
@ -1,20 +0,0 @@
|
|||
import { FunctionComponent } from "react";
|
||||
import './styles/CategorySelector.css';
|
||||
|
||||
const CategorySelector: FunctionComponent<{ keys: { id: string, name: string }[], selected: string, onChange: (key: string) => void }> = (props) => {
|
||||
return (
|
||||
<div className="category-selector-outer">
|
||||
{props.keys.map((k) => (
|
||||
<div
|
||||
className={`category-selector-inner ${props.selected == k.id ? 'selected' : ''}`}
|
||||
key={k.id}
|
||||
onClick={() => props.onChange(k.id)}
|
||||
>
|
||||
<span>{k.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CategorySelector;
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface CheckboxProps {
|
||||
value: boolean;
|
||||
onChange: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const Checkbox: React.FC<CheckboxProps> = ({ value, onChange, title, description }) => {
|
||||
return (
|
||||
<div style={{ maxWidth: '400px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label title={title}>
|
||||
{title}
|
||||
<br />
|
||||
<span style={{ fontSize: 'smaller' }}>
|
||||
{description}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
|
@ -1,19 +0,0 @@
|
|||
import { FunctionComponent, useState, useEffect } from "react";
|
||||
import Login from "../pages/Login";
|
||||
import { getAuth } from "../utils";
|
||||
|
||||
interface RequireAuthProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const RequireAuth: FunctionComponent<RequireAuthProps> = (props) => {
|
||||
const [loggedIn, setLoggedIn] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getAuth().then(res => setLoggedIn(!!res));
|
||||
});
|
||||
|
||||
return loggedIn ? <>{props.children}</> : <Login />
|
||||
}
|
||||
|
||||
export default RequireAuth;
|
|
@ -1,40 +0,0 @@
|
|||
.category-selector-outer {
|
||||
width: calc(100% - 20px);
|
||||
margin: 8px 10px;
|
||||
height: 32px;
|
||||
background-color: var(--secondary-background);
|
||||
display: flex;
|
||||
border-radius: 6px;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.category-selector-inner {
|
||||
background-color: var(--tertiary-background);
|
||||
height: 24px;
|
||||
margin: 4px;
|
||||
width: 100vw;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
transition: filter .2s, background-color .3s;
|
||||
}
|
||||
|
||||
.category-selector-inner:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.category-selector-inner:active {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.category-selector-inner.selected {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
.category-selector-inner span {
|
||||
color: var(--secondary-foreground);
|
||||
}
|
||||
.category-selector-inner.selected span {
|
||||
color: var(--foreground);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512" height="512" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m256 15.004c-20.838 0-55.66 7.9995-88.428 16.905-20.21 5.5598-40.326 11.454-60.339 17.684l308.22 308.22c37.274-64.278 62.383-151.26 47.694-261.55-1.1373-8.6707-4.7003-16.846-10.277-23.585-5.5768-6.7387-12.944-11.768-21.252-14.511-19.872-6.4903-53.668-17.206-87.19-26.262-32.768-8.9053-67.59-16.905-88.428-16.905z" fill="#9dc6d3" stroke-width="1.5431"/>
|
||||
<path d="m312.69 471.88c-9.9947 7.1785-20.584 13.49-31.651 18.868-8.4562 3.9843-17.547 7.2449-25.037 7.2449s-16.55-3.2606-25.037-7.2449c-11.067-5.3777-21.656-11.689-31.651-18.868-28.901-20.742-54.524-45.704-76.015-74.048-42.869-56.053-79.364-136.37-78.694-241.6l1.0894-28.77c0.68668-9.9932 1.7067-20.192 3.08-30.6l322.36 322.36 17.571-21.392-327.5-327.5 19.176-12.169 321.42 321.42-0.26233 0.41973c-4.1911 6.194-8.4794 12.137-12.832 17.831l-17.571 21.392c-5.3824 6.0536-10.967 11.92-16.744 17.59l-15.499 14.329c-8.3945 7.3097-17.136 14.231-26.2 20.736z" fill="#9dc6d3" stroke-width="1.5431"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,13 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
|
@ -1,79 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from "react";
|
||||
import { API_URL } from "../App";
|
||||
import { getAuthHeaders } from "../utils";
|
||||
|
||||
type Server = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string }
|
||||
|
||||
function permissionName(p: number) {
|
||||
switch(p) {
|
||||
case 0: return 'User';
|
||||
case 1: return 'Moderator';
|
||||
case 2:
|
||||
case 3: return 'Manager';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
const Dashboard: FunctionComponent = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [servers, setServers] = useState([] as Server[]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const loadServers = useCallback(async () => {
|
||||
try {
|
||||
const res = await axios.get(API_URL + '/dash/servers', { headers: await getAuthHeaders() });
|
||||
setServers(res.data.servers);
|
||||
setLoading(false);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadServers() }, []);
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: '12px', padding: '16px 0' }}>
|
||||
<h1>Dashboard</h1>
|
||||
<br/>
|
||||
<p hidden={!loading}>Loading...</p>
|
||||
{
|
||||
servers.map(server => <div className="server-card" style={{ paddingTop: '10px' }} key={server.id}>
|
||||
<img
|
||||
src={server.iconURL ?? 'https://dl.insrt.uk/projects/revolt/emotes/trol.png'}
|
||||
width={48}
|
||||
height={48}
|
||||
style={{
|
||||
float: 'left',
|
||||
marginLeft: '8px',
|
||||
marginRight: '12px',
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
float: 'left',
|
||||
maxWidth: '240px',
|
||||
textOverflow: 'clip',
|
||||
overflow: 'clip',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<h2>{server.name} ({permissionName(server.perms)})</h2>
|
||||
<code style={{ color: 'var(--foreground)' }}>{server.id}</code>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
style={{ position: 'relative', top: '8px', left: '12px' }}
|
||||
onClick={() => {
|
||||
navigate(`/dashboard/${server.id}`);
|
||||
}}
|
||||
>Open</button>
|
||||
</div>
|
||||
<div style={{ clear: 'both' }} />
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
|
@ -1,33 +0,0 @@
|
|||
import { FunctionComponent } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import AutomodIcon from '../favicon.svg';
|
||||
|
||||
const Home: FunctionComponent = () => {
|
||||
return (
|
||||
<div style={{ marginLeft: '12px' }}>
|
||||
<div style={{ display: 'flex', padding: '16px 0' }}>
|
||||
<img src={AutomodIcon} style={{ height: '40px' }} />
|
||||
<h1 style={{ color: 'var(--foreground)', margin: '0', paddingLeft: '8px' }}>Automod Web UI</h1>
|
||||
</div>
|
||||
|
||||
<span style={{ color: 'var(--foreground)' }}>
|
||||
This is a <b>work-in-progress</b> Web UI for the Automod Revolt bot.
|
||||
<br />
|
||||
<Link to='/dashboard'>
|
||||
Open the dashboard
|
||||
</Link> or <a href="https://app.revolt.chat/bot/01FHGJ3NPP7XANQQH8C2BE44ZY" target='_blank'>
|
||||
add the bot to your server.
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
You can also view usage stats and metrics for the bot <a href="https://grafana.janderedev.xyz/d/lC_-g_-nz/automod" target='_blank'>
|
||||
here
|
||||
</a>, or check out <a href="https://github.com/sussycatgirl/automod" target='_blank'>
|
||||
its GitHub repository.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
|
@ -1,75 +0,0 @@
|
|||
import localforage from "localforage";
|
||||
import axios from 'axios';
|
||||
import { FunctionComponent, useCallback, useState } from "react";
|
||||
import { API_URL, BOT_PREFIX } from "../App";
|
||||
|
||||
const Login: FunctionComponent = () => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [showInitial, setShowInitial] = useState(true);
|
||||
const [showSecond, setShowSecond] = useState(false);
|
||||
const [statusMsg, setStatusMsg] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [nonce, setNonce] = useState('');
|
||||
|
||||
const getCode = useCallback(async () => {
|
||||
if (!username) return;
|
||||
setShowInitial(false);
|
||||
|
||||
try {
|
||||
const res = await axios.post(`${API_URL}/login/begin`, { user: username });
|
||||
setShowSecond(true);
|
||||
setCode(res.data.code);
|
||||
setNonce(res.data.nonce);
|
||||
setUsername(res.data.uid);
|
||||
} catch(e: any) {
|
||||
setStatusMsg(e?.message || e);
|
||||
setShowInitial(true);
|
||||
setShowSecond(false);
|
||||
}
|
||||
}, [ username ]);
|
||||
|
||||
const getSession = useCallback(async () => {
|
||||
try {
|
||||
const res = await axios.post(`${API_URL}/login/complete`, {
|
||||
nonce, code, user: username
|
||||
});
|
||||
|
||||
await localforage.setItem('auth', { user: res.data.user, token: res.data.token });
|
||||
|
||||
setShowSecond(false);
|
||||
window.location.reload();
|
||||
} catch(e: any) {
|
||||
setStatusMsg(e?.message || e);
|
||||
}
|
||||
}, [ nonce, code, username ]);
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: '12px', padding: '16px 0' }}>
|
||||
<h1>Log in</h1>
|
||||
{statusMsg.length ? <a>{statusMsg}</a> : <br/>}
|
||||
<div hidden={!showInitial}>
|
||||
<input
|
||||
onChange={e => {
|
||||
setUsername(e.target.value);
|
||||
setStatusMsg('');
|
||||
}}
|
||||
placeholder="Enter your user ID..."
|
||||
style={{ width: "200px", float: "left" }}
|
||||
/>
|
||||
<button onClick={getCode} disabled={username.length == 0}>Continue</button>
|
||||
</div>
|
||||
<div hidden={!showSecond}>
|
||||
<h2>Your code: <a>{code}</a></h2>
|
||||
<p style={{ color: "var(--foreground)" }}>
|
||||
Run <code style={{ userSelect: 'all' }}>
|
||||
{BOT_PREFIX}login {code}
|
||||
</code> in any server using AutoMod, then <a
|
||||
onClick={getSession}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>click here</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
|
@ -1,731 +0,0 @@
|
|||
import axios from 'axios';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState } from "react";
|
||||
import { Icon } from '@mdi/react';
|
||||
import { mdiChevronLeft, mdiCloseBox } from '@mdi/js';
|
||||
import { API_URL } from "../../App";
|
||||
import { getAuthHeaders } from "../../utils";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import defaultChannelIcon from '../../assets/channel-default-icon.svg';
|
||||
import CategorySelector from '../../components/CategorySelector';
|
||||
import Checkbox from '../../components/Checkbox';
|
||||
import { CSSProperties } from 'styled-components';
|
||||
|
||||
type User = { id: string, username?: string, avatarURL?: string }
|
||||
type Channel = { id: string, name: string, icon?: string, type: 'VOICE'|'TEXT', nsfw: boolean }
|
||||
|
||||
type Server = {
|
||||
id?: string,
|
||||
perms?: 0|1|2|3,
|
||||
name?: string,
|
||||
description?: string,
|
||||
iconURL?: string,
|
||||
bannerURL?: string,
|
||||
serverConfig?: { [key: string]: any },
|
||||
users: User[],
|
||||
channels: Channel[],
|
||||
dmOnKick?: boolean,
|
||||
dmOnWarn?: boolean,
|
||||
contact?: string,
|
||||
}
|
||||
|
||||
type AntispamRule = {
|
||||
id: string;
|
||||
max_msg: number;
|
||||
timeframe: number;
|
||||
action: 0|1|2|3|4;
|
||||
channels: string[] | null;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
const STYLE_DISABLED: CSSProperties = {
|
||||
filter: 'grayscale(100%)',
|
||||
pointerEvents: 'none',
|
||||
}
|
||||
|
||||
const ServerDashboard: FunctionComponent = () => {
|
||||
const [category, setCategory] = useState('home');
|
||||
|
||||
const [serverInfo, setServerInfo] = useState({} as Server);
|
||||
const [status, setStatus] = useState('');
|
||||
|
||||
const [changed, setChanged] = useState({} as {
|
||||
prefix?: boolean,
|
||||
prefixAllowSpace?: boolean,
|
||||
dmOnKick?: boolean,
|
||||
dmOnWarn?: boolean,
|
||||
contact?: boolean,
|
||||
});
|
||||
const [prefix, setPrefix] = useState('' as string|undefined);
|
||||
const [prefixAllowSpace, setPrefixAllowSpace] = useState(false);
|
||||
const [dmOnKick, setDmOnKick] = useState(false);
|
||||
const [dmOnWarn, setDmOnWarn] = useState(false);
|
||||
const [contact, setContact] = useState('');
|
||||
|
||||
const [botManagers, setBotManagers] = useState([] as string[]);
|
||||
const [moderators, setModerators] = useState([] as string[]);
|
||||
|
||||
const [automodSettings, setAutomodSettings] = useState(null as { antispam: AntispamRule[] }|null);
|
||||
|
||||
const { serverid } = useParams();
|
||||
|
||||
const embedded = !!(new URLSearchParams(window.location.search).get('embedded'));
|
||||
|
||||
const saveConfig = useCallback(async () => {
|
||||
if (Object.values(changed).filter(i => i).length == 0) return;
|
||||
|
||||
const payload = {
|
||||
...(changed.prefix ? { prefix } : undefined),
|
||||
...(changed.prefixAllowSpace ? { spaceAfterPrefix: prefixAllowSpace } : undefined),
|
||||
...(changed.dmOnKick ? { dmOnKick } : undefined),
|
||||
...(changed.dmOnWarn ? { dmOnWarn } : undefined),
|
||||
...(changed.contact ? { contact: contact || null } : undefined),
|
||||
}
|
||||
|
||||
const res = await axios.put(
|
||||
API_URL + `/dash/server/${serverid}/config`,
|
||||
payload,
|
||||
{ headers: await getAuthHeaders() }
|
||||
);
|
||||
|
||||
if (res.data.success) {
|
||||
setChanged({});
|
||||
}
|
||||
}, [ prefix, prefixAllowSpace, changed ]);
|
||||
|
||||
const loadInfo = useCallback(async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_URL}/dash/server/${serverid}`, { headers: await getAuthHeaders() });
|
||||
console.log(res.data);
|
||||
|
||||
const server: Server = res.data.server;
|
||||
setServerInfo(server);
|
||||
|
||||
setPrefix(server.serverConfig?.['prefix'] || '');
|
||||
setPrefixAllowSpace(!!server.serverConfig?.['spaceAfterPrefix']);
|
||||
setDmOnKick(!!server.serverConfig?.['dmOnKick']);
|
||||
setDmOnWarn(!!server.serverConfig?.['dmOnWarn']);
|
||||
setContact(server.serverConfig?.['contact'] || '');
|
||||
|
||||
setBotManagers(server.serverConfig?.['botManagers'] ?? []);
|
||||
setModerators(server.serverConfig?.['moderators'] ?? []);
|
||||
|
||||
loadAutomodInfo(server);
|
||||
} catch(e: any) {
|
||||
console.error(e);
|
||||
setStatus(`${e?.message ?? e}`);
|
||||
}
|
||||
}, [serverInfo]);
|
||||
|
||||
const loadAutomodInfo = useCallback(async (server: Server) => {
|
||||
if ((server.perms ?? 0) > 0) {
|
||||
const res = await axios.get(API_URL + `/dash/server/${serverid}/automod`, { headers: await getAuthHeaders() });
|
||||
setAutomodSettings(res.data);
|
||||
console.log(res.data);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{status.length ? <a>{status}</a> : <></>}
|
||||
{!embedded && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '8px',
|
||||
marginLeft: '8px',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
color: 'var(--secondary-foreground)',
|
||||
maxWidth: 'calc(100% - 20px)',
|
||||
}}
|
||||
>
|
||||
<Link to='/dashboard' style={{ float: 'left' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Icon path={mdiChevronLeft} style={{ height: '25px' }} />
|
||||
<span>Back</span>
|
||||
</div>
|
||||
</Link>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--foreground)',
|
||||
marginLeft: '8px',
|
||||
}}
|
||||
>
|
||||
{serverInfo?.name ?? 'Loading...'}
|
||||
</span>
|
||||
<span style={{ color: 'var(--secondary-foreground)', marginLeft: '6px' }}>
|
||||
•
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--secondary-foreground)',
|
||||
marginLeft: '6px',
|
||||
}}
|
||||
>
|
||||
{serverInfo.description || <i>No server description set</i>}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CategorySelector
|
||||
keys={[
|
||||
{ id: 'home', name: 'Home' },
|
||||
{ id: 'automod', name: 'Moderation Rules' },
|
||||
]}
|
||||
selected={category}
|
||||
onChange={setCategory}
|
||||
/>
|
||||
|
||||
<div hidden={Object.keys(serverInfo).length == 0}>
|
||||
<div style={{ paddingLeft: '10px', paddingRight: '10px' }}>
|
||||
|
||||
{category == 'home' && (
|
||||
<div style={serverInfo.perms ? {} : STYLE_DISABLED}>
|
||||
<>
|
||||
<h3>Prefix</h3>
|
||||
<input
|
||||
type="text"
|
||||
style={{ width: '150px', }}
|
||||
placeholder="Enter a prefix..."
|
||||
value={prefix}
|
||||
onChange={e => {
|
||||
setPrefix(e.currentTarget.value);
|
||||
setChanged({ ...changed, prefix: true });
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
value={prefixAllowSpace}
|
||||
onChange={() => {
|
||||
setPrefixAllowSpace(!prefixAllowSpace);
|
||||
setChanged({ ...changed, prefixAllowSpace: true });
|
||||
}}
|
||||
title="Allow space after prefix"
|
||||
description={'Whether the bot recognizes a command if the prefix is followed by a space. Enable if your prefix is a word.'}
|
||||
/>
|
||||
</>
|
||||
|
||||
<hr />
|
||||
|
||||
<>
|
||||
<h3>Bot Managers</h3>
|
||||
<h4>
|
||||
Only users with "Manage Server" permission are allowed to add/remove other
|
||||
bot managers and are automatically considered bot manager.
|
||||
</h4>
|
||||
<UserListTypeContainer>
|
||||
<UserListContainer disabled={(serverInfo.perms ?? 0) < 3}>
|
||||
{botManagers.map((uid: string) => {
|
||||
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
|
||||
return (
|
||||
<UserListEntry type='MANAGER' user={user} key={uid} />
|
||||
)})}
|
||||
<UserListAddField type='MANAGER' />
|
||||
</UserListContainer>
|
||||
</UserListTypeContainer>
|
||||
|
||||
<h3>Moderators</h3>
|
||||
<h4>
|
||||
Only bot managers are allowed to add/remove moderators.
|
||||
All bot managers and users with "Kick Members" permission are also moderators.
|
||||
</h4>
|
||||
<UserListTypeContainer>
|
||||
<UserListContainer disabled={(serverInfo.perms ?? 0) < 2}>
|
||||
{moderators.map((uid: string) => {
|
||||
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
|
||||
return (
|
||||
<UserListEntry type='MOD' user={user} key={uid} />
|
||||
)})}
|
||||
<UserListAddField type='MOD' />
|
||||
</UserListContainer>
|
||||
</UserListTypeContainer>
|
||||
</>
|
||||
|
||||
<hr />
|
||||
|
||||
<>
|
||||
<h3>Infraction DMs</h3>
|
||||
<Checkbox
|
||||
title="DM on kick/ban"
|
||||
description="If enabled, users will receive a DM when getting kicked or banned"
|
||||
value={dmOnKick}
|
||||
onChange={() => { setDmOnKick(!dmOnKick); setChanged({ ...changed, dmOnKick: true }) }}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
title="DM on warn"
|
||||
description="If enabled, users will receive a DM when getting warned"
|
||||
value={dmOnWarn}
|
||||
onChange={() => { setDmOnWarn(!dmOnWarn); setChanged({ ...changed, dmOnWarn: true }) }}
|
||||
/>
|
||||
|
||||
<h3>Contact info</h3>
|
||||
<h4>
|
||||
Provide a link, email address or instructions for users on how to contact you.
|
||||
If provided, this data will be sent along with warn/kick/ban DM messages.
|
||||
</h4>
|
||||
<input
|
||||
type="text"
|
||||
style={{ margin: '8px', width: 'calc(100% - 16px)' }}
|
||||
title='Contact info'
|
||||
placeholder='http/https URL, mailto link or custom text...'
|
||||
value={contact}
|
||||
onChange={e => { setContact(e.currentTarget.value); setChanged({ ...changed, contact: true }) }}
|
||||
/>
|
||||
|
||||
<button
|
||||
style={{
|
||||
position: "fixed",
|
||||
right: '8px',
|
||||
bottom: Object.values(changed).filter(i => i).length ? '8px' : '-40px',
|
||||
}}
|
||||
onClick={saveConfig}
|
||||
>Save</button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{category == 'automod' && (
|
||||
<>
|
||||
<h3>Antispam Rules</h3>
|
||||
{serverInfo.perms != null && automodSettings && (
|
||||
serverInfo.perms > 0
|
||||
? (
|
||||
<>
|
||||
{automodSettings.antispam.map((r, i) => (
|
||||
<>
|
||||
<AntispamRule rule={r} key={r.id} />
|
||||
{i < automodSettings.antispam.length - 1 && <hr/>}
|
||||
</>
|
||||
))}
|
||||
<button style={{
|
||||
marginTop: '12px',
|
||||
marginBottom: '8px',
|
||||
}} onClick={async () => {
|
||||
const newRule: AntispamRule = {
|
||||
action: 0,
|
||||
max_msg: 5,
|
||||
timeframe: 3,
|
||||
message: null,
|
||||
id: '',
|
||||
channels: [],
|
||||
}
|
||||
|
||||
const res = await axios.post(
|
||||
`${API_URL}/dash/server/${serverid}/automod`,
|
||||
{
|
||||
action: newRule.action,
|
||||
max_msg: newRule.max_msg,
|
||||
timeframe: newRule.timeframe,
|
||||
},
|
||||
{ headers: await getAuthHeaders() }
|
||||
);
|
||||
|
||||
newRule.id = res.data.id;
|
||||
|
||||
setAutomodSettings({ antispam: [ ...(automodSettings.antispam), newRule ] });
|
||||
}}>
|
||||
Create Rule
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
<p style={{ color: 'var(--foreground)' }}>
|
||||
You do not have access to this.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
function RemoveButton(props: { onClick: () => void }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
height: '30px',
|
||||
}}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Icon // todo: hover effect
|
||||
path={mdiCloseBox}
|
||||
color='var(--tertiary-foreground)'
|
||||
size='30px'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UserListEntry(props: { user: User, type: 'MANAGER'|'MOD' }) {
|
||||
return (
|
||||
<div
|
||||
key={props.user.id}
|
||||
style={{
|
||||
display: 'block',
|
||||
margin: '4px 6px',
|
||||
padding: '4px',
|
||||
backgroundColor: 'var(--tertiary-background)',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={props.user.avatarURL ?? 'https://amogus.org/amogus.png'}
|
||||
width={28}
|
||||
height={28}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--foreground)',
|
||||
fontSize: '20px',
|
||||
paddingLeft: '6px',
|
||||
marginBottom: '2px',
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>{props.user.username ?? 'Unknown'}</span>
|
||||
<RemoveButton
|
||||
onClick={async () => {
|
||||
const res = await axios.delete(
|
||||
`${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}/${props.user.id}`,
|
||||
{ headers: await getAuthHeaders() }
|
||||
);
|
||||
|
||||
if (props.type == 'MANAGER') {
|
||||
setBotManagers(res.data.managers);
|
||||
}
|
||||
else if (props.type == 'MOD') {
|
||||
setModerators(res.data.mods);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserListContainer(props: { disabled: boolean, children: any }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
...(props.disabled ? STYLE_DISABLED : {})
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserListTypeContainer(props: any) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
backgroundColor: 'var(--secondary-background)',
|
||||
borderRadius: '10px',
|
||||
marginTop: '15px',
|
||||
paddingTop: '5px',
|
||||
paddingBottom: '5px',
|
||||
}}
|
||||
>{props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserListAddField(props: { type: 'MANAGER'|'MOD' }) {
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const onConfirm = useCallback(async () => {0
|
||||
if (content.length) {
|
||||
const res = await axios.put(
|
||||
`${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}`,
|
||||
{ item: content },
|
||||
{ headers: await getAuthHeaders() }
|
||||
);
|
||||
|
||||
if (res.data.users?.length) {
|
||||
res.data.users.forEach((user: User) => {
|
||||
if (!serverInfo.users.find(u => u.id == user.id)) serverInfo.users.push(user);
|
||||
});
|
||||
}
|
||||
|
||||
if (props.type == 'MANAGER') {
|
||||
setBotManagers(res.data.managers);
|
||||
}
|
||||
else if (props.type == 'MOD') {
|
||||
setModerators(res.data.mods);
|
||||
}
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
placeholder={`Add a ${props.type == 'MANAGER' ? 'bot manager' : 'moderator'}...`}
|
||||
value={content}
|
||||
onChange={e => setContent(e.currentTarget.value)}
|
||||
style={{
|
||||
float: 'left',
|
||||
width: '180px',
|
||||
height: '38px',
|
||||
margin: '4px 8px',
|
||||
}}
|
||||
onKeyDown={e => e.key == 'Enter' && onConfirm()}
|
||||
/>
|
||||
<button
|
||||
style={{
|
||||
float: 'left',
|
||||
width: '40px',
|
||||
height: '38px',
|
||||
margin: '4px 8px',
|
||||
opacity: content.length > 0 ? '1' : '0',
|
||||
}}
|
||||
onClick={onConfirm}
|
||||
>Ok</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelListAddField(props: { onInput: (channel: Channel) => void }) {
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const onConfirm = useCallback(async () => {
|
||||
if (content.length) {
|
||||
const channel = serverInfo.channels
|
||||
.find(c => c.id == content.toUpperCase())
|
||||
|| serverInfo.channels
|
||||
.find(c => c.name == content)
|
||||
|| serverInfo.channels // Prefer channel with same capitalization,
|
||||
.find(c => c.name.toLowerCase() == content.toLowerCase()); // otherwise search case insensitive
|
||||
|
||||
if (channel && channel.type == 'TEXT') {
|
||||
props.onInput(channel);
|
||||
setContent('');
|
||||
}
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
placeholder={`Add a channel...`}
|
||||
value={content}
|
||||
onChange={e => setContent(e.currentTarget.value)}
|
||||
style={{
|
||||
float: 'left',
|
||||
width: '180px',
|
||||
height: '38px',
|
||||
margin: '4px 8px',
|
||||
}}
|
||||
onKeyDown={e => e.key == 'Enter' && onConfirm()}
|
||||
/>
|
||||
<button
|
||||
style={{
|
||||
float: 'left',
|
||||
width: '40px',
|
||||
height: '38px',
|
||||
margin: '4px 8px',
|
||||
opacity: content.length > 0 ? '1' : '0',
|
||||
}}
|
||||
onClick={onConfirm}
|
||||
>Ok</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AntispamRule(props: { rule: AntispamRule }) {
|
||||
const [maxMsg, setMaxMsg] = useState(props.rule.max_msg);
|
||||
const [timeframe, setTimeframe] = useState(props.rule.timeframe);
|
||||
const [action, setAction] = useState(props.rule.action);
|
||||
const [message, setMessage] = useState(props.rule.message || '');
|
||||
const [channels, setChannels] = useState(props.rule.channels ?? []);
|
||||
const [channelsChanged, setChannelsChanged] = useState(false);
|
||||
|
||||
const save = useCallback(async () => {
|
||||
await axios.patch(
|
||||
`${API_URL}/dash/server/${serverid}/automod/${props.rule.id}`,
|
||||
{
|
||||
action: action != props.rule.action ? action : undefined,
|
||||
channels: channelsChanged ? channels : undefined,
|
||||
max_msg: maxMsg != props.rule.max_msg ? maxMsg : undefined,
|
||||
message: message != props.rule.message ? message : undefined,
|
||||
timeframe: timeframe != props.rule.timeframe ? timeframe : undefined,
|
||||
} as AntispamRule,
|
||||
{ headers: await getAuthHeaders() }
|
||||
);
|
||||
|
||||
await loadAutomodInfo(serverInfo);
|
||||
}, [maxMsg, timeframe, action, message, channels, channelsChanged]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setMaxMsg(props.rule.max_msg);
|
||||
setTimeframe(props.rule.timeframe);
|
||||
setAction(props.rule.action);
|
||||
setMessage(props.rule.message || '');
|
||||
setChannels(props.rule.channels ?? []);
|
||||
setChannelsChanged(false);
|
||||
}, []);
|
||||
|
||||
const remove = useCallback(async () => {
|
||||
if (confirm(`Do you want to irreversably delete rule ${props.rule.id}?`)) {
|
||||
await axios.delete(`${API_URL}/dash/server/${serverid}/automod/${props.rule.id}`, { headers: await getAuthHeaders() });
|
||||
setAutomodSettings({ antispam: automodSettings!.antispam.filter(r => r.id != props.rule.id) });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
maxWidth: '100px',
|
||||
margin: '8px 8px 0px 8px',
|
||||
}
|
||||
|
||||
const messagePlaceholders = {
|
||||
0: '',
|
||||
1: 'Message content...',
|
||||
2: '(Optional) Warn reason...',
|
||||
3: '',
|
||||
4: '',
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--foreground)',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
If user sends more than
|
||||
<input style={inputStyle} value={maxMsg || ''} placeholder={`${props.rule.max_msg}`} onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
if (!isNaN(Number(val)) && val.length <= 4 && Number(val) >= 0) setMaxMsg(Number(val));
|
||||
}} />
|
||||
messages in
|
||||
<input style={inputStyle} value={timeframe || ''} placeholder={`${props.rule.timeframe}`} onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
if (!isNaN(Number(val)) && val.length <= 4 && Number(val) >= 0) setTimeframe(Number(val));
|
||||
}} />
|
||||
seconds,
|
||||
<select
|
||||
style={{ ...inputStyle, maxWidth: '200px' }}
|
||||
value={action}
|
||||
onChange={ev => setAction(ev.currentTarget.value as any)}
|
||||
>
|
||||
<option value={0}>Delete message</option>
|
||||
<option value={1}>Send a message</option>
|
||||
<option value={2}>Warn user</option>
|
||||
<option value={3}>Kick user</option>
|
||||
<option value={4}>Ban user</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
style={{
|
||||
...inputStyle,
|
||||
maxWidth: 'min(400px, calc(100% - 20px))',
|
||||
display: action >= 3 || action == 0 ? 'none' : 'unset' }}
|
||||
value={message}
|
||||
placeholder={messagePlaceholders[action] || ''}
|
||||
onChange={ev => setMessage(ev.currentTarget.value)}
|
||||
/>
|
||||
<a style={{ display: action >= 3 ? 'unset' : 'none'}}>
|
||||
<br/>
|
||||
"Kick" and "Ban" actions are currently placeholders, they do not have any functionality yet.
|
||||
</a>
|
||||
|
||||
<h4 style={{ paddingTop: '16px' }}>
|
||||
You can specify channels here that this rule will run in.
|
||||
If left empty, it will run in all channels.
|
||||
</h4>
|
||||
<UserListTypeContainer>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||
{
|
||||
channels.map(cid => {
|
||||
const channel: Channel = serverInfo.channels.find(c => c.id == cid && c.type == 'TEXT')
|
||||
|| { id: cid, name: 'Unknown channel', nsfw: false, type: 'TEXT' };
|
||||
return (
|
||||
<div
|
||||
key={cid}
|
||||
style={{
|
||||
display: 'block',
|
||||
margin: '4px 6px',
|
||||
padding: '4px',
|
||||
backgroundColor: 'var(--tertiary-background)',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={channel.icon ?? defaultChannelIcon}
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '10%',
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
verticalAlign: 'middle',
|
||||
marginLeft: '4px',
|
||||
}}
|
||||
>{channel.name}</span>
|
||||
<RemoveButton onClick={() => {
|
||||
setChannels(channels.filter(c => c != cid));
|
||||
setChannelsChanged(true);
|
||||
}} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
<ChannelListAddField onInput={channel => {
|
||||
if (!channels.includes(channel.id)) {
|
||||
setChannels([ ...channels, channel.id ]);
|
||||
setChannelsChanged(true);
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
</UserListTypeContainer>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: '16px'
|
||||
}}
|
||||
>
|
||||
<button style={{ float: 'left' }} onClick={save}>Save</button>
|
||||
<button style={{ float: 'left', marginLeft: '8px' }} onClick={reset}>Reset</button>
|
||||
<button style={{ float: 'left', marginLeft: '8px' }} onClick={remove}>Delete</button>
|
||||
<code
|
||||
style={{
|
||||
float: 'left',
|
||||
color: 'var(--secondary-foreground)',
|
||||
marginTop: '10px',
|
||||
paddingLeft: '12px',
|
||||
}}
|
||||
>
|
||||
{props.rule.id}
|
||||
</code>
|
||||
<div style={{ clear: 'both' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default ServerDashboard;
|
|
@ -1,30 +0,0 @@
|
|||
import { FunctionComponent, useMemo } from "react";
|
||||
import katex from "katex";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
const TexPage: FunctionComponent = () => {
|
||||
const tex = new URLSearchParams(useLocation().search).get("tex");
|
||||
const html = useMemo(() => katex.renderToString(tex ?? ""), [tex]);
|
||||
|
||||
return tex ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
marginTop: "32px",
|
||||
fontSize: "24px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ color: "var(--foreground)" }}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h3 style={{ color: "var(--foreground)" }}>No input TeX provided</h3>
|
||||
);
|
||||
};
|
||||
|
||||
export default TexPage;
|
|
@ -1,30 +0,0 @@
|
|||
import axios from "axios";
|
||||
import localforage from "localforage";
|
||||
import { API_URL } from "./App";
|
||||
|
||||
async function getAuthHeaders() {
|
||||
const auth: any = await localforage.getItem('auth');
|
||||
return {
|
||||
'x-auth-user': auth.user,
|
||||
'x-auth-token': auth.token,
|
||||
}
|
||||
}
|
||||
|
||||
async function getAuth(): Promise<false|{ user: string, token: string }> {
|
||||
const auth: any = await localforage.getItem('auth');
|
||||
if (!auth) return false;
|
||||
|
||||
try {
|
||||
const res = await axios.get(API_URL, {
|
||||
headers: {
|
||||
'x-auth-user': auth.user,
|
||||
'x-auth-token': auth.token,
|
||||
}
|
||||
});
|
||||
|
||||
if (res.data?.authenticated) return { user: auth.user ?? '', token: auth.token ?? '' }
|
||||
else return false;
|
||||
} catch(e) { return false } // todo: dont assume we're logged out if death
|
||||
}
|
||||
|
||||
export { getAuth, getAuthHeaders }
|
1
web/src/vite-env.d.ts
vendored
1
web/src/vite-env.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Some stricter flags (enabled)
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Additional improvements
|
||||
"useDefineForClassFields": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
publicDir: 'public',
|
||||
})
|
2418
web/yarn.lock
2418
web/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue