Compare commits
73 Commits
Author | SHA1 | Date | |
---|---|---|---|
b27af9c035 | |||
25db5c73fb | |||
92b097fcca | |||
23b4247de3 | |||
2806cafe82 | |||
237b5686e4 | |||
8586090732 | |||
a65fd5ddb2 | |||
d246e01dd7 | |||
e032c51185 | |||
9fc386a973 | |||
86e380ec51 | |||
411c9e0597 | |||
48636926b2 | |||
0d74aaeffc | |||
6fb96a49ad | |||
a6dafe871b | |||
c05d578116 | |||
3590f8e9ce | |||
ccbf4c538e | |||
74142fb48e | |||
b66c6f173d | |||
7434cd1d09 | |||
08a286db13 | |||
a146a4793c | |||
f590936b2c | |||
6a68f81e59 | |||
7ba12db083 | |||
443b1cb223 | |||
c53435027c | |||
f262d821e0 | |||
ae53457344 | |||
dede2ee06e | |||
0776130d37 | |||
f0a431b93e | |||
51b923b101 | |||
ca38ecbf57 | |||
88680f58a4 | |||
2a3ebd5b24 | |||
5dafa6955f | |||
e007272efa | |||
1319d147a5 | |||
7ce25d324d | |||
31ce135b3a | |||
4577bb72b9 | |||
501c15207b | |||
56ecdf03a6 | |||
cc2e19e166 | |||
1a0b4af712 | |||
d276281ce9 | |||
3bbfed025d | |||
0a26a8b4a8 | |||
efdda2e18b | |||
58c2eb9ac6 | |||
118c6f7991 | |||
cdaf525512 | |||
e188869811 | |||
60f77a2691 | |||
ca470154d4 | |||
6fe3ce4b34 | |||
5dbb6620c7 | |||
fc424ddec7 | |||
ac16048dd5 | |||
0a4298f49b | |||
de2c3c960f | |||
56f91ba239 | |||
ff6f445703 | |||
24b80fcbdc | |||
0a402ea21a | |||
2b13681334 | |||
e15664e39a | |||
b3974987a1 | |||
e98c36b270 |
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,7 +1,7 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
venv/
|
||||
.env
|
||||
*.log
|
||||
*.log.*
|
||||
.vscode/
|
||||
.vscode/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
dist/
|
||||
*.sqlite
|
||||
tailwind.css
|
25
bot/bot.py
25
bot/bot.py
@ -1,25 +0,0 @@
|
||||
import logging
|
||||
|
||||
from discord import Intents
|
||||
from discord.ext import commands
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DiscordBot(commands.Bot):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
command_prefix="@",
|
||||
intents=Intents.all()
|
||||
)
|
||||
|
||||
async def on_ready(self):
|
||||
await self.wait_until_ready()
|
||||
await self.tree.sync()
|
||||
log.info("Bot is synced and ready")
|
||||
|
||||
async def load_cogs(self, cog_path: str):
|
||||
log.info("Loading cogs")
|
||||
for path in cog_path.iterdir():
|
||||
if path.suffix == ".py":
|
||||
await self.load_extension(f"cogs.{path.stem}")
|
@ -1,51 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"simple": {
|
||||
"format": "%(levelname)s %(message)s"
|
||||
},
|
||||
"detail": {
|
||||
"format": "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s"
|
||||
},
|
||||
"complex": {
|
||||
"format": "[%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s %(message)s",
|
||||
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"stdout": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stdout"
|
||||
},
|
||||
"file": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "complex",
|
||||
"filename": "logs/pyrss.log",
|
||||
"maxBytes": 1048576,
|
||||
"backupCount": 3
|
||||
},
|
||||
"queue_handler": {
|
||||
"class": "logging.handlers.QueueHandler",
|
||||
"handlers": [
|
||||
"stdout",
|
||||
"file"
|
||||
],
|
||||
"respect_handler_level": true
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"root": {
|
||||
"level": "DEBUG",
|
||||
"handlers": [
|
||||
"queue_handler"
|
||||
]
|
||||
},
|
||||
"discord": {
|
||||
"level": "INFO"
|
||||
}
|
||||
}
|
||||
}
|
53
main.py
53
main.py
@ -1,53 +0,0 @@
|
||||
import json
|
||||
import atexit
|
||||
import asyncio
|
||||
import logging
|
||||
import logging.config
|
||||
from os import getenv
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(override=True)
|
||||
|
||||
from bot.bot import DiscordBot
|
||||
from web.app import create_app
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
|
||||
async def start_web():
|
||||
app = create_app()
|
||||
await app.run_task(host="0.0.0.0", port=5000)
|
||||
|
||||
async def start_bot(token: str):
|
||||
async with DiscordBot() as bot:
|
||||
await bot.load_cogs(BASE_DIR / "bot" / "cogs")
|
||||
await bot.start(token, reconnect=True)
|
||||
|
||||
def setup_logging():
|
||||
# load config from file
|
||||
log_config_path = BASE_DIR / "logs" / "config.json"
|
||||
if not log_config_path.exists():
|
||||
raise FileNotFoundError("Logging config not found")
|
||||
|
||||
with open(log_config_path, "r") as file:
|
||||
logging_config = json.load(file)
|
||||
|
||||
logging.config.dictConfig(logging_config)
|
||||
|
||||
# create queue handler for non-blocking logs
|
||||
queue_handler = logging.getHandlerByName("queue_handler")
|
||||
if queue_handler is not None:
|
||||
queue_handler.listener.start()
|
||||
atexit.register(queue_handler.listener.stop)
|
||||
|
||||
async def main():
|
||||
bot_token = getenv("BOT_TOKEN")
|
||||
if not bot_token:
|
||||
raise ValueError("'BOT_TOKEN' is missing")
|
||||
|
||||
setup_logging()
|
||||
|
||||
await asyncio.gather(start_web(), start_bot(bot_token))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
62
package.json
Normal file
62
package.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "pyrss-ng",
|
||||
"version": "0.0.0",
|
||||
"main": "src/app.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"tailwind": "npx @tailwindcss/cli -i ./src/client/public/css/main.css -o ./src/client/public/css/tailwind.css",
|
||||
"build:server": "./scripts/build.sh",
|
||||
"build:client": "npx tsc --project ./tsconfig.client.json",
|
||||
"build": "npm run tailwind && npm run build:client && npm run build:server",
|
||||
"dev": "cross-env TS_NODE_PROJECT=tsconfig.server.json nodemon -r tsconfig-paths/register --exec 'npm run build:client && ts-node' ./src/app.ts",
|
||||
"start": "node dist/app.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.cor.bz/corbz/pyrss-ng.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@preline/datatable": "^2.5.2",
|
||||
"@tailwindcss/cli": "^4.0.9",
|
||||
"canvas": "^3.1.0",
|
||||
"connect-flash": "^0.1.1",
|
||||
"datatables.net-dt": "^2.2.1",
|
||||
"datatables.net-select-dt": "^3.0.0",
|
||||
"discord.js": "^14.17.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"ejs": "^3.1.10",
|
||||
"ejs-mate": "^4.0.0",
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.1",
|
||||
"jquery": "^3.7.1",
|
||||
"knex": "^3.1.0",
|
||||
"ncp": "^2.0.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-discord": "^0.1.4",
|
||||
"preline": "^3.0.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@types/connect-flash": "^0.0.40",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/express-session": "^1.18.1",
|
||||
"@types/jquery": "^3.5.32",
|
||||
"@types/node": "^22.10.10",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/passport-discord": "^0.1.14",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
"nodemon": "^3.1.9",
|
||||
"tailwindcss": "^4.0.9",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
aiofiles==24.1.0
|
||||
aiohappyeyeballs==2.4.4
|
||||
aiohttp==3.11.11
|
||||
aiosignal==1.3.2
|
||||
attrs==24.3.0
|
||||
blinker==1.9.0
|
||||
click==8.1.8
|
||||
discord.py==2.4.0
|
||||
Flask==3.1.0
|
||||
frozenlist==1.5.0
|
||||
h11==0.14.0
|
||||
h2==4.1.0
|
||||
hpack==4.0.0
|
||||
Hypercorn==0.17.3
|
||||
hyperframe==6.0.1
|
||||
idna==3.10
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.5
|
||||
MarkupSafe==3.0.2
|
||||
multidict==6.1.0
|
||||
priority==2.0.0
|
||||
propcache==0.2.1
|
||||
python-dotenv==1.0.1
|
||||
Quart==0.20.0
|
||||
Werkzeug==3.1.3
|
||||
wsproto==1.2.0
|
||||
yarl==1.18.3
|
18
scripts/build.sh
Executable file
18
scripts/build.sh
Executable file
@ -0,0 +1,18 @@
|
||||
cd "$(dirname "$0")/../"
|
||||
|
||||
echo "Erasing previous dist folder"
|
||||
rm -rf ./dist
|
||||
|
||||
echo "Compiling tailwind css ..."
|
||||
npx tailwindcss -i ./src/client/public/css/main.css -o ./src/client/public/css/tailwind.css
|
||||
|
||||
echo "Compiling typescript ..."
|
||||
npx tsc --project ./tsconfig.server.json
|
||||
|
||||
echo "Copying client files"
|
||||
cp -r src/client dist/client
|
||||
|
||||
echo "Building typescript path aliases ..."
|
||||
npx tsc-alias -p ./tsconfig.server.json
|
||||
|
||||
echo "Done!"
|
2
scripts/migrate.sh
Executable file
2
scripts/migrate.sh
Executable file
@ -0,0 +1,2 @@
|
||||
cd "$(dirname "$0")/../"
|
||||
npx knex migrate:latest --knexfile ./src/knexfile.ts
|
2
scripts/seeds.sh
Executable file
2
scripts/seeds.sh
Executable file
@ -0,0 +1,2 @@
|
||||
cd "$(dirname "$0")/../"
|
||||
npx knex seed:run --knexfile ./src/knexfile.ts
|
62
src/app.ts
Normal file
62
src/app.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import path from "path";
|
||||
import express from "express";
|
||||
import engine from "ejs-mate";
|
||||
import passport from "passport";
|
||||
import session from "express-session";
|
||||
import flash from "connect-flash";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import "@bot/bot";
|
||||
import { setupPassport } from "@server/controllers/auth.web.controller";
|
||||
import { attachUser } from "@server/middleware/attachUser";
|
||||
import { flashMiddleware } from "@server/middleware/flash";
|
||||
import { ensureAuthenticated } from "@server/middleware/authenticated";
|
||||
|
||||
// import routers & middleware
|
||||
import { attachGuilds } from "@server/middleware/attachGuilds";
|
||||
import homeWebRouter from "@server/routes/home.web.routes";
|
||||
import guildApiRouter from "@server/routes/guild.api.routes";
|
||||
import guildWebRouter from "@server/routes/guild.web.routes";
|
||||
import authWebRouter from "@server/routes/auth.web.routes";
|
||||
|
||||
const app = express();
|
||||
|
||||
app.engine("ejs", engine);
|
||||
app.set("views", path.resolve(__dirname, "client/views"));
|
||||
app.set("view engine", "ejs");
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Public files, including 3rd party resources (foreign)
|
||||
app.use("/static", express.static(path.resolve(__dirname, "client/public")));
|
||||
app.use("/static/foreign/preline.js", express.static(path.resolve(__dirname, "../node_modules/preline/dist/preline.js")));
|
||||
app.use("/static/foreign/jquery.js", express.static(path.resolve(__dirname, "../node_modules/jquery/dist/jquery.min.js")));
|
||||
app.use("/static/foreign/dataTables.js", express.static(path.resolve(__dirname, "../node_modules/datatables.net/js/dataTables.min.js")));
|
||||
app.use("/static/foreign/dataTablesSelect.js", express.static(path.resolve(__dirname, "../node_modules/datatables.net-select/js/dataTables.select.min.js")));
|
||||
app.use("/static/foreign/floatingUiCore.js", express.static(path.resolve(__dirname, "../node_modules/@floating-ui/core/dist/floating-ui.core.umd.min.js")));
|
||||
app.use("/static/foreign/floatingUiDOM.js", express.static(path.resolve(__dirname, "../node_modules/@floating-ui/dom/dist/floating-ui.dom.umd.min.js")));
|
||||
|
||||
// User authentication & sessions
|
||||
app.use(session({
|
||||
secret: "unsecure-development-secret",
|
||||
resave: true,
|
||||
saveUninitialized: true
|
||||
}));
|
||||
|
||||
setupPassport(passport);
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.use(flash());
|
||||
app.use(flashMiddleware);
|
||||
|
||||
// register routers & middleware
|
||||
app.use("/auth", authWebRouter);
|
||||
app.use("/guild", ensureAuthenticated, attachUser, attachGuilds, guildWebRouter, guildApiRouter);
|
||||
app.use("/", ensureAuthenticated, attachUser, attachGuilds, homeWebRouter);
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is listening on http://localhost:${PORT}`);
|
||||
});
|
21
src/bot/bot.ts
Normal file
21
src/bot/bot.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// const { Client, GatewayIntentBits } = require("discord.js");
|
||||
import { Client, GatewayIntentBits, ActivityType } from "discord.js";
|
||||
|
||||
export const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildWebhooks
|
||||
]
|
||||
});
|
||||
|
||||
client.on("ready", () => {
|
||||
if (!client.user) {
|
||||
throw Error("Client is null");
|
||||
}
|
||||
|
||||
client.user.setActivity("new sources", { type: ActivityType.Watching });
|
||||
console.log(`Discord Bot '${client.user.displayName}' is online!`)
|
||||
});
|
||||
|
||||
client.login(process.env.BOT_TOKEN);
|
272
src/client/public/css/main.css
Normal file
272
src/client/public/css/main.css
Normal file
@ -0,0 +1,272 @@
|
||||
@import 'tailwindcss';
|
||||
@import "preline/variants.css";
|
||||
|
||||
@config '../../../../tailwind.config.js';
|
||||
|
||||
/*
|
||||
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||
so we've added these compatibility styles to make sure everything still
|
||||
looks the same as it did with Tailwind CSS v3.
|
||||
|
||||
If we ever want to remove these styles, we need to add an explicit border
|
||||
color utility to any element that depends on these defaults.
|
||||
*/
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
}
|
||||
|
||||
/* flex items-center gap-x-3.5 py-2 px-2.5 bg-gray-100 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:bg-neutral-700 dark:text-white
|
||||
hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:text-neutral-200 */
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 200, 700;
|
||||
font-display: swap;
|
||||
src: url("/static/fonts/inter-variablefont.ttf");
|
||||
}
|
||||
|
||||
/* Datatables */
|
||||
|
||||
.dt-layout-row:has(.dt-search),
|
||||
.dt-layout-row:has(.dt-length),
|
||||
.dt-layout-row:has(.dt-paging) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* End Datatables */
|
||||
|
||||
/* Sidebar Components */
|
||||
|
||||
.mobile-sidebar-header {
|
||||
@apply
|
||||
sticky
|
||||
top-0
|
||||
inset-x-0
|
||||
z-20
|
||||
lg:hidden;
|
||||
}
|
||||
|
||||
.mobile-sidebar-header .mobile-sidebar-nav {
|
||||
@apply
|
||||
flex
|
||||
p-4
|
||||
sm:px-6
|
||||
border-b
|
||||
bg-white
|
||||
dark:bg-neutral-800
|
||||
dark:border-neutral-700;
|
||||
}
|
||||
|
||||
.mobile-sidebar-header .mobile-sidebar-nav .mobile-sidebar-brand {
|
||||
@apply text-xl font-semibold;
|
||||
}
|
||||
|
||||
.sidebar-aside {
|
||||
@apply
|
||||
/* Position and Display */
|
||||
transform
|
||||
w-64
|
||||
h-full
|
||||
fixed
|
||||
top-0
|
||||
bottom-0
|
||||
|
||||
/* Colours and Border */
|
||||
bg-white
|
||||
border-e
|
||||
border-gray-200
|
||||
dark:bg-neutral-800
|
||||
dark:border-neutral-700;
|
||||
}
|
||||
|
||||
.sidebar-aside.main-sidebar {
|
||||
@apply
|
||||
[--auto-close:lg]
|
||||
lg:block
|
||||
lg:translate-x-0
|
||||
lg:end-auto
|
||||
lg:bottom-0
|
||||
-translate-x-full
|
||||
transition-all
|
||||
duration-200
|
||||
ease-in
|
||||
start-0
|
||||
z-60
|
||||
}
|
||||
|
||||
.sidebar-aside.guild-sidebar {
|
||||
@apply
|
||||
[--auto-close:lg]
|
||||
lg:block
|
||||
lg:end-auto
|
||||
lg:bottom-0
|
||||
-translate-x-full
|
||||
transition-all
|
||||
duration-200
|
||||
lg:duration-100
|
||||
ease-in
|
||||
start-0
|
||||
lg:start-64
|
||||
z-59
|
||||
}
|
||||
|
||||
.sidebar-aside .sidebar-container {
|
||||
@apply
|
||||
relative
|
||||
flex
|
||||
flex-col
|
||||
h-full
|
||||
max-h-full;
|
||||
}
|
||||
|
||||
.sidebar-aside .sidebar-container .sidebar-header {
|
||||
@apply
|
||||
px-6
|
||||
pt-4
|
||||
flex
|
||||
justify-between
|
||||
items-center;
|
||||
}
|
||||
|
||||
.sidebar-aside .sidebar-container .sidebar-header .sidebar-brand {
|
||||
@apply
|
||||
flex-none
|
||||
rounded-xl
|
||||
text-xl
|
||||
inline-block
|
||||
font-semibold
|
||||
focus:outline-hidden
|
||||
focus:opacity-80;
|
||||
}
|
||||
|
||||
.sidebar-aside .sidebar-container .sidebar-content {
|
||||
@apply
|
||||
h-full
|
||||
overflow-y-auto
|
||||
[&::-webkit-scrollbar]:w-2
|
||||
[&::-webkit-scrollbar-thumb]:rounded-full
|
||||
[&::-webkit-scrollbar-track]:bg-gray-100
|
||||
[&::-webkit-scrollbar-thumb]:bg-gray-300
|
||||
dark:[&::-webkit-scrollbar-track]:bg-neutral-700
|
||||
dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500
|
||||
}
|
||||
|
||||
.sidebar-aside .sidebar-container .sidebar-content .sidebar-nav {
|
||||
@apply
|
||||
px-4
|
||||
py-6
|
||||
w-full
|
||||
h-full
|
||||
flex
|
||||
flex-col
|
||||
flex-wrap;
|
||||
}
|
||||
|
||||
.sidebar-nav .sidebar-btn {
|
||||
@apply
|
||||
w-full
|
||||
flex
|
||||
items-center
|
||||
gap-x-3.5
|
||||
py-2
|
||||
px-2.5
|
||||
text-sm
|
||||
rounded-lg
|
||||
focus:outline-hidden
|
||||
text-gray-800
|
||||
hover:bg-gray-100
|
||||
focus:bg-gray-100
|
||||
dark:bg-neutral-800
|
||||
dark:hover:bg-neutral-700
|
||||
dark:focus:bg-neutral-700
|
||||
dark:text-neutral-200;
|
||||
}
|
||||
|
||||
/* End Sidebar Components */
|
||||
|
||||
/* Guild Header */
|
||||
|
||||
.guild-header-btn {
|
||||
@apply
|
||||
text-sm
|
||||
text-gray-800
|
||||
hover:text-blue-600
|
||||
focus:outline-hidden
|
||||
focus:text-blue-600
|
||||
dark:text-neutral-200
|
||||
dark:hover:text-blue-500
|
||||
dark:focus:text-blue-500;
|
||||
}
|
||||
|
||||
.guild-header-btn.active {
|
||||
@apply text-blue-600;
|
||||
}
|
||||
|
||||
/* End Guild Header */
|
||||
|
||||
/* Form Controls */
|
||||
|
||||
.text-input-label {
|
||||
@apply block text-sm font-medium mb-2 dark:text-white
|
||||
}
|
||||
|
||||
.text-input {
|
||||
@apply
|
||||
py-3
|
||||
px-4
|
||||
block
|
||||
w-full
|
||||
rounded-lg
|
||||
text-sm
|
||||
disabled:opacity-50
|
||||
disabled:pointer-events-none
|
||||
border-gray-200
|
||||
focus:border-blue-500
|
||||
focus:ring-blue-500
|
||||
dark:bg-neutral-900
|
||||
dark:border-neutral-700
|
||||
dark:text-neutral-400
|
||||
dark:placeholder-neutral-500
|
||||
dark:focus:ring-neutral-600
|
||||
}
|
||||
|
||||
.text-input-help {
|
||||
@apply mt-2 text-sm text-gray-500 dark:text-neutral-500
|
||||
}
|
||||
|
||||
.select-input {
|
||||
@apply
|
||||
relative
|
||||
py-3
|
||||
ps-4
|
||||
pe-9
|
||||
flex
|
||||
gap-x-2
|
||||
text-nowrap
|
||||
w-full
|
||||
cursor-pointer
|
||||
bg-white
|
||||
border
|
||||
border-gray-200
|
||||
rounded-lg
|
||||
text-start
|
||||
text-sm
|
||||
focus:outline-hidden
|
||||
focus:ring-2
|
||||
focus:ring-blue-500
|
||||
dark:bg-neutral-900
|
||||
dark:border-neutral-700
|
||||
dark:text-neutral-400
|
||||
dark:focus:outline-hidden
|
||||
dark:focus:ring-1
|
||||
dark:focus:ring-neutral-600
|
||||
}
|
||||
|
||||
/* End Form Controls */
|
3138
src/client/public/css/tailwind.css
Normal file
3138
src/client/public/css/tailwind.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/client/public/fonts/inter-variablefont.ttf
Normal file
BIN
src/client/public/fonts/inter-variablefont.ttf
Normal file
Binary file not shown.
BIN
src/client/public/images/pyrss_logo.webp
Normal file
BIN
src/client/public/images/pyrss_logo.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
286
src/client/public/js/guild/filters.js
Normal file
286
src/client/public/js/guild/filters.js
Normal file
@ -0,0 +1,286 @@
|
||||
|
||||
const emptyTableHtml = `
|
||||
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
||||
<div class="flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
||||
<svg class="shrink-0 size-6 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
|
||||
</div>
|
||||
<h2 class="mt-5 font-semibold text-gray-800 dark:text-white">
|
||||
No results found
|
||||
</h2>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
||||
Create a filter and it will appear here.
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
||||
<button type="button" class="openFilterModal-js py-2 px-3 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none" data-hs-overlay="#filterModal">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
Create a filter
|
||||
</button>
|
||||
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||
Use a Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
var table;
|
||||
const defineTable = () => {
|
||||
table = new HSDataTable("#table", {
|
||||
ajax: {
|
||||
url: `/guild/${guildId}/filters/api/datatable`,
|
||||
dataSrc: "data",
|
||||
data: (d) => {
|
||||
if (d === undefined) { return ;}
|
||||
|
||||
d.filters = {};
|
||||
const is_whitelist = $("input[name='filterType']:checked").val();
|
||||
d.filters.is_whitelist = is_whitelist;
|
||||
}
|
||||
},
|
||||
serverSide: true,
|
||||
processing: true,
|
||||
selecting: true,
|
||||
pagingOptions: {
|
||||
pageBtnClasses: "hidden"
|
||||
},
|
||||
rowSelectingOptions: {
|
||||
selectAllSelector: "#selectAllBox"
|
||||
},
|
||||
language: {
|
||||
zeroRecords: emptyTableHtml,
|
||||
emptyTable: emptyTableHtml,
|
||||
loading: "Placeholder Loading Message...",
|
||||
},
|
||||
rowCallback: (row, data, index) => {
|
||||
$(row).addClass("bg-white dark:bg-neutral-900");
|
||||
},
|
||||
drawCallback: () => {
|
||||
HSDropdown.autoInit();
|
||||
},
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:first-child input[type='checkbox']"
|
||||
},
|
||||
columnDefs: [
|
||||
{
|
||||
// Row select checkbox
|
||||
targets: 0,
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: (data, type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap">
|
||||
<div class="ps-6 py-4">
|
||||
<label for="rowSelect${row.id}-js" class="flex">
|
||||
<input type="checkbox" id="rowSelect${row.id}-js" class="form-checkbox shrink-0 border-gray-300 rounded-sm text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" data-hs-datatable-row-selecting-individual="">
|
||||
<span class="sr-only">Checkbox</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Name
|
||||
targets: 1,
|
||||
data: "name",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: (data, _type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<span class="openFilterModal-js block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap cursor-pointer" data-id="${row.id}">
|
||||
${data}
|
||||
</sp>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Match
|
||||
targets: 2,
|
||||
data: "match",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<a href="#" class="block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap">
|
||||
${data}
|
||||
</a>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Algorithm
|
||||
target: 3,
|
||||
data: "algorithm",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: (data, type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<a href="#" class="block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap">
|
||||
${data}
|
||||
</a>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Filters
|
||||
target: 4,
|
||||
data: "is_insensitive",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
wrapper = $("<div>").addClass("px-6 py-4");
|
||||
badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center text-xs font-medium rounded-full");
|
||||
label = $("<span>");
|
||||
|
||||
if (data) {
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:bg-red-500");
|
||||
badge.append(label.text("No"));
|
||||
} else {
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
badge.append(label.text("Yes"));
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Whitelist
|
||||
target: 5,
|
||||
data: "is_whitelist",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
wrapper = $("<div>").addClass("px-6 py-4");
|
||||
badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center text-xs font-medium rounded-full");
|
||||
label = $("<span>");
|
||||
|
||||
if (data) {
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
badge.append(label.text("Whitelist"));
|
||||
} else {
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:bg-red-500");
|
||||
badge.append(label.text("Blacklist"));
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Created At
|
||||
target: 6,
|
||||
data: "created_at",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<div class="px-6 py-4">
|
||||
<span class="text-sm text-gray-500 dark:text-neutral-500 text-nowrap">
|
||||
${formatTimestamp(data)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
table.dataTable
|
||||
.on("select", onTableSelectChange)
|
||||
.on("deselect", onTableSelectChange)
|
||||
.on("draw", onTableSelectChange);
|
||||
}
|
||||
|
||||
// Ensure the datatable recognises when all rows are selected, otherwise rows are only visually selected
|
||||
$("#selectAllBox").on("change", function() {
|
||||
this.checked ? table.dataTable.rows().select() : table.dataTable.rows().deselect();
|
||||
});
|
||||
|
||||
const onTableSelectChange = () => {
|
||||
const selectedRowCount = table.dataTable.rows({ selected: true }).count();
|
||||
$("#deleteRowsBtn").prop("disabled", selectedRowCount === 0);
|
||||
$(".rows-selected-count-js").text(selectedRowCount);
|
||||
|
||||
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
||||
selectedRowCount === 0 ? $elem.hide() : $elem.show();
|
||||
}
|
||||
|
||||
$(window).ready(() => {
|
||||
setTimeout(defineTable, 500);
|
||||
});
|
||||
|
||||
$("input[name='filterType']").on("change", () => {
|
||||
table.dataTable.draw();
|
||||
});
|
||||
|
||||
const openFilterForm = async id => {
|
||||
$("#filterForm").removeClass("submitted");
|
||||
|
||||
const formAlgorithmSelect = HSSelect.getInstance("#formAlgorithm");
|
||||
formAlgorithmSelect.setValue("");
|
||||
|
||||
if (id === -1) {
|
||||
$("#formName").val("");
|
||||
$("#formMatch").val("");
|
||||
$("#formWhitelist").prop("checked", false);
|
||||
$("#formInsensitive").prop("checked", false);
|
||||
} else {
|
||||
const data = await $.ajax({
|
||||
url: `/guild/${guildId}/filters/api?id=${id}`,
|
||||
method: "get"
|
||||
});
|
||||
|
||||
$("#formName").val(data.name);
|
||||
$("#formMatch").val(data.match);
|
||||
$("#formWhitelist").prop("checked", data.is_whitelist);
|
||||
$("#formInsensitive").prop("checked", data.is_insensitive);
|
||||
formAlgorithmSelect.setValue(data.algorithm);
|
||||
}
|
||||
|
||||
HSOverlay.open($("#filterModal").get(0));
|
||||
}
|
||||
|
||||
const closeFilterForm = () => {
|
||||
$("#filterForm").removeClass("submitted");
|
||||
HSOverlay.close($("#filterModal").get(0));
|
||||
}
|
||||
|
||||
$(document).on("click", ".openFilterModal-js", event => {
|
||||
openFilterForm($(event.target).data("id") || -1);
|
||||
});
|
||||
|
||||
const submitForm = async event => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = $(event.target).get(0);
|
||||
$(form).addClass("submitted");
|
||||
|
||||
if (!form.checkValidity()) { return; }
|
||||
|
||||
await $.ajax({
|
||||
url: `/guild/${guildId}/filters/api`,
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
data: $(event.target).serializeArray(),
|
||||
success: () => {
|
||||
table.dataTable.draw();
|
||||
closeFilterForm();
|
||||
},
|
||||
error: error => {
|
||||
alert(JSON.stringify(error, null, 4));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#filterForm").on("submit", submitForm);
|
359
src/client/public/js/guild/subscriptions.js
Normal file
359
src/client/public/js/guild/subscriptions.js
Normal file
@ -0,0 +1,359 @@
|
||||
|
||||
//#region Table
|
||||
|
||||
const emptyTableHtml = `
|
||||
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
||||
<div class="flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
||||
<svg class="shrink-0 size-6 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
|
||||
</div>
|
||||
<h2 class="mt-5 font-semibold text-gray-800 dark:text-white">
|
||||
No results found
|
||||
</h2>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
||||
Create a subscription and it will appear here.
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
||||
<button type="button" class="openSubModal-js py-2 px-3 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none" data-hs-overlay="#subModal">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
Create a subscription
|
||||
</button>
|
||||
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||
Use a Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
var table;
|
||||
const defineTable = () => {
|
||||
table = new HSDataTable("#table", {
|
||||
ajax: {
|
||||
url: `/guild/${guildId}/subscriptions/api/datatable`,
|
||||
dataSrc: "data",
|
||||
data: (d) => {
|
||||
if (d === undefined) { return ;}
|
||||
|
||||
d.filters = {};
|
||||
const active = $("input[name='filterActive']:checked").val();
|
||||
d.filters.active = active;
|
||||
}
|
||||
},
|
||||
serverSide: true,
|
||||
processing: true,
|
||||
selecting: true,
|
||||
pagingOptions: {
|
||||
pageBtnClasses: "hidden"
|
||||
},
|
||||
rowSelectingOptions: {
|
||||
selectAllSelector: "#selectAllBox"
|
||||
},
|
||||
language: {
|
||||
zeroRecords: emptyTableHtml,
|
||||
emptyTable: emptyTableHtml,
|
||||
loading: "Placeholder Loading Message...",
|
||||
},
|
||||
rowCallback: (row, data, index) => {
|
||||
$(row).addClass("bg-white dark:bg-neutral-900");
|
||||
},
|
||||
drawCallback: () => {
|
||||
HSDropdown.autoInit();
|
||||
},
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:first-child input[type='checkbox']"
|
||||
},
|
||||
columnDefs: [
|
||||
{
|
||||
// Row select checkbox
|
||||
targets: 0,
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: (data, type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap">
|
||||
<div class="ps-6 py-4">
|
||||
<label for="rowSelect${row.id}-js" class="flex">
|
||||
<input type="checkbox" id="rowSelect${row.id}-js" class="form-checkbox shrink-0 border-gray-300 rounded-sm text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" data-hs-datatable-row-selecting-individual="">
|
||||
<span class="sr-only">Checkbox</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Name
|
||||
targets: 1,
|
||||
data: "name",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: (data, _type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<span class="openSubModal-js block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap cursor-pointer" data-id="${row.id}">
|
||||
${data}
|
||||
</span>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Url
|
||||
targets: 2,
|
||||
data: "url",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<a href="${data}" class="block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap" target="_blank">
|
||||
${data}
|
||||
</a>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Channels
|
||||
target: 3,
|
||||
data: "channels",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: (data, type, row) => {
|
||||
if (type !== "display") { return data; }
|
||||
if (!data.length) { return ""; }
|
||||
|
||||
const wrapper = $("<div>").addClass("flex flex-nowrap gap-1 px-6 py-4");
|
||||
const tag = $("<span>").addClass("inline-flex items-center whitespace-nowrap gap-1 py-1 px-2.5 border text-xs rounded-md bg-white dark:bg-neutral-800 border-gray-200 dark:border-neutral-700 text-gray-800 dark:text-neutral-200");
|
||||
|
||||
const firstChannelName = "# " + channels.find(c => c.id === data[0]).name;
|
||||
wrapper.append(tag.clone().text(firstChannelName));
|
||||
|
||||
// No need to run the dropdown code if there's no more to show
|
||||
if (data.length === 1) {
|
||||
return wrapper.get(0);
|
||||
}
|
||||
else if (data.length <= 2) {
|
||||
const secondChannelName = "# " + channels.find(c => c.id === data[1]).name;
|
||||
wrapper.append(tag.clone().text(secondChannelName));
|
||||
data.shift();
|
||||
return wrapper.get(0);
|
||||
}
|
||||
|
||||
// drop the first element to exclude it from the dropdown
|
||||
data.shift();
|
||||
|
||||
const dropdown = $("<div>").addClass("hs-dropdown inline-block");
|
||||
const dropdownBtn = $("<button>").attr("id", `channelDrop-${row.id}`).attr("type", "button").addClass("cursor-pointer inline-flex items-center gap-1 py-1 px-2.5 border text-xs rounded-md bg-white dark:bg-neutral-800 border-gray-200 dark:border-neutral-700 text-gray-800 dark:text-neutral-200");
|
||||
const dropdownMenu = $("<div>").addClass("hs-dropdown-menu hidden opacity-0 hs-dropdown-open:opacity-100 transition-[opacity,margin] overflow-hidden z-10 w-fit max-w-64 border p-2 rounded-md bg-gray-200 dark:bg-neutral-700 border-gray-300 dark:border-neutral-600");
|
||||
|
||||
dropdown.append(dropdownBtn.text(`+${data.length}`));
|
||||
|
||||
data.forEach(channelId => {
|
||||
channelName = "# " + channels.find(c => c.id === channelId).name;
|
||||
dropdownMenu.append(tag.clone().text(channelName));
|
||||
});
|
||||
|
||||
dropdown.append(dropdownMenu);
|
||||
wrapper.append(dropdown);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Filters
|
||||
target: 4,
|
||||
data: "filters",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: (data, type, row) => {
|
||||
return `
|
||||
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Style
|
||||
target: 5,
|
||||
style: "style",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: (data, type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<div class="px-6 py-4 text-center">
|
||||
<div class="inline-block rounded-md size-5 bg-red-500 mx-auto">
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Created At
|
||||
target: 6,
|
||||
data: "created_at",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<div class="px-6 py-4">
|
||||
<span class="text-sm text-gray-500 dark:text-neutral-500 text-nowrap">
|
||||
${formatTimestamp(data)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Status
|
||||
target: 7,
|
||||
data: "active",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: (_data, _type, row) => {
|
||||
wrapper = $("<div>").addClass("px-6 py-4");
|
||||
badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-full");
|
||||
label = $("<span>");
|
||||
|
||||
if (row.active) {
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
badge.append($('<svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/></svg>'))
|
||||
.append(label.text("Active"));
|
||||
} else {
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||
badge.append($('<svg class="size-2.5" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/></svg>'))
|
||||
.append(label.text("Inactive"));
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
table.dataTable
|
||||
.on("select", onTableSelectChange)
|
||||
.on("deselect", onTableSelectChange)
|
||||
.on("draw", onTableSelectChange);
|
||||
}
|
||||
|
||||
//#endregion Table
|
||||
|
||||
// Ensure the datatable recognises when all rows are selected, otherwise rows are only visually selected
|
||||
$("#selectAllBox").on("change", function() {
|
||||
this.checked ? table.dataTable.rows().select() : table.dataTable.rows().deselect();
|
||||
});
|
||||
|
||||
const onTableSelectChange = () => {
|
||||
const selectedRowCount = table.dataTable.rows({ selected: true }).count();
|
||||
$("#deleteRowsBtn").prop("disabled", selectedRowCount === 0);
|
||||
$(".rows-selected-count-js").text(selectedRowCount);
|
||||
|
||||
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
||||
selectedRowCount === 0 ? $elem.hide() : $elem.show();
|
||||
}
|
||||
|
||||
$("#deleteRowsBtn").on("click", async () => {
|
||||
const rowIds = table.dataTable.rows({ selected: true }).data().toArray().map(row => row.id);
|
||||
console.log(JSON.stringify(rowIds))
|
||||
await $.ajax({
|
||||
url: `/guild/${guildId}/subscriptions/api`,
|
||||
method: "delete",
|
||||
dataType: "json",
|
||||
data: { ids: rowIds },
|
||||
success: () => {
|
||||
table.dataTable.draw();
|
||||
table.dataTable.rows().deselect();
|
||||
},
|
||||
error: error => {
|
||||
alert(typeof error === "object" ? JSON.stringify(error, null, 4) : error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(window).ready(() => {
|
||||
setTimeout(defineTable, 500);
|
||||
});
|
||||
|
||||
$("input[name='filterActive']").on("change", () => {
|
||||
table.dataTable.draw();
|
||||
});
|
||||
|
||||
const openSubForm = async id => {
|
||||
$("#subForm").removeClass("submitted");
|
||||
|
||||
// Always clear the selects to avoid bugs
|
||||
HSSelect.getInstance("#formStyle", true).element.setValue([]);
|
||||
HSSelect.getInstance("#formChannels", true).element.setValue([]);
|
||||
HSSelect.getInstance("#formFilters", true).element.setValue([]);
|
||||
$("#formChannelsInput").css("width", "");
|
||||
$("#formFiltersInput").css("width", "");
|
||||
|
||||
// New Subscription: Clear the form values
|
||||
if (id === -1) {
|
||||
$("#formName").val("");
|
||||
$("#formUrl").val("");
|
||||
$("#formPublishedThreshold").val(new Date().toISOString().slice(0, 16));
|
||||
$("#formActive").prop("checked", true);
|
||||
}
|
||||
// Existing subscription: Load the values
|
||||
else {
|
||||
const data = await $.ajax({
|
||||
url: `/guild/${guildId}/subscriptions/api?id=${id}`,
|
||||
method: "get",
|
||||
});
|
||||
|
||||
$("#formName").val(data.name);
|
||||
$("#formUrl").val(data.url);
|
||||
$("#formPublishedThreshold").val(new Date(data.created_at));
|
||||
|
||||
HSSelect.getInstance("#formStyle", true).element.setValue([]);
|
||||
HSSelect.getInstance("#formChannels", true).element.setValue(data.channels);
|
||||
HSSelect.getInstance("#formFilters", true).element.setValue([]);
|
||||
// $("#formChannelsInput").css("width", "");
|
||||
// $("#formFiltersInput").css("width", "");
|
||||
|
||||
$("#formActive").prop("checked", data.active);
|
||||
}
|
||||
|
||||
|
||||
HSOverlay.open($("#subModal").get(0));
|
||||
}
|
||||
|
||||
const closeSubForm = () => {
|
||||
$("#subForm").removeClass("submitted");
|
||||
HSOverlay.close($("#subModal").get(0));
|
||||
}
|
||||
|
||||
$(document).on("click", ".openSubModal-js", event => {
|
||||
openSubForm($(event.target).data("id") || -1);
|
||||
});
|
||||
|
||||
const submitForm = async event => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = $(event.target).get(0);
|
||||
$(form).addClass("submitted");
|
||||
|
||||
if (!form.checkValidity()) { return; }
|
||||
|
||||
await $.ajax({
|
||||
url: `/guild/${guildId}/subscriptions/api`,
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
data: $(event.target).serializeArray(),
|
||||
success: () => {
|
||||
table.dataTable.draw();
|
||||
closeSubForm();
|
||||
},
|
||||
error: error => {
|
||||
alert(JSON.stringify(error, null, 4));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#subForm").on("submit", submitForm);
|
22
src/client/public/js/main.js
Normal file
22
src/client/public/js/main.js
Normal file
@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
window.addEventListener("load", () => {
|
||||
const inputs = document.querySelectorAll('.dt-container thead input');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === "a") {
|
||||
event.target.select();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
const formatTimestamp = (timestamp) => {
|
||||
const date = new Date(typeof timestamp === "string"
|
||||
? timestamp.replace(" ", "T")
|
||||
: timestamp);
|
||||
const now = new Date();
|
||||
const difference = now.getTime() - date.getTime();
|
||||
const result = `${date.getDate()} ${date.toLocaleString("en-GB", { month: "short" })}`;
|
||||
return difference < 31536000000
|
||||
? result + `, ${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`
|
||||
: result + ` ${date.getFullYear()}`;
|
||||
};
|
286
src/client/public/ts/guild/filters.ts
Normal file
286
src/client/public/ts/guild/filters.ts
Normal file
@ -0,0 +1,286 @@
|
||||
|
||||
const emptyTableHtml: string = `
|
||||
<div class="max-w-md w-full min-h-[400px] flex flex-col justify-center mx-auto px-6 py-4">
|
||||
<div class="flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
||||
<svg class="shrink-0 size-6 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
|
||||
</div>
|
||||
<h2 class="mt-5 font-semibold text-gray-800 dark:text-white">
|
||||
No results found
|
||||
</h2>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
||||
Create a filter and it will appear here.
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
||||
<button type="button" class="openFilterModal-js py-2 px-3 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none" data-hs-overlay="#filterModal">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
Create a filter
|
||||
</button>
|
||||
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||
Use a Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
var table: any;
|
||||
const defineTable = () => {
|
||||
table = new HSDataTable($("#table")[0], {
|
||||
ajax: {
|
||||
url: `/guild/${guildId}/filters/api/datatable`,
|
||||
dataSrc: "data",
|
||||
data: (d: any) => {
|
||||
if (d === undefined) { return ;}
|
||||
|
||||
d.filters = {};
|
||||
const is_whitelist = $("input[name='filterType']:checked").val();
|
||||
d.filters.is_whitelist = is_whitelist;
|
||||
}
|
||||
},
|
||||
serverSide: true,
|
||||
processing: true,
|
||||
selecting: true,
|
||||
pagingOptions: {
|
||||
pageBtnClasses: "hidden"
|
||||
},
|
||||
rowSelectingOptions: {
|
||||
selectAllSelector: "#selectAllBox"
|
||||
},
|
||||
language: {
|
||||
zeroRecords: emptyTableHtml,
|
||||
emptyTable: emptyTableHtml,
|
||||
loading: "Placeholder Loading Message...",
|
||||
},
|
||||
rowCallback: (row: any, data: any, index: number) => {
|
||||
$(row).addClass("bg-white dark:bg-neutral-900");
|
||||
},
|
||||
drawCallback: () => {
|
||||
HSDropdown.autoInit();
|
||||
},
|
||||
select: {
|
||||
style: "multi",
|
||||
selector: "td:first-child input[type='checkbox']"
|
||||
},
|
||||
columnDefs: [
|
||||
{
|
||||
// Row select checkbox
|
||||
targets: 0,
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: (data, type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap">
|
||||
<div class="ps-6 py-4">
|
||||
<label for="rowSelect${row.id}-js" class="flex">
|
||||
<input type="checkbox" id="rowSelect${row.id}-js" class="form-checkbox shrink-0 border-gray-300 rounded-sm text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" data-hs-datatable-row-selecting-individual="">
|
||||
<span class="sr-only">Checkbox</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Name
|
||||
targets: 1,
|
||||
data: "name",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: (data, _type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<span class="openFilterModal-js block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap cursor-pointer" data-id="${row.id}">
|
||||
${data}
|
||||
</sp>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Match
|
||||
targets: 2,
|
||||
data: "match",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<a href="#" class="block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap">
|
||||
${data}
|
||||
</a>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Algorithm
|
||||
target: 3,
|
||||
data: "algorithm",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: (data, type, row) => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<a href="#" class="block px-6 py-4 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 text-nowrap">
|
||||
${data}
|
||||
</a>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Filters
|
||||
target: 4,
|
||||
data: "is_insensitive",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
wrapper = $("<div>").addClass("px-6 py-4");
|
||||
badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center text-xs font-medium rounded-full");
|
||||
label = $("<span>");
|
||||
|
||||
if (data) {
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:bg-red-500");
|
||||
badge.append(label.text("No"));
|
||||
} else {
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
badge.append(label.text("Yes"));
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Whitelist
|
||||
target: 5,
|
||||
data: "is_whitelist",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
wrapper = $("<div>").addClass("px-6 py-4");
|
||||
badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center text-xs font-medium rounded-full");
|
||||
label = $("<span>");
|
||||
|
||||
if (data) {
|
||||
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||
badge.append(label.text("Whitelist"));
|
||||
} else {
|
||||
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:bg-red-500");
|
||||
badge.append(label.text("Blacklist"));
|
||||
}
|
||||
|
||||
wrapper.append(badge);
|
||||
return wrapper.get(0);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Created At
|
||||
target: 6,
|
||||
data: "created_at",
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
render: data => {
|
||||
return `
|
||||
<td class="size-px whitespace-nowrap align-top">
|
||||
<div class="px-6 py-4">
|
||||
<span class="text-sm text-gray-500 dark:text-neutral-500 text-nowrap">
|
||||
${formatTimestamp(data)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
table.dataTable
|
||||
.on("select", onTableSelectChange)
|
||||
.on("deselect", onTableSelectChange)
|
||||
.on("draw", onTableSelectChange);
|
||||
}
|
||||
|
||||
// Ensure the datatable recognises when all rows are selected, otherwise rows are only visually selected
|
||||
$("#selectAllBox").on("change", function() {
|
||||
this.checked ? table.dataTable.rows().select() : table.dataTable.rows().deselect();
|
||||
});
|
||||
|
||||
const onTableSelectChange = () => {
|
||||
const selectedRowCount = table.dataTable.rows({ selected: true }).count();
|
||||
$("#deleteRowsBtn").prop("disabled", selectedRowCount === 0);
|
||||
$(".rows-selected-count-js").text(selectedRowCount);
|
||||
|
||||
const $elem = $(".rows-selected-count-js.zero-empty-js");
|
||||
selectedRowCount === 0 ? $elem.hide() : $elem.show();
|
||||
}
|
||||
|
||||
$(window).ready(() => {
|
||||
setTimeout(defineTable, 500);
|
||||
});
|
||||
|
||||
$("input[name='filterType']").on("change", () => {
|
||||
table.dataTable.draw();
|
||||
});
|
||||
|
||||
const openFilterForm = async id => {
|
||||
$("#filterForm").removeClass("submitted");
|
||||
|
||||
const formAlgorithmSelect = HSSelect.getInstance("#formAlgorithm");
|
||||
formAlgorithmSelect.setValue("");
|
||||
|
||||
if (id === -1) {
|
||||
$("#formName").val("");
|
||||
$("#formMatch").val("");
|
||||
$("#formWhitelist").prop("checked", false);
|
||||
$("#formInsensitive").prop("checked", false);
|
||||
} else {
|
||||
const data = await $.ajax({
|
||||
url: `/guild/${guildId}/filters/api?id=${id}`,
|
||||
method: "get"
|
||||
});
|
||||
|
||||
$("#formName").val(data.name);
|
||||
$("#formMatch").val(data.match);
|
||||
$("#formWhitelist").prop("checked", data.is_whitelist);
|
||||
$("#formInsensitive").prop("checked", data.is_insensitive);
|
||||
formAlgorithmSelect.setValue(data.algorithm);
|
||||
}
|
||||
|
||||
HSOverlay.open($("#filterModal").get(0));
|
||||
}
|
||||
|
||||
const closeFilterForm = () => {
|
||||
$("#filterForm").removeClass("submitted");
|
||||
HSOverlay.close($("#filterModal").get(0));
|
||||
}
|
||||
|
||||
$(document).on("click", ".openFilterModal-js", event => {
|
||||
openFilterForm($(event.target).data("id") || -1);
|
||||
});
|
||||
|
||||
const submitForm = async event => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = $(event.target).get(0);
|
||||
$(form).addClass("submitted");
|
||||
|
||||
if (!form.checkValidity()) { return; }
|
||||
|
||||
await $.ajax({
|
||||
url: `/guild/${guildId}/filters/api`,
|
||||
method: "post",
|
||||
dataType: "json",
|
||||
data: $(event.target).serializeArray(),
|
||||
success: () => {
|
||||
table.dataTable.draw();
|
||||
closeFilterForm();
|
||||
},
|
||||
error: error => {
|
||||
alert(JSON.stringify(error, null, 4));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#filterForm").on("submit", submitForm);
|
36
src/client/public/ts/main.ts
Normal file
36
src/client/public/ts/main.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// Preline: necessary for header events.
|
||||
window.addEventListener("load", () => {
|
||||
const inputs = document.querySelectorAll('.dt-container thead input');
|
||||
|
||||
inputs.forEach(input => {
|
||||
(input as HTMLInputElement).addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === "a") {
|
||||
(event.target as HTMLInputElement).select();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Formats a given timestamp to one of two formats depending on its age.
|
||||
* @param timestamp
|
||||
* @returns 'DD MMM, HH:mm' if younger than 1 year, else 'DD MMM YYYY'
|
||||
*/
|
||||
const formatTimestamp = (timestamp: string | number) => {
|
||||
const date = new Date(
|
||||
typeof timestamp === "string"
|
||||
? timestamp.replace(" ", "T")
|
||||
: timestamp
|
||||
);
|
||||
const now = new Date();
|
||||
const difference = now.getTime() - date.getTime();
|
||||
|
||||
// Day and short month (example: 21 Oct)
|
||||
const result = `${date.getDate()} ${date.toLocaleString("en-GB", { month: "short" })}`
|
||||
|
||||
// Difference is less than a year: 'DD MMM, HH:mm'
|
||||
// Or, difference is more than a year: 'DD MMM YYYY'
|
||||
return difference < 31536000000
|
||||
? result + `, ${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`
|
||||
: result + ` ${date.getFullYear()}`;
|
||||
}
|
1
src/client/public/types/globals.d.ts
vendored
Normal file
1
src/client/public/types/globals.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare const guildId: string;
|
83
src/client/public/types/preline.d.ts
vendored
Normal file
83
src/client/public/types/preline.d.ts
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
import type INoUiSlider from 'nouislider';
|
||||
|
||||
import { ICollectionItem } from 'preline/src/interfaces';
|
||||
|
||||
import { IStaticMethods } from 'preline/src/static/interfaces';
|
||||
|
||||
import type HSCopyMarkup from 'preline/src/plugins/copy-markup';
|
||||
import type HSAccordion from 'preline/src/plugins/accordion';
|
||||
import type HSCarousel from 'preline/src/plugins/carousel';
|
||||
import type HSCollapse from 'preline/src/plugins/collapse';
|
||||
import type HSComboBox from 'preline/src/plugins/combobox';
|
||||
import type HSDataTable from 'preline/src/plugins/datatable';
|
||||
import type HSDropdown from 'preline/src/plugins/dropdown';
|
||||
import type HSFileUpload from 'preline/src/plugins/file-upload';
|
||||
import type HSInputNumber from 'preline/src/plugins/input-number';
|
||||
import type HSLayoutSplitter from 'preline/src/plugins/layout-splitter';
|
||||
import type HSOverlay from 'preline/src/plugins/overlay';
|
||||
import type HSPinInput from 'preline/src/plugins/pin-input';
|
||||
import type HSRangeSlider from 'preline/src/plugins/range-slider';
|
||||
import type HSRemoveElement from 'preline/src/plugins/remove-element';
|
||||
import type HSScrollNav from 'preline/src/plugins/scroll-nav';
|
||||
import type HSScrollspy from 'preline/src/plugins/scrollspy';
|
||||
import type HSSelect from 'preline/src/plugins/select';
|
||||
import type HSStepper from 'preline/src/plugins/stepper';
|
||||
import type HSStrongPassword from 'preline/src/plugins/strong-password';
|
||||
import type HSTabs from 'preline/src/plugins/tabs';
|
||||
import type HSTextareaAutoHeight from 'preline/src/plugins/textarea-auto-height';
|
||||
import type HSThemeSwitch from 'preline/src/plugins/theme-switch';
|
||||
import type HSToggleCount from 'preline/src/plugins/toggle-count';
|
||||
import type HSTogglePassword from 'preline/src/plugins/toggle-password';
|
||||
import type HSTooltip from 'preline/src/plugins/tooltip';
|
||||
import type HSTreeView from 'preline/src/plugins/tree-view';
|
||||
|
||||
declare global {
|
||||
var noUiSlider: typeof INoUiSlider;
|
||||
var FloatingUIDOM: {
|
||||
computePosition: (
|
||||
reference: Element,
|
||||
floating: HTMLElement,
|
||||
options?: any
|
||||
) => Promise<{ x: number; y: number; placement: string }>;
|
||||
autoUpdate: (
|
||||
reference: Element,
|
||||
floating: HTMLElement,
|
||||
update: () => void,
|
||||
) => () => void;
|
||||
offset: (offset: number | [number, number]) => any;
|
||||
flip: () => any;
|
||||
};
|
||||
|
||||
interface Window {
|
||||
HS_CLIPBOARD_SELECTOR: string;
|
||||
|
||||
HSStaticMethods: IStaticMethods;
|
||||
|
||||
$hsCopyMarkupCollection: ICollectionItem<HSCopyMarkup>[];
|
||||
$hsAccordionCollection: ICollectionItem<HSAccordion>[];
|
||||
$hsCarouselCollection: ICollectionItem<HSCarousel>[];
|
||||
$hsCollapseCollection: ICollectionItem<HSCollapse>[];
|
||||
$hsComboBoxCollection: ICollectionItem<HSComboBox>[];
|
||||
$hsDataTableCollection: ICollectionItem<HSDataTable>[];
|
||||
$hsDropdownCollection: ICollectionItem<HSDropdown>[];
|
||||
$hsFileUploadCollection: ICollectionItem<HSFileUpload>[];
|
||||
$hsInputNumberCollection: { id: number; element: HSInputNumber }[];
|
||||
$hsLayoutSplitterCollection: ICollectionItem<HSLayoutSplitter>[];
|
||||
$hsOverlayCollection: ICollectionItem<HSOverlay>[];
|
||||
$hsPinInputCollection: ICollectionItem<HSPinInput>[];
|
||||
$hsRemoveElementCollection: ICollectionItem<HSRemoveElement>[];
|
||||
$hsRangeSliderCollection: ICollectionItem<HSRangeSlider>[];
|
||||
$hsScrollNavCollection: ICollectionItem<HSScrollNav>[];
|
||||
$hsScrollspyCollection: ICollectionItem<HSScrollspy>[];
|
||||
$hsSelectCollection: ICollectionItem<HSSelect>[];
|
||||
$hsStepperCollection: ICollectionItem<HSStepper>[];
|
||||
$hsStrongPasswordCollection: ICollectionItem<HSStrongPassword>[];
|
||||
$hsTabsCollection: ICollectionItem<HSTabs>[];
|
||||
$hsTextareaAutoHeightCollection: ICollectionItem<HSTextareaAutoHeight>[];
|
||||
$hsThemeSwitchCollection: ICollectionItem<HSThemeSwitch>[];
|
||||
$hsToggleCountCollection: ICollectionItem<HSToggleCount>[];
|
||||
$hsTogglePasswordCollection: ICollectionItem<HSTogglePassword>[];
|
||||
$hsTooltipCollection: ICollectionItem<HSTooltip>[];
|
||||
$hsTreeViewCollection: ICollectionItem<HSTreeView>[];
|
||||
}
|
||||
}
|
30
src/client/views/errors/status.ejs
Normal file
30
src/client/views/errors/status.ejs
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="/static/css/tailwind.css">
|
||||
</head>
|
||||
<body class="bg-neutral-100 dark:bg-neutral-900 text-gray-600 dark:text-gray-400 font-[Inter]">
|
||||
|
||||
<main class="flex flex-col justify-center min-h-[100vh]">
|
||||
<div class="text-center p-2 sm:p-5 sm-pb-0">
|
||||
<div class="max-w-[28rem] mx-auto">
|
||||
<h1 class="text-[6rem] text-gray-800 dark:text-neutral-200">404</h1>
|
||||
<h2 class="text-3xl text-gray-800 dark:text-neutral-200">Page not found</h2>
|
||||
<p class="">Sorry, the page you're looking for cannot be found.</p>
|
||||
<div>
|
||||
<button type="button">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg>
|
||||
Back to home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<script src="/static/preline.js"></script>
|
||||
</body>
|
||||
</html>
|
7
src/client/views/footer.ejs
Normal file
7
src/client/views/footer.ejs
Normal file
@ -0,0 +1,7 @@
|
||||
<footer class="fixed left-0 right-0 bottom-0 lg:ps-64 h-10 sm:h-16">
|
||||
<div class="flex justify-between items-center p-2 sm:p-5">
|
||||
<p class="text-xs sm:text-sm">
|
||||
© 2025 Xord
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
7
src/client/views/guild/content.ejs
Normal file
7
src/client/views/guild/content.ejs
Normal file
@ -0,0 +1,7 @@
|
||||
<% layout("layout") -%>
|
||||
|
||||
<%- include("guildHeader") -%>
|
||||
|
||||
<div class="p-4">
|
||||
Content
|
||||
</div>
|
311
src/client/views/guild/filters.ejs
Normal file
311
src/client/views/guild/filters.ejs
Normal file
@ -0,0 +1,311 @@
|
||||
<% layout("layout") -%>
|
||||
|
||||
<%- include("guildHeader") -%>
|
||||
|
||||
<!-- Table Section -->
|
||||
<div id="table" class="--prevent-on-load-init max-w-full overflow-hidden px-4 sm:px-6">
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col">
|
||||
<div class="-m-1.5">
|
||||
<div class="max-w-full p-1.5 min-w-full inline-block align-middle">
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-xs overflow-hidden dark:bg-neutral-900 dark:border-neutral-800">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center border-b border-gray-200 dark:border-neutral-700">
|
||||
<!-- Input -->
|
||||
<div class="hidden sm:block sm:col-span-1">
|
||||
<label for="search" class="sr-only">Search</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="search" name="search" class="form-input px-3 ps-11 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600" placeholder="Search" data-hs-datatable-search="">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4">
|
||||
<svg class="shrink-0 size-4 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Input -->
|
||||
|
||||
<div class="sm:col-span-2 md:grow">
|
||||
<div class="flex justify-end gap-x-2">
|
||||
|
||||
<button type="button" id="deleteRowsBtn" disabled class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-red-500 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800" href="#">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||
<span>
|
||||
<span class="hidden sm:inline">Delete</span>
|
||||
<span class="rows-selected-count-js zero-empty-js before:content-['('] after:content-[')'] empty:hidden"></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="hs-dropdown [--placement:bottom-right] relative inline-block h-full" data-hs-dropdown-auto-close="inside">
|
||||
<button id="hs-as-table-table-filter-dropdown" type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" aria-haspopup="menu" aria-expanded="false" aria-label="Dropdown">
|
||||
<svg class="shrink-0 size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>
|
||||
Filter
|
||||
</button>
|
||||
<div class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden divide-y divide-gray-200 min-w-48 z-10 bg-white shadow-md rounded-lg mt-2 dark:divide-neutral-700 dark:bg-neutral-800 dark:border dark:border-neutral-700" role="menu" aria-orientation="vertical" aria-labelledby="hs-as-table-table-filter-dropdown">
|
||||
<div class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<label for="hs-as-filters-dropdown-all" class="flex py-2.5 px-3">
|
||||
<input type="radio" name="filterType" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-all" checked value="">
|
||||
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">All</span>
|
||||
</label>
|
||||
<label for="hs-as-filters-dropdown-published" class="flex py-2.5 px-3">
|
||||
<input type="radio" name="filterType" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-published" value="1">
|
||||
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">Whitelist</span>
|
||||
</label>
|
||||
<label for="hs-as-filters-dropdown-pending" class="flex py-2.5 px-3">
|
||||
<input type="radio" name="filterType" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-pending" value="0">
|
||||
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">Blacklist</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="openFilterModal-js py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
<span>
|
||||
Add
|
||||
<span class="hidden sm:inline">filter</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Header -->
|
||||
|
||||
<div class="min-w-full overflow-x-auto">
|
||||
<!-- Table -->
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<thead class="bg-gray-50 dark:bg-neutral-800 border-none">
|
||||
<tr>
|
||||
<th scope="col" class="ps-6 py-3 text-start --exclude-from-ordering">
|
||||
<label for="hs-at-with-checkboxes-main" class="flex ml-[1px]">
|
||||
<input type="checkbox" id="selectAllBox" class="form-checkbox shrink-0 border-gray-300 rounded-sm text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800">
|
||||
<span class="sr-only">Checkbox</span>
|
||||
</label>
|
||||
</th>
|
||||
|
||||
<th scope="col" data-dt-column="name" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Name
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Match
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Algorithm
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap">
|
||||
Case-Sensitive
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Type
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap">
|
||||
Created at
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- End Table -->
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 gap-3 flex justify-between items-center border-t border-gray-200 dark:border-neutral-700">
|
||||
<div class="max-w-sm space-y-3">
|
||||
<select data-hs-select='{
|
||||
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||
"toggleClasses": "form-select hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 px-3 pe-9 flex text-nowrap w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 before:absolute before:inset-0 before:z-1 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800",
|
||||
"dropdownClasses": "mt-2 z-50 w-20 max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
|
||||
"dropdownScope": "window",
|
||||
"optionClasses": "py-2 px-3 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||
"optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
|
||||
}' class="hidden" data-hs-datatable-page-entities="">
|
||||
<option value="1">1</option>
|
||||
<option value="5">5</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="15">15</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:inline-flex items-center gap-x-2" data-hs-datatable-info="">
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||
Showing
|
||||
<span data-hs-datatable-info-from=""></span>
|
||||
to
|
||||
<span data-hs-datatable-info-to=""></span>
|
||||
of
|
||||
<span data-hs-datatable-info-length=""></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex gap-x-2" data-hs-datatable-paging="">
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" data-hs-datatable-paging-prev="">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Prev
|
||||
</button>
|
||||
<div class="flex items-center space-x-1 " data-hs-datatable-paging-pages=""></div>
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" data-hs-datatable-paging-next="">
|
||||
Next
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Footer -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
</div>
|
||||
<!-- End Table Section -->
|
||||
|
||||
<!-- Popup -->
|
||||
<div id="filterModal" class="hs-overlay hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1" aria-labelledby="hs-scale-animation-modal-label">
|
||||
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 sm:max-w-lg sm:w-full m-3 sm:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
||||
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-xs rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-neutral-200">Filter</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||
Filter out unwanted content from your feeds.
|
||||
</p>
|
||||
</div>
|
||||
<form id="filterForm" novalidate class="group grid grid-cols-1 gap-y-4 sm:gap-y-6 md:gap-y-8 gap-x-6 sm:gap-x-8 md:gap-x-10">
|
||||
<div>
|
||||
<label for="formName" class="text-input-label">Name</label>
|
||||
<input type="text" id="formName" name="name" class="form-input text-input peer invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500" required>
|
||||
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||
Human-readable name for this entry.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||
Please enter a name.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formMatch" class="text-input-label">Match</label>
|
||||
<input type="text" id="formMatch" name="match" class="form-input text-input peer invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500" required>
|
||||
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||
The statement to match against.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||
Please enter a match.
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label for="formAlgorithm" class="text-input-label">Algorithm</label>
|
||||
<select id="formAlgorithm" name="algorithm" class="peer" data-hs-select='{
|
||||
"placeholder": "Select option...",
|
||||
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||
"toggleClasses": "form-select hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 select-input peer-invalid:group-[.submitted]:border-red-500 peer-invalid:group-[.submitted]:ring-red-500",
|
||||
"dropdownScope": "window",
|
||||
"wrapperClasses": "peer",
|
||||
"dropdownClasses": "z-80 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
|
||||
"optionClasses": "py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||
"optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500 \" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
|
||||
}' class="hidden" required>
|
||||
<option value="">Choose</option>
|
||||
<option value="any">Match Any Word</option>
|
||||
<option value="all">Match All Words</option>
|
||||
<option value="exact">Match Exact Expression</option>
|
||||
<option value="regex">Match Regex</option>
|
||||
<option value="fuzzy">Match Roughly</option>
|
||||
</select>
|
||||
<p class="text-input-help block peer-has-invalid:group-[.submitted]:hidden">
|
||||
The algorithm used to filter out content.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-has-invalid:group-[.submitted]:block">
|
||||
Please select an option.
|
||||
</p>
|
||||
</div>
|
||||
<label for="formWhitelist" class="flex gap-4">
|
||||
<input type="checkbox" id="formWhitelist" name="is_whitelist" class="form-radio relative w-[3.25rem] h-7 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600 before:inline-block before:size-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow-sm before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-blue-200">
|
||||
<span class="flex flex-col">
|
||||
<span class="block text-sm dark:text-neutral-400">Whitelist</span>
|
||||
<span class="block text-sm text-gray-500 dark:text-neutral-500">Inactive entries will not be processed.</span>
|
||||
</span>
|
||||
</label>
|
||||
<label for="formInsensitive" class="flex gap-4">
|
||||
<input type="checkbox" id="formInsensitive" name="is_insensitive" class="form-radio relative w-[3.25rem] h-7 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600 before:inline-block before:size-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow-sm before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-blue-200">
|
||||
<span class="flex flex-col">
|
||||
<span class="block text-sm dark:text-neutral-400">Insensitive</span>
|
||||
<span class="block text-sm text-gray-500 dark:text-neutral-500">Inactive entries will not be processed.</span>
|
||||
</span>
|
||||
</label>
|
||||
</form>
|
||||
<div class="flex items-center gap-x-2 mt-8">
|
||||
<button type="button" class="me-auto py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||
Templates
|
||||
</button>
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" data-hs-overlay="#filterModal">
|
||||
Close
|
||||
</button>
|
||||
<button type="submit" form="filterForm" class="group-invalid:pointer-events-none group-invalid:opacity-30 py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Popup -->
|
||||
|
||||
<script>
|
||||
var guildId = "<%- guild.id %>"
|
||||
</script>
|
||||
|
||||
<% block("scripts").append('<script defer src="/static/js/guild/filters.js"></script>'); %>
|
65
src/client/views/guild/guildHeader.ejs
Normal file
65
src/client/views/guild/guildHeader.ejs
Normal file
@ -0,0 +1,65 @@
|
||||
<div>
|
||||
<div class="relative pt-5 before:w-full before:h-[200px] before:-z-10 before:top-0 before:start-0 before:absolute">
|
||||
<div class="flex sm:items-center gap-5 p-4 sm:p-6 pb-1!">
|
||||
|
||||
<div class="shrink-0">
|
||||
<div class="relative">
|
||||
<% if (guild.icon) { %>
|
||||
<img src="<%= guild.iconURL() %>" class="rounded-md sm:rounded-lg size-16 sm:size-20" alt="">
|
||||
<% } else { %>
|
||||
<div class="size-16 sm:size-20 rounded-md sm:rounded-lg flex shrink-0 justify-center items-center bg-white dark:bg-neutral-800">
|
||||
<span class="text-2xl"><%= guild.nameAcronym %></span>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grow">
|
||||
<h1 class="text-2xl md:text-3xl font-semibold text-gray-800 dark:text-neutral-200">
|
||||
<%= guild.name %>
|
||||
</h1>
|
||||
<ul class="flex flex-wrap items-center gap-3 mt-2">
|
||||
<li class="relative">
|
||||
<span class="text-sm">ID:</span>
|
||||
<span class="text-sm inline-flex items-center gap-2 text-gray-800 dark:text-neutral-200"><%= guild.id %></span>
|
||||
</li>
|
||||
<li class="relative">
|
||||
<span class="text-sm">Members:</span>
|
||||
<span class="text-sm inline-flex items-center gap-2 text-gray-800 dark:text-neutral-200"><%= guild.memberCount %></span>
|
||||
</li>
|
||||
<li class="relative">
|
||||
<span class="text-sm">Channels:</span>
|
||||
<span class="text-sm inline-flex items-center gap-2 text-gray-800 dark:text-neutral-200"><%= guild.channels.channelCountWithoutThreads %></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto flex flex-row flex-wrap gap-2">
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||
<svg class="shrink-0 size-4" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
Validate channels
|
||||
</button>
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||
<svg class="shrink-0 size-4" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
|
||||
Copy data
|
||||
</button>
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-red-500 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800">
|
||||
<svg class="shrink-0 size-4" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
|
||||
Remove bot
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center items-center mx-auto p-4 sm:p-6">
|
||||
<div class="flex flex-row w-full pb-1 whitespace-nowrap overflow-x-auto overflow-y-hidden">
|
||||
<!-- <a href="/guild/<%= guild.id %>" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 <%= !isNaN(+guildPage) ? 'bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Overview</a> -->
|
||||
<a href="/guild/<%= guild.id %>/subscriptions" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 <%= guildPage === 'subscriptions' ? 'text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Subscriptions</a>
|
||||
<a href="/guild/<%= guild.id %>/filters" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 <%= guildPage === 'filters' ? 'text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Filters</a>
|
||||
<a href="/guild/<%= guild.id %>/styles" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 <%= guildPage === 'styles' ? 'text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Styles</a>
|
||||
<a href="/guild/<%= guild.id %>/content" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 <%= guildPage === 'content' ? 'text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Content</a>
|
||||
<a href="#" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
122
src/client/views/guild/overview.ejs
Normal file
122
src/client/views/guild/overview.ejs
Normal file
@ -0,0 +1,122 @@
|
||||
<% layout("layout") -%>
|
||||
|
||||
<%- include("guildHeader") -%>
|
||||
|
||||
<!-- Card Section -->
|
||||
<div class="max-w-full p-4 md:p-6">
|
||||
<!-- Grid -->
|
||||
<div class="grid sm:grid-cols-2 xl:grid-cols-4 gap-4 sm:gap-6">
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col bg-white border shadow-xs rounded-md dark:bg-neutral-800 dark:border-neutral-700">
|
||||
<div class="p-4 md:p-5 flex gap-x-4">
|
||||
<div class="shrink-0 flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
||||
<svg class="shrink-0 size-5 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-neutral-500">
|
||||
Total Members
|
||||
</p>
|
||||
<div class="hs-tooltip">
|
||||
<div class="hs-tooltip-toggle">
|
||||
<svg class="shrink-0 size-4 text-gray-500 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
|
||||
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-gray-900 text-xs font-medium text-white rounded-sm shadow-xs dark:bg-neutral-700" role="tooltip">
|
||||
The number of server members
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-neutral-200">
|
||||
<%= guild.memberCount %>
|
||||
</h3>
|
||||
<!-- <span class="flex items-center gap-x-1 text-green-600">
|
||||
<svg class="inline-block size-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>
|
||||
<span class="inline-block text-sm">
|
||||
1.7%
|
||||
</span>
|
||||
</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col bg-white border shadow-xs rounded-md dark:bg-neutral-800 dark:border-neutral-700">
|
||||
<div class="p-4 md:p-5 flex gap-x-4">
|
||||
<div class="shrink-0 flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
||||
<svg class="shrink-0 size-6 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="9" x2="20" y2="9"></line><line x1="4" y1="15" x2="20" y2="15"></line><line x1="10" y1="3" x2="8" y2="21"></line><line x1="16" y1="3" x2="14" y2="21"></line></svg>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-neutral-500">
|
||||
Channels
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-neutral-200">
|
||||
<%= guild.channels.channelCountWithoutThreads %>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col bg-white border shadow-xs rounded-md dark:bg-neutral-800 dark:border-neutral-700">
|
||||
<div class="p-4 md:p-5 flex gap-x-4">
|
||||
<div class="shrink-0 flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
||||
<svg class="shrink-0 size-5 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-neutral-500">
|
||||
Processed Content
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-neutral-200">
|
||||
0
|
||||
</h3>
|
||||
<!-- <span class="flex items-center gap-x-1 text-red-600">
|
||||
<svg class="inline-block size-4 self-center" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 17 13.5 8.5 8.5 13.5 2 7"/><polyline points="16 17 22 17 22 11"/></svg>
|
||||
<span class="inline-block text-sm">
|
||||
1.7%
|
||||
</span>
|
||||
</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col bg-white border shadow-xs rounded-md dark:bg-neutral-800 dark:border-neutral-700">
|
||||
<div class="p-4 md:p-5 flex gap-x-4">
|
||||
<div class="shrink-0 flex justify-center items-center size-[46px] bg-gray-100 rounded-lg dark:bg-neutral-800">
|
||||
<svg class="shrink-0 size-5 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-neutral-500">
|
||||
Blocked Content
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-neutral-200">
|
||||
0
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
</div>
|
||||
<!-- End Grid -->
|
||||
</div>
|
||||
<!-- End Card Section -->
|
7
src/client/views/guild/styles.ejs
Normal file
7
src/client/views/guild/styles.ejs
Normal file
@ -0,0 +1,7 @@
|
||||
<% layout("layout") -%>
|
||||
|
||||
<%- include("guildHeader") -%>
|
||||
|
||||
<div class="p-4">
|
||||
Styles
|
||||
</div>
|
377
src/client/views/guild/subscriptions.ejs
Normal file
377
src/client/views/guild/subscriptions.ejs
Normal file
@ -0,0 +1,377 @@
|
||||
<% layout("layout") -%>
|
||||
|
||||
<%- include("guildHeader") -%>
|
||||
|
||||
<!-- Table Section -->
|
||||
<div id="table" class="--prevent-on-load-init max-w-full overflow-hidden px-4 sm:px-6">
|
||||
<!-- Card -->
|
||||
<div class="flex flex-col">
|
||||
<div class="-m-1.5">
|
||||
<div class="max-w-full p-1.5 min-w-full inline-block align-middle">
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-xs overflow-hidden dark:bg-neutral-900 dark:border-neutral-800">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center border-b border-gray-200 dark:border-neutral-700">
|
||||
<!-- Input -->
|
||||
<div class="hidden sm:block sm:col-span-1">
|
||||
<label for="search" class="sr-only">Search</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="search" name="search" class="form-input px-3 ps-11 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600" placeholder="Search" data-hs-datatable-search="">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4">
|
||||
<svg class="shrink-0 size-4 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Input -->
|
||||
|
||||
<div class="sm:col-span-2 md:grow">
|
||||
<div class="flex justify-end gap-x-2">
|
||||
|
||||
<button type="button" id="deleteRowsBtn" disabled class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-red-500 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800" href="#">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||
<span>
|
||||
<span class="hidden sm:inline">Delete</span>
|
||||
<span class="rows-selected-count-js zero-empty-js before:content-['('] after:content-[')'] empty:hidden"></span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="hs-dropdown [--placement:bottom-right] relative inline-block h-full" data-hs-dropdown-auto-close="inside">
|
||||
<button id="hs-as-table-table-filter-dropdown" type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" aria-haspopup="menu" aria-expanded="false" aria-label="Dropdown">
|
||||
<svg class="shrink-0 size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>
|
||||
Filter
|
||||
</button>
|
||||
<div class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden divide-y divide-gray-200 min-w-48 z-10 bg-white shadow-md rounded-lg mt-2 dark:divide-neutral-700 dark:bg-neutral-800 dark:border dark:border-neutral-700" role="menu" aria-orientation="vertical" aria-labelledby="hs-as-table-table-filter-dropdown">
|
||||
<div class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<label for="hs-as-filters-dropdown-all" class="flex py-2.5 px-3">
|
||||
<input type="radio" name="filterActive" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-all" checked value="">
|
||||
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">All</span>
|
||||
</label>
|
||||
<label for="hs-as-filters-dropdown-published" class="flex py-2.5 px-3">
|
||||
<input type="radio" name="filterActive" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-published" value="1">
|
||||
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">Active</span>
|
||||
</label>
|
||||
<label for="hs-as-filters-dropdown-pending" class="flex py-2.5 px-3">
|
||||
<input type="radio" name="filterActive" class="form-radio shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-as-filters-dropdown-pending" value="0">
|
||||
<span class="ms-3 text-sm text-gray-800 dark:text-neutral-200">Inactive</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="openSubModal-js py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
|
||||
<span>
|
||||
Add
|
||||
<span class="hidden sm:inline">subscription</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Header -->
|
||||
|
||||
<div class="min-w-full overflow-x-auto">
|
||||
<!-- Table -->
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
<thead class="bg-gray-50 dark:bg-neutral-800 border-none">
|
||||
<tr>
|
||||
<th scope="col" class="ps-6 py-3 text-start --exclude-from-ordering">
|
||||
<label for="hs-at-with-checkboxes-main" class="flex ml-[1px]">
|
||||
<input type="checkbox" id="selectAllBox" class="form-checkbox shrink-0 border-gray-300 rounded-sm text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800">
|
||||
<span class="sr-only">Checkbox</span>
|
||||
</label>
|
||||
</th>
|
||||
|
||||
<th scope="col" data-dt-column="name" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Name
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
URL
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start --exclude-from-ordering">
|
||||
<div class="flex justify-between items-center gap-x-2">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Channels
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start --exclude-from-ordering">
|
||||
<div class="flex justify-between items-center gap-x-2">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Filters
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Style
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap">
|
||||
Created at
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th scope="col" class="px-6 py-3 text-start">
|
||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
||||
Status
|
||||
</span>
|
||||
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
|
||||
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-neutral-700">
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- End Table -->
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 gap-3 flex justify-between items-center border-t border-gray-200 dark:border-neutral-700">
|
||||
<div class="max-w-sm space-y-3">
|
||||
<select data-hs-select='{
|
||||
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||
"toggleClasses": "form-select hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 relative py-2 px-3 pe-9 flex text-nowrap w-full cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 before:absolute before:inset-0 before:z-1 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800",
|
||||
"dropdownClasses": "mt-2 z-50 w-20 max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
|
||||
"dropdownScope": "window",
|
||||
"optionClasses": "py-2 px-3 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||
"optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500\" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
|
||||
}' class="hidden" data-hs-datatable-page-entities="">
|
||||
<option value="1">1</option>
|
||||
<option value="5">5</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="15">15</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:inline-flex items-center gap-x-2" data-hs-datatable-info="">
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||
Showing
|
||||
<span data-hs-datatable-info-from=""></span>
|
||||
to
|
||||
<span data-hs-datatable-info-to=""></span>
|
||||
of
|
||||
<span data-hs-datatable-info-length=""></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex gap-x-2" data-hs-datatable-paging="">
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" data-hs-datatable-paging-prev="">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Prev
|
||||
</button>
|
||||
<div class="flex items-center space-x-1 " data-hs-datatable-paging-pages=""></div>
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" data-hs-datatable-paging-next="">
|
||||
Next
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Footer -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Card -->
|
||||
|
||||
</div>
|
||||
<!-- End Table Section -->
|
||||
|
||||
<!-- Popup -->
|
||||
<div id="subModal" class="hs-overlay hidden size-full fixed top-0 start-0 z-80 overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1" aria-labelledby="hs-scale-animation-modal-label">
|
||||
<div class="hs-overlay-animation-target hs-overlay-open:scale-100 hs-overlay-open:opacity-100 scale-95 opacity-0 ease-in-out transition-all duration-200 lg:max-w-4xl lg:w-full m-3 lg:mx-auto min-h-[calc(100%-3.5rem)] flex items-center">
|
||||
<div class="w-full p-4 sm:p-7 flex flex-col bg-white border shadow-xs rounded-lg pointer-events-auto dark:bg-neutral-900 dark:border-neutral-800 dark:shadow-neutral-700/70">
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-neutral-200">Subscription</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||
Manage your RSS feeds with filters and channel targets.
|
||||
</p>
|
||||
</div>
|
||||
<form id="subForm" novalidate class="group grid sm:grid-cols-2 gap-y-4 sm:gap-y-6 md:gap-y-8 gap-x-6 sm:gap-x-8 md:gap-x-10">
|
||||
<div>
|
||||
<label for="formName" class="text-input-label">Name</label>
|
||||
<input type="text" id="formName" name="name" class="form-input text-input peer invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500" required>
|
||||
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||
Human-readable name for this entry.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||
Please enter a name.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formUrl" class="text-input-label">URL</label>
|
||||
<input type="url" id="formUrl" name="url" class="form-input text-input peer invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500" required>
|
||||
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||
Source of RSS content.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||
Please enter a valid URL.
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label for="formStyle" class="text-input-label">Message Style</label>
|
||||
<select id="formStyle" name="message_style" class="peer" data-hs-select='{
|
||||
"placeholder": "Select option...",
|
||||
"toggleTag": "<button type=\"button\" aria-expanded=\"false\"></button>",
|
||||
"toggleClasses": "form-select hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 select-input peer-invalid:group-[.submitted]:border-red-500 peer-invalid:group-[.submitted]:ring-red-500",
|
||||
"dropdownScope": "window",
|
||||
"wrapperClasses": "peer",
|
||||
"dropdownClasses": "z-80 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
|
||||
"optionClasses": "py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||
"optionTemplate": "<div class=\"flex justify-between items-center w-full\"><span data-title></span><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-3.5 text-blue-600 dark:text-blue-500 \" xmlns=\"http:.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg></span></div>"
|
||||
}' class="hidden" required>
|
||||
<option value="">Choose</option>
|
||||
<option>Default style</option>
|
||||
</select>
|
||||
<p class="text-input-help block peer-has-invalid:group-[.submitted]:hidden">
|
||||
Appearance of delivered content.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-has-invalid:group-[.submitted]:block">
|
||||
Please select an option.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="formPublishedThreshold" class="text-input-label">Published Threshold</label>
|
||||
<input type="datetime-local" id="formPublishedThreshold" name="published_threshold" class="form-input text-input invalid:group-[.submitted]:border-red-500 invalid:group-[.submitted]:ring-red-500 peer" required>
|
||||
<p class="text-input-help block peer-invalid:group-[.submitted]:hidden">
|
||||
Ignore content older than this date.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-red-500 hidden peer-invalid:group-[.submitted]:block">
|
||||
Please enter a date & time.
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label for="formChannelsInput" class="block text-sm font-medium mb-2 dark:text-white">Channels</label>
|
||||
<select id="formChannels" name="channels" multiple="multiple" data-hs-select='{
|
||||
"placeholder": "Select option...",
|
||||
"dropdownClasses": "z-80 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
|
||||
"dropdownScope": "window",
|
||||
"optionClasses": "py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||
"mode": "tags",
|
||||
"tagsInputId": "formChannelsInput",
|
||||
"wrapperClasses": "relative form-select has-invalid:group-[.submitted]:border-red-500 has-invalid:group-[.submitted]:ring-red-500 py-0 ps-0.5 pe-9 min-h-[46px] flex items-center flex-wrap text-nowrap w-full border border-gray-200 rounded-lg text-start text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400",
|
||||
"tagsItemTemplate": "<div class=\"flex flex-nowrap items-center relative z-10 bg-white border border-gray-200 rounded-lg p-1 m-1 dark:bg-neutral-900 dark:border-neutral-700 \"><div class=\"size-6 flex justify-center items-center\" data-icon></div><div class=\"whitespace-nowrap text-gray-800 dark:text-neutral-200 \" data-title></div><div class=\"inline-flex shrink-0 justify-center items-center size-5 ms-2 rounded-lg text-gray-800 bg-gray-200 hover:bg-gray-300 focus:outline-hidden focus:ring-2 focus:ring-gray-400 text-sm dark:bg-neutral-700/50 dark:hover:bg-neutral-700 dark:text-neutral-400 cursor-pointer\" data-remove><svg class=\"shrink-0 size-3\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/></svg></div></div>",
|
||||
"tagsInputClasses": "px-2 rounded-xs order-1 text-sm outline-hidden dark:bg-neutral-900 dark:placeholder-neutral-500 dark:text-neutral-400",
|
||||
"optionTemplate": "<div class=\"flex items-center\"><div class=\"size-8 me-2 flex shrink-0 items-center justify-center text-gray-500 dark:text-neutral-500\" data-icon></div><div><div class=\"text-sm font-semibold text-gray-800 dark:text-neutral-200 \" data-title></div><div class=\"text-xs text-gray-500 dark:text-neutral-500 \" data-description></div></div><div class=\"ms-auto\"><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-4 text-blue-600\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path d=\"M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z\"/></svg></span></div></div>"
|
||||
}' class="hidden">
|
||||
<option value="">Choose</option>
|
||||
|
||||
<% guild.channels.cache
|
||||
.filter(channel => channel.type == 0)
|
||||
.sort((a, b) => a.rawPosition - b.rawPosition)
|
||||
.forEach(channel => { %>
|
||||
<option value="<%= channel.id %>" data-hs-select-option='{
|
||||
"description": "<%= channel.id %>",
|
||||
"icon": "<svg class=\"shrink-0 size-[16px]\" viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"4\" y1=\"9\" x2=\"20\" y2=\"9\"></line><line x1=\"4\" y1=\"15\" x2=\"20\" y2=\"15\"></line><line x1=\"10\" y1=\"3\" x2=\"8\" y2=\"21\"></line><line x1=\"16\" y1=\"3\" x2=\"14\" y2=\"21\"></line></svg>"
|
||||
}'>
|
||||
<%= channel.name %>
|
||||
</option>
|
||||
<% }); %>
|
||||
|
||||
</select>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-neutral-500">
|
||||
Send content to these channels.
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label for="formFilters" class="block text-sm font-medium mb-2 dark:text-white">Filters</label>
|
||||
<select id="formFilters" name="filters" multiple="multiple" data-hs-select='{
|
||||
"placeholder": "Select option...",
|
||||
"dropdownClasses": "z-80 w-full max-h-72 p-1 space-y-0.5 bg-white border border-gray-200 rounded-lg overflow-hidden overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300 dark:[&::-webkit-scrollbar-track]:bg-neutral-700 dark:[&::-webkit-scrollbar-thumb]:bg-neutral-500 dark:bg-neutral-900 dark:border-neutral-700",
|
||||
"dropdownScope": "window",
|
||||
"optionClasses": "py-2 px-4 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100 rounded-lg focus:outline-hidden focus:bg-gray-100 hs-select-disabled:pointer-events-none hs-select-disabled:opacity-50 dark:bg-neutral-900 dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800",
|
||||
"mode": "tags",
|
||||
"tagsInputId": "formFiltersInput",
|
||||
"wrapperClasses": "relative ps-0.5 pe-9 min-h-[46px] flex items-center flex-wrap text-nowrap w-full border border-gray-200 rounded-lg text-start text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400",
|
||||
"tagsItemTemplate": "<div class=\"flex flex-nowrap items-center relative z-10 bg-white border border-gray-200 rounded-lg p-1 m-1 dark:bg-neutral-900 dark:border-neutral-700 \"><div class=\"ms-1 whitespace-nowrap text-gray-800 dark:text-neutral-200 \" data-title></div><div class=\"inline-flex shrink-0 justify-center items-center size-5 ms-2 rounded-lg text-gray-800 bg-gray-200 hover:bg-gray-300 focus:outline-hidden focus:ring-2 focus:ring-gray-400 text-sm dark:bg-neutral-700/50 dark:hover:bg-neutral-700 dark:text-neutral-400 cursor-pointer\" data-remove><svg class=\"shrink-0 size-3\" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/></svg></div></div>",
|
||||
"tagsInputClasses": "py-3 px-2 rounded-lg order-1 text-sm outline-hidden dark:bg-neutral-900 dark:placeholder-neutral-500 dark:text-neutral-400",
|
||||
"optionTemplate": "<div class=\"flex items-center\"><div class=\"size-8 me-2 flex shrink-0 items-center justify-center text-gray-500 dark:text-neutral-500\" data-icon></div><div><div class=\"text-sm font-semibold text-gray-800 dark:text-neutral-200 \" data-title></div><div class=\"text-xs text-gray-500 dark:text-neutral-500 \" data-description></div></div><div class=\"ms-auto\"><span class=\"hidden hs-selected:block\"><svg class=\"shrink-0 size-4 text-blue-600\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\"><path d=\"M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z\"/></svg></span></div></div>",
|
||||
"extraMarkup": "<div class=\"absolute top-1/2 end-3 -translate-y-1/2\"><svg class=\"shrink-0 size-3.5 text-gray-500 dark:text-neutral-500 \" xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m7 15 5 5 5-5\"/><path d=\"m7 9 5-5 5 5\"/></svg></div>"
|
||||
}' class="hidden">
|
||||
<option value="">Choose</option>
|
||||
<option>Filter 1</option>
|
||||
<option>Filter 2</option>
|
||||
<option>Filter 3</option>
|
||||
</select>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-neutral-500">
|
||||
Filter out unwanted content.
|
||||
</p>
|
||||
</div>
|
||||
<label for="formActive" class="flex gap-4">
|
||||
<input type="checkbox" id="formActive" name="active" class="form-radio relative w-[3.25rem] h-7 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600 before:inline-block before:size-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow-sm before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-blue-200">
|
||||
<span class="flex flex-col">
|
||||
<span class="block text-sm dark:text-neutral-400">Active</span>
|
||||
<span class="block text-sm text-gray-500 dark:text-neutral-500">Inactive entries will not be processed.</span>
|
||||
</span>
|
||||
</label>
|
||||
</form>
|
||||
<div class="flex items-center gap-x-2 mt-8">
|
||||
<button type="button" class="me-auto py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||
Templates
|
||||
</button>
|
||||
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" data-hs-overlay="#subModal">
|
||||
Close
|
||||
</button>
|
||||
<button type="submit" form="subForm" class="group-invalid:pointer-events-none group-invalid:opacity-30 py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-hidden focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Popup -->
|
||||
|
||||
<script>
|
||||
var guildId = "<%- guild.id %>"
|
||||
var channels = <%- JSON.stringify(
|
||||
guild.channels.cache
|
||||
.filter(channel => channel.type == 0)
|
||||
.sort((a, b) => a.rawPosition - b.rawPosition)
|
||||
.map(channel => channel.toJSON())
|
||||
) %>;
|
||||
</script>
|
||||
|
||||
<% block("scripts").append('<script defer src="/static/js/guild/subscriptions.js"></script>'); %>
|
46
src/client/views/home.ejs
Normal file
46
src/client/views/home.ejs
Normal file
@ -0,0 +1,46 @@
|
||||
<% layout("layout") -%>
|
||||
|
||||
<div class="p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<h1 class="text-4xl">Home Page</h1>
|
||||
|
||||
<p>
|
||||
This is some placeholder text.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Nulla aliquid temporibus sed odit blanditiis, repellendus id quibusdam voluptas harum vel culpa eligendi nihil corporis exercitationem ipsam a, deserunt voluptatibus soluta.
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Animi fugit necessitatibus assumenda eum. Sint est officiis ad laborum, dolores, illum explicabo error, doloribus repudiandae animi quis quam mollitia expedita fugiat!
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Rerum asperiores provident et expedita ea unde vitae aut blanditiis, beatae sed rem a ad, quis quia dolorem ullam omnis magni sit?
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Optio ipsum voluptatibus enim laboriosam dolorem eveniet. Vitae cumque recusandae molestias molestiae necessitatibus nam quidem incidunt officia dignissimos, facilis, mollitia quaerat ad.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Cupiditate hic odio non ad expedita itaque accusantium nostrum. Veritatis provident quaerat, quibusdam ullam possimus beatae corrupti repudiandae! Accusantium dolorem enim cupiditate?
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur ullam exercitationem voluptas ducimus, veritatis repellat mollitia inventore recusandae tempore numquam voluptatibus dolores perferendis minima ut omnis, corrupti deserunt. Debitis, fuga?
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero natus quod iure id cum a eligendi et laudantium in incidunt, consequatur perspiciatis aliquid aliquam eaque debitis deserunt. Alias, accusamus veniam.
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia, laudantium. Reprehenderit beatae autem illum et similique nesciunt suscipit? Quo dignissimos delectus minus, distinctio vero nesciunt iusto architecto accusamus eius iste.
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolor harum vero, ut laboriosam error dolores itaque animi quo accusamus consectetur. Distinctio pariatur quo iste dolorem a nobis eum fuga delectus.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sit cumque rerum commodi aut modi maiores, incidunt suscipit odit? Natus aspernatur amet, est sapiente cupiditate consequuntur nam rerum autem accusamus culpa!
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Facere quaerat rem amet, laboriosam eius facilis minus pariatur magnam ea molestiae totam vitae suscipit veniam nihil explicabo voluptatibus sequi deleniti. Earum!
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nam dolores exercitationem voluptas qui dolor consequuntur nostrum quidem, similique, incidunt voluptatum cum nulla. Quis odio ducimus cum eos saepe officiis at.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Magni ratione quaerat, sequi nostrum eius odio corrupti deserunt corporis, voluptates ex quas hic laborum non consequatur neque commodi, fuga inventore assumenda.
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quibusdam aperiam distinctio numquam esse perferendis, placeat, pariatur consequatur nulla atque amet voluptatem molestiae quasi veniam quis sunt similique, modi ipsam! Consectetur!
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Illo consequuntur neque numquam quam cumque dolores debitis dolorum exercitationem ratione optio fuga libero itaque, et ut. Tempore repellendus sit impedit illo.
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet velit natus inventore unde rerum nemo, aliquam cum nihil mollitia eligendi. Iusto aliquam ea molestias, nobis nostrum expedita delectus neque saepe.
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos odit sint cumque et perspiciatis debitis magnam inventore ratione magni, architecto sequi vel ipsum sunt id minus vitae necessitatibus, neque voluptatem!
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iste, delectus facere enim veritatis reiciendis esse non ex perspiciatis explicabo incidunt pariatur fuga impedit officiis doloribus commodi quam deleniti fugiat modi.
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iste odit consequuntur neque, voluptate aut similique cupiditate ex asperiores excepturi unde sed totam sapiente, placeat praesentium sit corrupti officia minima veritatis.
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolorum rem quidem omnis fuga aspernatur, error qui maiores blanditiis rerum suscipit enim, totam vero, eligendi beatae laudantium inventore cumque mollitia impedit.
|
||||
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Tenetur, maxime soluta consequuntur est ullam quia dolor, consequatur quasi ut porro ipsam! Maxime labore obcaecati dolorum, reprehenderit sunt repudiandae consequuntur assumenda.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Vitae atque explicabo consequuntur quo totam similique magni laboriosam, reprehenderit quos deleniti pariatur ab esse sed fugiat accusantium veritatis, voluptas possimus ut?
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Odio ducimus esse quae ad sapiente possimus quisquam dolorum ab error, voluptatum libero eveniet tenetur dignissimos id sunt, ullam, facere corrupti eaque.
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus laborum id temporibus quis eveniet. Accusantium consequatur, beatae et facilis natus pariatur vitae aliquam possimus. Enim ratione odio harum eos hic?
|
||||
</p>
|
||||
</div>
|
30
src/client/views/layout.ejs
Normal file
30
src/client/views/layout.ejs
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="/static/css/tailwind.css">
|
||||
</head>
|
||||
<body class="bg-neutral-100 dark:bg-neutral-900 text-gray-600 dark:text-gray-400 font-[Inter]">
|
||||
|
||||
<%- include("sidebar") -%>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="w-full lg:ps-64">
|
||||
<%- body -%>
|
||||
</div>
|
||||
<!-- End Content -->
|
||||
|
||||
<!-- <%- include("footer") -%> -->
|
||||
|
||||
<script type="text/javascript" src="/static/foreign/jquery.js"></script>
|
||||
<script type="text/javascript" src="/static/foreign/dataTables.js"></script>
|
||||
<script type="text/javascript" src="/static/foreign/dataTablesSelect.js"></script>
|
||||
<script type="text/javascript" src="/static/foreign/floatingUiCore.js"></script>
|
||||
<script type="text/javascript" src="/static/foreign/floatingUiDOM.js"></script>
|
||||
<script type="text/javascript" src="/static/foreign/preline.js"></script>
|
||||
<script type="text/javascript" src="/static/js/main.js"></script>
|
||||
<%- block("scripts").toString() %>
|
||||
</body>
|
||||
</html>
|
38
src/client/views/login.ejs
Normal file
38
src/client/views/login.ejs
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="/static/css/tailwind.css">
|
||||
</head>
|
||||
<body class="h-screen flex justify-center items-center bg-neutral-100 dark:bg-neutral-900 text-gray-600 dark:text-gray-400 font-[Inter]">
|
||||
|
||||
<div class="mt-7 w-[26rem] bg-white border border-gray-200 rounded-xl shadow-xs dark:bg-neutral-900 dark:border-neutral-700">
|
||||
<div class="p-4 sm:p-7">
|
||||
<div class="text-center">
|
||||
<h1 class="block text-2xl font-bold text-gray-800 dark:text-white">Sign in</h1>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
||||
Login is required to use Relay.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<a href="/auth/api" class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800 dark:focus:bg-neutral-800">
|
||||
<svg class="w-4 h-auto" xmlns="http://www.w3.org/2000/svg" width="46" height="46" fill="currentColor" class="bi bi-discord" viewBox="0 0 16 16">
|
||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"/>
|
||||
</svg>
|
||||
Sign in with Discord
|
||||
</a>
|
||||
</div>
|
||||
<% if (error != "") { %>
|
||||
<%= error %>
|
||||
<% } if (success != "") { %>
|
||||
<%= success %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/preline.js"></script>
|
||||
</body>
|
||||
</html>
|
144
src/client/views/sidebar.ejs
Normal file
144
src/client/views/sidebar.ejs
Normal file
@ -0,0 +1,144 @@
|
||||
<!-- Mobile Sidebar Controls -->
|
||||
<header class="mobile-sidebar-header">
|
||||
<nav class="mobile-sidebar-nav">
|
||||
<a href="/" class="mobile-sidebar-brand">Relay</a>
|
||||
<button type="button" class="ml-auto" data-hs-overlay="#layoutSidebarMain">
|
||||
<svg class="shrink-0 size-6" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<!-- End Mobile Sidebar Controls -->
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside id="layoutSidebarMain" class="sidebar-aside main-sidebar hs-overlay hs-overlay-open:!translate-x-0" role="dialog" tabindex="-1">
|
||||
<div class="sidebar-container">
|
||||
<!-- Sidebar Header -->
|
||||
<div class="sidebar-header">
|
||||
<a href="/" class="sidebar-header-brand">
|
||||
Relay
|
||||
</a>
|
||||
<button type="button" class="lg:hidden" data-hs-overlay="#layoutSidebarMain">
|
||||
<svg class="shrink-0 size-5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- End Sidebar Header -->
|
||||
<!-- Sidebar Content -->
|
||||
<div class="sidebar-content">
|
||||
<nav class="sidebar-nav">
|
||||
<ul class="flex flex-col space-y-1">
|
||||
<li>
|
||||
<a href="/" class="sidebar-btn">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="sidebar-btn" data-hs-overlay="#layoutSidebarGuilds">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
|
||||
Servers
|
||||
|
||||
<svg class="block ms-auto size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
||||
<svg class="hidden ms-auto size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://gitea.cor.bz/corbz/pyrss-ng" class="sidebar-btn" target="_blank">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
|
||||
Source Code
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://gitea.cor.bz/corbz/pyrss-ng" class="sidebar-btn" target="_blank">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
||||
Help
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="sidebar-btn">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<!-- End Sidebar Content -->
|
||||
<!-- Sidebar Footer -->
|
||||
<div class="mt-auto p-2 border-t border-gray-200 dark:border-neutral-700">
|
||||
<div class="hs-dropdown [--strategy:absolute] [--auto-close:inside] relative w-full inline-flex">
|
||||
<button type="button" id="userMenu" class="w-full inline-flex shrink-0 items-center gap-x-2 p-2 text-start text-sm text-gray-800 rounded-lg hover:bg-gray-100 focus:outline-hidden focus:bg-gray-100 dark:text-neutral-200 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||
<img class="shrink-0 size-5 rounded-full" src="https://cdn.discordapp.com/avatars/<%= user.id %>/<%= user.avatar %>" alt="Avatar">
|
||||
<%= user.username %>
|
||||
<svg class="block hs-dropdown-open:hidden ms-auto size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>
|
||||
<svg class="hidden hs-dropdown-open:block ms-auto size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div class="hs-dropdown-menu hs-dropdown-open:opacity-100 mb-2! w-60 transition-[opacity,margin] duration opacity-0 hidden z-20 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-neutral-900 dark:border-neutral-700" role="menu">
|
||||
<!-- <div class="p-1">
|
||||
<a href="#" class="flex items-center gap-x-3 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800">
|
||||
Settings
|
||||
</a>
|
||||
</div>
|
||||
<hr class="border-gray-200 dark:border-neutral-700"> -->
|
||||
<div class="p-1">
|
||||
<div class="flex justify-between items-center py-1 px-3">
|
||||
<label for="themeSwitch" class="text-sm text-gray-800 dark:text-neutral-300">Dark mode</label>
|
||||
<div class="relative inline-block">
|
||||
<input type="checkbox" name="themeSwitch" id="themeSwitch" class="form-radio relative w-11 h-6 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600 before:inline-block before:size-5 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow-sm before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-blue-200" data-hs-theme-switch="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-gray-200 dark:border-neutral-700">
|
||||
<div class="p-1">
|
||||
<a href="/auth/logout" class="flex items-center gap-x-3 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none focus:outline-hidden focus:bg-gray-100 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800">
|
||||
Sign out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Sidebar Footer -->
|
||||
</div>
|
||||
</aside>
|
||||
<!-- End Sidebar -->
|
||||
|
||||
<!-- Guild Sidebar -->
|
||||
<aside id="layoutSidebarGuilds" class="sidebar-aside guild-sidebar hs-overlay hs-overlay-open:!translate-x-0 [--is-layout-affect:true]" role="dialog" tabindex="-1">
|
||||
<div class="sidebar-container">
|
||||
<!-- Guild Sidebar Header -->
|
||||
<div class="sidebar-header">
|
||||
<span class="flex-none text-gray-600 dark:text-gray-400 text-sm font-semibold">
|
||||
Discord Servers
|
||||
</span>
|
||||
<button type="button" data-hs-overlay="#layoutSidebarGuilds">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- End Guild Sidebar Header -->
|
||||
<!-- Guild Sidebar Content -->
|
||||
<div class="sidebar-content">
|
||||
<nav class="sidebar-nav">
|
||||
<ul class="flex flex-col space-y-1">
|
||||
<% guilds.forEach(guild => { %>
|
||||
<li>
|
||||
<a href="/guild/<%= guild.id %>" class="sidebar-btn">
|
||||
<% if (guild.icon) { %>
|
||||
<img class="size-[28px] rounded-sm" src="<%= guild.iconURL() %>" alt="<%= guild.name %>">
|
||||
<% } else { %>
|
||||
<div class="size-[28px] flex shrink-0 justify-center items-center rounded-sm bg-neutral-100 dark:bg-neutral-900">
|
||||
<span class="text-xs"><%= guild.name.split(" ").slice(0, 2).map(word => word[0].toUpperCase()).join(""); %></span>
|
||||
</div>
|
||||
<% } %>
|
||||
<div>
|
||||
<span class="text-base"><%= guild.name %></span>
|
||||
<!-- <span class="text-sm font-mono text-gray-600 dark:text-gray-400"><%= guild.id %></span> -->
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<!-- End Guild Sidebar Content -->
|
||||
</div>
|
||||
</aside>
|
||||
<!-- End Guild Sidebar -->
|
4
src/db/db.ts
Normal file
4
src/db/db.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import knex from "knex";
|
||||
import knexConfig from "@server/../knexfile";
|
||||
|
||||
export const db = knex(knexConfig);
|
@ -0,0 +1,19 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
const TABLE = "subscriptions";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable(TABLE, table => {
|
||||
table.increments("id").primary();
|
||||
table.string("name").notNullable();
|
||||
table.string("url").notNullable();
|
||||
table.string("guild_id").notNullable();
|
||||
table.boolean("active").notNullable().defaultTo(true);
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TABLE);
|
||||
}
|
||||
|
21
src/db/migrations/20250203221106_create_channels.ts
Normal file
21
src/db/migrations/20250203221106_create_channels.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
const TABLE = "channels";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable(TABLE, table => {
|
||||
table.increments("id").primary();
|
||||
table.string("channel_id").notNullable();
|
||||
table.integer("subscription_id")
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references("id").inTable("subscriptions")
|
||||
.onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TABLE);
|
||||
}
|
||||
|
25
src/db/migrations/20250213221612_create_filters.ts
Normal file
25
src/db/migrations/20250213221612_create_filters.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
const TABLE = "filters";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable(TABLE, table => {
|
||||
table.increments("id").primary();
|
||||
table.string("guild_id").notNullable();
|
||||
table.string("name").notNullable();
|
||||
table.string("match").notNullable();
|
||||
table.enum("algorithm", [
|
||||
"any", "all", "exact",
|
||||
"regex", "fuzzy"
|
||||
]).notNullable();
|
||||
table.boolean("is_insensitive").notNullable();
|
||||
table.boolean("is_whitelist").notNullable();
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TABLE);
|
||||
}
|
||||
|
12
src/db/models/filters.model.ts
Normal file
12
src/db/models/filters.model.ts
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
export interface Filter {
|
||||
id: number;
|
||||
guild_id: string;
|
||||
name: string;
|
||||
match: string;
|
||||
algorithm: string;
|
||||
is_insensitive: boolean;
|
||||
is_whitelist: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
18
src/db/models/subs.model.ts
Normal file
18
src/db/models/subs.model.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Url } from "url";
|
||||
|
||||
export interface Subscription {
|
||||
id: number;
|
||||
name: string;
|
||||
url: Url;
|
||||
guild_id: string;
|
||||
active: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
channels?: string[];
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
id: number;
|
||||
channel_id: string;
|
||||
subscription_id: number;
|
||||
}
|
42
src/db/seeds/test_channel.ts
Normal file
42
src/db/seeds/test_channel.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { subscribe } from "diagnostics_channel";
|
||||
import { Knex, SchemaBuilder } from "knex";
|
||||
|
||||
const TABLE = "channels";
|
||||
|
||||
export async function seed(knex: Knex): Promise<void> {
|
||||
// Deletes ALL existing entries
|
||||
await knex(TABLE).del();
|
||||
|
||||
// Inserts seed entries
|
||||
await knex(TABLE).insert([
|
||||
{
|
||||
channel_id: "1204426363440861207",
|
||||
subscription_id: 1,
|
||||
},
|
||||
{
|
||||
channel_id: "1204540472031322192",
|
||||
subscription_id: 1,
|
||||
},
|
||||
{
|
||||
channel_id: "819332348214247471",
|
||||
subscription_id: 10
|
||||
},
|
||||
{
|
||||
channel_id: "819325370087112747",
|
||||
subscription_id: 10
|
||||
},
|
||||
{
|
||||
channel_id: "819334448268181515",
|
||||
subscription_id: 14
|
||||
},
|
||||
{
|
||||
channel_id: "819325370087112747",
|
||||
subscription_id: 14
|
||||
},
|
||||
{
|
||||
channel_id: "918648963492618261",
|
||||
subscription_id: 14
|
||||
}
|
||||
|
||||
]);
|
||||
};
|
123
src/db/seeds/test_sub.ts
Normal file
123
src/db/seeds/test_sub.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
const TABLE = "subscriptions";
|
||||
|
||||
export async function seed(knex: Knex): Promise<void> {
|
||||
// Deletes ALL existing entries
|
||||
await knex(TABLE).del();
|
||||
|
||||
// Inserts seed entries
|
||||
await knex(TABLE).insert([
|
||||
// pyrss guild
|
||||
{
|
||||
name: "BBC News · Home",
|
||||
url: "http://feeds.bbci.co.uk/news/rss.xml",
|
||||
guild_id: "1204426362794811453",
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: "BBC News · World",
|
||||
url: "http://feeds.bbci.co.uk/news/world/rss.xml",
|
||||
guild_id: "1204426362794811453",
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: "BBC News · Entertainment",
|
||||
url: "http://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml",
|
||||
guild_id: "1204426362794811453",
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: "BBC News · Technology",
|
||||
url: "https://feeds.bbci.co.uk/news/technology/rss.xml",
|
||||
guild_id: "1204426362794811453",
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: "BBC News · Science & Environment",
|
||||
url: "https://feeds.bbci.co.uk/news/science_and_environment/rss.xml",
|
||||
guild_id: "1204426362794811453",
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: "BBC News · Health",
|
||||
url: "https://feeds.bbci.co.uk/news/health/rss.xml",
|
||||
guild_id: "1204426362794811453",
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: "BBC News · Politics",
|
||||
url: "https://feeds.bbci.co.uk/news/politics/rss.xml",
|
||||
guild_id: "1204426362794811453",
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: "BBC News · US & Canada",
|
||||
url: "https://feeds.bbci.co.uk/news/world/us_and_canada/rss.xml",
|
||||
guild_id: "1204426362794811453",
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: "TechRadar · Phones",
|
||||
url: "https://www.techradar.com/uk/feeds/tag/phones",
|
||||
guild_id: "1204426362794811453",
|
||||
active: true
|
||||
},
|
||||
|
||||
// bot testing guild
|
||||
{
|
||||
name: "BBC News · Home",
|
||||
url: "http://feeds.bbci.co.uk/news/rss.xml",
|
||||
guild_id: "819325370087112744",
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: "BBC News · World",
|
||||
url: "http://feeds.bbci.co.uk/news/world/rss.xml",
|
||||
guild_id: "819325370087112744",
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: "BBC News · Entertainment",
|
||||
url: "http://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml",
|
||||
guild_id: "819325370087112744",
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: "BBC News · Technology",
|
||||
url: "https://feeds.bbci.co.uk/news/technology/rss.xml",
|
||||
guild_id: "819325370087112744",
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: "BBC News · Science & Environment",
|
||||
url: "https://feeds.bbci.co.uk/news/science_and_environment/rss.xml",
|
||||
guild_id: "819325370087112744",
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: "BBC News · Health",
|
||||
url: "https://feeds.bbci.co.uk/news/health/rss.xml",
|
||||
guild_id: "819325370087112744",
|
||||
active: true
|
||||
},
|
||||
{
|
||||
name: "BBC News · Politics",
|
||||
url: "https://feeds.bbci.co.uk/news/politics/rss.xml",
|
||||
guild_id: "819325370087112744",
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: "BBC News · US & Canada",
|
||||
url: "https://feeds.bbci.co.uk/news/world/us_and_canada/rss.xml",
|
||||
guild_id: "819325370087112744",
|
||||
active: false
|
||||
},
|
||||
{
|
||||
name: "TechRadar · Phones",
|
||||
url: "https://www.techradar.com/uk/feeds/tag/phones",
|
||||
guild_id: "819325370087112744",
|
||||
active: true
|
||||
},
|
||||
]);
|
||||
};
|
47
src/knexfile.ts
Normal file
47
src/knexfile.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import path from "path";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config(); // I env vars are already loaded, but this file is invoked directly sometimes.
|
||||
|
||||
const {
|
||||
DB_CLIENT = "sqlite", // Default:
|
||||
SQLITE_FILE = path.resolve(__dirname, "../db.sqlite"), // use sqlite if not specified
|
||||
PG_HOST = "",
|
||||
PG_PORT = "",
|
||||
PG_USER = "",
|
||||
PG_PASSWORD = "",
|
||||
PG_DATABASE = ""
|
||||
} = process.env;
|
||||
|
||||
const dbConfig = {
|
||||
sqlite: {
|
||||
client: "sqlite3",
|
||||
connection: {
|
||||
filename: SQLITE_FILE
|
||||
},
|
||||
useNullAsDefault: true
|
||||
},
|
||||
postgresql: {
|
||||
client: "pg",
|
||||
connection: {
|
||||
host: PG_HOST,
|
||||
port: PG_PORT,
|
||||
user: PG_USER,
|
||||
password: PG_PASSWORD,
|
||||
database: PG_DATABASE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const knexConfig = {
|
||||
...dbConfig[DB_CLIENT as keyof typeof dbConfig],
|
||||
migrations: {
|
||||
tableName: "knex_migrations",
|
||||
directory: path.resolve(__dirname, "./db/migrations")
|
||||
},
|
||||
seeds: {
|
||||
directory: path.resolve(__dirname, "./db/seeds")
|
||||
}
|
||||
}
|
||||
|
||||
export default knexConfig;
|
54
src/server/controllers/auth.web.controller.ts
Normal file
54
src/server/controllers/auth.web.controller.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import passport, { PassportStatic } from "passport"
|
||||
import { Strategy, Profile } from "passport-discord";
|
||||
|
||||
export const get = async (_request: Request, response: Response) => {
|
||||
response.render("login", {
|
||||
title: "Login - Relay"
|
||||
});
|
||||
};
|
||||
|
||||
export const authenticate = async (request: Request, response: Response, next: NextFunction) => {
|
||||
passport.authenticate("discord", {
|
||||
successRedirect: "/",
|
||||
failureRedirect: "/auth/login",
|
||||
failureFlash: true
|
||||
})(request, response, next);
|
||||
};
|
||||
|
||||
export const logout = async (request: Request, response: Response, next: NextFunction) => {
|
||||
request.logout(error => {
|
||||
if (error) { return next(); }
|
||||
response.redirect("/");
|
||||
});
|
||||
};
|
||||
|
||||
export const setupPassport = (passport: PassportStatic) => {
|
||||
const scopes: Array<string> = ["identify"];
|
||||
const authorisedUserIds: Array<string> = process.env.DISCORD_USER_IDS?.split("/") || [];
|
||||
passport.use(new Strategy({
|
||||
clientID: process.env.CLIENT_ID || "",
|
||||
clientSecret: process.env.CLIENT_SECRET || "",
|
||||
callbackURL: process.env.CALLBACK_URL || "",
|
||||
scope: scopes
|
||||
},
|
||||
(_accessToken: string, _refreshToken: string, profile: Profile, callback: CallableFunction) => {
|
||||
if (authorisedUserIds.includes(profile.id)) {
|
||||
return callback(null, profile);
|
||||
}
|
||||
|
||||
return callback(null, false, {
|
||||
"message": "User ID is not authorised!"
|
||||
})
|
||||
}));
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user);
|
||||
});
|
||||
|
||||
passport.deserializeUser((user: any, done) => {
|
||||
done(null, user);
|
||||
});
|
||||
}
|
||||
|
||||
export default { get, authenticate, logout };
|
19
src/server/controllers/guild/content.web.controller.ts
Normal file
19
src/server/controllers/guild/content.web.controller.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Request, Response } from "express";
|
||||
import { client as bot } from "@bot/bot";
|
||||
|
||||
export const get = async (request: Request, response: Response) => {
|
||||
const guildId = request.params.guildId;
|
||||
const guild = bot.guilds.cache.get(guildId);
|
||||
|
||||
if (!guild) {
|
||||
response.status(404).send("404: guild not found");
|
||||
return;
|
||||
}
|
||||
|
||||
response.render("guild/content", {
|
||||
title: `${guild.name} - Relay`,
|
||||
guild: guild,
|
||||
});
|
||||
};
|
||||
|
||||
export default { get };
|
88
src/server/controllers/guild/filter.api.controller.ts
Normal file
88
src/server/controllers/guild/filter.api.controller.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { Request, Response } from "express";
|
||||
import { buildDatatableQuery } from "@utils/datatable";
|
||||
import { db } from "@db/db";
|
||||
import { Filter } from "@db/models/filters.model";
|
||||
|
||||
const isPostgres = db.client.config.client === "pg";
|
||||
const TABLE = "filters";
|
||||
|
||||
export const datatable = async (request: Request, response: Response) => {
|
||||
try {
|
||||
|
||||
let query = db(TABLE).where({ guild_id: request.params.guildId });
|
||||
|
||||
const datatableQuery = await buildDatatableQuery(request as any, query, TABLE);
|
||||
const { recordsTotal, recordsFiltered } = datatableQuery;
|
||||
const data = await datatableQuery.query;
|
||||
|
||||
console.debug(`total: ${recordsTotal} filtered: ${recordsFiltered} filtered+paged: ${data.length}`);
|
||||
|
||||
response.json({
|
||||
data,
|
||||
recordsFiltered,
|
||||
recordsTotal,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({ error: "Failed to fetch datatable for filters" });
|
||||
}
|
||||
}
|
||||
|
||||
export const get = async (request: Request, response: Response) => {
|
||||
try {
|
||||
if (!request.query.id) {
|
||||
response.status(400).json({ error: "missing 'id' query" });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await db("filters")
|
||||
.select("filters.*")
|
||||
.where({ "filters.id": request.query.id })
|
||||
.first();
|
||||
|
||||
if (!data) {
|
||||
response.status(404).json({ message: "no result found" });
|
||||
return;
|
||||
}
|
||||
|
||||
response.json(data);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({ error: "Failed to fetch filter" });
|
||||
}
|
||||
}
|
||||
|
||||
export const post = async (request: Request, response: Response) => {
|
||||
try {
|
||||
console.debug(JSON.stringify(request.body, null, 4));
|
||||
|
||||
const guild_id = request.params.guildId;
|
||||
const { name, match, algorithm, is_insensitive, is_whitelist } = request.body;
|
||||
|
||||
if (!name || !match || !algorithm) {
|
||||
response.status(400).json({ error: "Missing required fields" });
|
||||
return;
|
||||
}
|
||||
|
||||
const [filter] = await db<Filter>(TABLE)
|
||||
.insert({
|
||||
guild_id,
|
||||
name,
|
||||
match,
|
||||
algorithm,
|
||||
is_insensitive: is_insensitive == "on",
|
||||
is_whitelist: is_whitelist == "on"
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
response.status(201).json(filter);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({ error: "Failed to create Filter" });
|
||||
}
|
||||
}
|
||||
|
||||
export default { datatable, get, post }
|
19
src/server/controllers/guild/filter.web.controller.ts
Normal file
19
src/server/controllers/guild/filter.web.controller.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Request, Response } from "express";
|
||||
import { client as bot } from "@bot/bot";
|
||||
|
||||
export const get = async (request: Request, response: Response) => {
|
||||
const guildId = request.params.guildId;
|
||||
const guild = bot.guilds.cache.get(guildId);
|
||||
|
||||
if (!guild) {
|
||||
response.status(404).send("404: guild not found");
|
||||
return;
|
||||
}
|
||||
|
||||
response.render("guild/filters", {
|
||||
title: `${guild.name} - Relay`,
|
||||
guild: guild,
|
||||
});
|
||||
};
|
||||
|
||||
export default { get };
|
23
src/server/controllers/guild/index.web.controller.ts
Normal file
23
src/server/controllers/guild/index.web.controller.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Request, Response } from "express";
|
||||
import { client as bot } from "@bot/bot";
|
||||
|
||||
export const get = async (request: Request, response: Response) => {
|
||||
const guildId = request.params.guildId;
|
||||
|
||||
response.redirect(`/guild/${guildId}/subscriptions`);
|
||||
return
|
||||
|
||||
// const guild = bot.guilds.cache.get(guildId);
|
||||
|
||||
// if (!guild) {
|
||||
// response.status(404).send("404: guild not found");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// response.render("guild/overview", {
|
||||
// title: `${guild.name} - Relay`,
|
||||
// guild: guild,
|
||||
// });
|
||||
};
|
||||
|
||||
export default { get };
|
19
src/server/controllers/guild/style.web.controller.ts
Normal file
19
src/server/controllers/guild/style.web.controller.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Request, Response } from "express";
|
||||
import { client as bot } from "@bot/bot";
|
||||
|
||||
export const get = async (request: Request, response: Response) => {
|
||||
const guildId = request.params.guildId;
|
||||
const guild = bot.guilds.cache.get(guildId);
|
||||
|
||||
if (!guild) {
|
||||
response.status(404).send("404: guild not found");
|
||||
return;
|
||||
}
|
||||
|
||||
response.render("guild/styles", {
|
||||
title: `${guild.name} - Relay`,
|
||||
guild: guild,
|
||||
});
|
||||
};
|
||||
|
||||
export default { get };
|
156
src/server/controllers/guild/sub.api.controller.ts
Normal file
156
src/server/controllers/guild/sub.api.controller.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { Request, Response } from "express";
|
||||
import { buildDatatableQuery } from "@utils/datatable";
|
||||
import { db } from "@db/db";
|
||||
import { Subscription, Channel } from "@db/models/subs.model";
|
||||
|
||||
const isPostgres = db.client.config.client === "pg";
|
||||
const TABLE = "subscriptions";
|
||||
|
||||
export const datatable = async (request: Request, response: Response) => {
|
||||
try {
|
||||
|
||||
let query = db(TABLE).where({ guild_id: request.params.guildId });
|
||||
|
||||
const datatableQuery = await buildDatatableQuery(request as any, query, TABLE);
|
||||
const { recordsTotal, recordsFiltered } = datatableQuery;
|
||||
query = datatableQuery.query;
|
||||
|
||||
query.select("subscriptions.*")
|
||||
.leftJoin("channels", "subscriptions.id", "=", "channels.subscription_id")
|
||||
.select(db.raw(isPostgres
|
||||
? "json_agg(channels.channel_id) as channels"
|
||||
:"JSON_GROUP_ARRAY(channels.channel_id) as channels"
|
||||
))
|
||||
.groupBy("subscriptions.id")
|
||||
|
||||
|
||||
const data = await query;
|
||||
|
||||
console.debug(`total: ${recordsTotal} filtered: ${recordsFiltered} filtered+paged: ${data.length}`);
|
||||
|
||||
data.forEach((item: any) => {
|
||||
item.channels = item.channels === "[null]"
|
||||
? []
|
||||
: JSON.parse(item.channels);
|
||||
});
|
||||
|
||||
response.json({
|
||||
data,
|
||||
recordsFiltered,
|
||||
recordsTotal,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({ error: "Failed to fetch datatable for subscriptions" });
|
||||
}
|
||||
}
|
||||
|
||||
export const get = async (request: Request, response: Response) => {
|
||||
try {
|
||||
if (!request.query.id) {
|
||||
response.status(400).json({ error: "missing 'id' query" });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await db("subscriptions")
|
||||
.leftJoin("channels", "subscriptions.id", "=", "channels.subscription_id")
|
||||
.select("subscriptions.*")
|
||||
.select(db.raw(isPostgres
|
||||
? "json_agg(channels.channel_id) as channels"
|
||||
:"JSON_GROUP_ARRAY(channels.channel_id) as channels"
|
||||
))
|
||||
.groupBy("subscriptions.id")
|
||||
.where({ "subscriptions.id": request.query.id })
|
||||
.first();
|
||||
|
||||
if (!data) {
|
||||
response.status(404).json({ message: "no result found" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse empty channel arrays
|
||||
data.channels = data.channels === "[null]"
|
||||
? []
|
||||
: JSON.parse(data.channels);
|
||||
|
||||
response.json(data);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({ error: "Failed to fetch subscription" });
|
||||
}
|
||||
}
|
||||
|
||||
export const post = async (request: Request, response: Response) => {
|
||||
try {
|
||||
console.debug(JSON.stringify(request.body, null, 4));
|
||||
|
||||
const guild_id = request.params.guildId;
|
||||
const { name, url, message_style, published_threshold, channels, filters, active } = request.body;
|
||||
|
||||
if (!name || !url || !message_style || !published_threshold) {
|
||||
response.status(400).json({ error: "Missing required fields" });
|
||||
return;
|
||||
}
|
||||
|
||||
const [subscription] = await db<Subscription>(TABLE)
|
||||
.insert({
|
||||
name,
|
||||
url,
|
||||
guild_id,
|
||||
active: active === "on",
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
if (channels && channels.length) {
|
||||
const channelData = channels.map((channel_id: string) => ({
|
||||
channel_id,
|
||||
subscription_id: subscription.id
|
||||
}));
|
||||
|
||||
await db<Channel>("channels").insert(channelData);
|
||||
}
|
||||
|
||||
response.status(201).json({
|
||||
...subscription,
|
||||
channels: channels ?? []
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({ error: "Failed to create subscription" });
|
||||
}
|
||||
}
|
||||
|
||||
export const del = async (request: Request, response: Response) => {
|
||||
try {
|
||||
const { ids } = request.body;
|
||||
const guild_id = request.params.guildId;
|
||||
console.log(JSON.stringify(ids));
|
||||
console.log(`deleting subs: ${ids}`);
|
||||
|
||||
if (!ids || !Array.isArray(ids)) {
|
||||
response.status(400).json({ error: "invalid request body" });
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriptionsToDelete = await db<Subscription>(TABLE).whereIn("id", ids).select("guild_id");
|
||||
|
||||
if (subscriptionsToDelete.some(sub => sub.guild_id !== guild_id)) {
|
||||
response.status(400).json({ error: `Some subscriptions do not belong to guild: ${guild_id}` });
|
||||
return;
|
||||
}
|
||||
|
||||
await db(TABLE).whereIn("id", ids).andWhere("guild_id", guild_id).delete();
|
||||
response.status(204).json(null);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
response.status(500).json({ error: "Failed to delete subscription" })
|
||||
}
|
||||
}
|
||||
|
||||
export default { datatable, get, post, del }
|
21
src/server/controllers/guild/sub.web.controller.ts
Normal file
21
src/server/controllers/guild/sub.web.controller.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Request, Response } from "express";
|
||||
import { client as bot } from "@bot/bot";
|
||||
|
||||
export const get = async (request: Request, response: Response) => {
|
||||
const guildId = request.params.guildId;
|
||||
const guild = bot.guilds.cache.get(guildId);
|
||||
|
||||
if (!guild) {
|
||||
response.status(404).send("404: guild not found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(guild.toJSON());
|
||||
|
||||
response.render("guild/subscriptions", {
|
||||
title: `${guild.name} - Relay`,
|
||||
guild: guild,
|
||||
});
|
||||
}
|
||||
|
||||
export default { get }
|
9
src/server/controllers/home.web.controller.ts
Normal file
9
src/server/controllers/home.web.controller.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Request, Response } from "express";
|
||||
|
||||
export const get = async (_request: Request, response: Response) => {
|
||||
response.render("home", {
|
||||
title: "Dashboard - Relay"
|
||||
});
|
||||
};
|
||||
|
||||
export default { get };
|
7
src/server/middleware/attachGuilds.ts
Normal file
7
src/server/middleware/attachGuilds.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { client as bot } from "@bot/bot";
|
||||
|
||||
export const attachGuilds = (_request: Request, response: Response, next: NextFunction) => {
|
||||
response.locals.guilds = bot.guilds.cache.map(guild => guild);
|
||||
next();
|
||||
};
|
6
src/server/middleware/attachUser.ts
Normal file
6
src/server/middleware/attachUser.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const attachUser = (request: Request, response: Response, next: NextFunction) => {
|
||||
response.locals.user = request.user;
|
||||
next();
|
||||
};
|
17
src/server/middleware/authenticated.ts
Normal file
17
src/server/middleware/authenticated.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const ensureAuthenticated = (request: Request, response: Response, next: NextFunction) => {
|
||||
if (request.isAuthenticated()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
response.redirect("/auth/login");
|
||||
};
|
||||
|
||||
export const forwardAuthenticated = (request: Request, response: Response, next: NextFunction) => {
|
||||
if (request.isAuthenticated()) {
|
||||
return response.redirect("/");
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
7
src/server/middleware/flash.ts
Normal file
7
src/server/middleware/flash.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const flashMiddleware = (request: Request, response: Response, next: NextFunction) => {
|
||||
response.locals.success = request.flash("success");
|
||||
response.locals.error = request.flash("error");
|
||||
next();
|
||||
};
|
7
src/server/middleware/guildPage.ts
Normal file
7
src/server/middleware/guildPage.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
export const getGuildPage = (request: Request, response: Response, next: NextFunction) => {
|
||||
const currentPath = request.path.split("/");
|
||||
response.locals.guildPage = currentPath[currentPath.length - 1];
|
||||
next();
|
||||
};
|
11
src/server/routes/auth.web.routes.ts
Normal file
11
src/server/routes/auth.web.routes.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Router } from "express";
|
||||
import { ensureAuthenticated, forwardAuthenticated } from "@server/middleware/authenticated";
|
||||
import controller from "@server/controllers/auth.web.controller";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/login", forwardAuthenticated, controller.get);
|
||||
router.get("/logout", ensureAuthenticated, controller.logout);
|
||||
router.get("/api", forwardAuthenticated, controller.authenticate);
|
||||
|
||||
export default router;
|
18
src/server/routes/guild.api.routes.ts
Normal file
18
src/server/routes/guild.api.routes.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import subApiController from "@server/controllers/guild/sub.api.controller";
|
||||
import filterApiController from "@server/controllers/guild/filter.api.controller";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/:guildId/subscriptions/api/datatable", subApiController.datatable);
|
||||
router.get("/:guildId/subscriptions/api", subApiController.get);
|
||||
router.post("/:guildId/subscriptions/api", subApiController.post);
|
||||
router.delete("/:guildId/subscriptions/api", subApiController.del);
|
||||
|
||||
router.get("/:guildId/filters/api/datatable", filterApiController.datatable);
|
||||
router.get("/:guildId/filters/api", filterApiController.get);
|
||||
router.post("/:guildId/filters/api", filterApiController.post);
|
||||
// router.delete("/:guildId/filters/api", filterApiController.del);
|
||||
|
||||
export default router;
|
17
src/server/routes/guild.web.routes.ts
Normal file
17
src/server/routes/guild.web.routes.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Router } from "express";
|
||||
|
||||
import { getGuildPage } from "@server/middleware/guildPage";
|
||||
import indexWebController from "@server/controllers/guild/index.web.controller";
|
||||
import subWebController from "@server/controllers/guild/sub.web.controller";
|
||||
import filterWebController from "@server/controllers/guild/filter.web.controller";
|
||||
import styleWebController from "@server/controllers/guild/style.web.controller";
|
||||
import contentWebController from "@server/controllers/guild/content.web.controller";
|
||||
|
||||
const router = Router();
|
||||
router.get("/:guildId", getGuildPage, indexWebController.get);
|
||||
router.get("/:guildId/subscriptions", getGuildPage, subWebController.get);
|
||||
router.get("/:guildId/filters", getGuildPage, filterWebController.get);
|
||||
router.get("/:guildId/styles", getGuildPage, styleWebController.get);
|
||||
router.get("/:guildId/content", getGuildPage, contentWebController.get);
|
||||
|
||||
export default router;
|
8
src/server/routes/home.web.routes.ts
Normal file
8
src/server/routes/home.web.routes.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Router } from "express";
|
||||
import controller from "@server/controllers/home.web.controller";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", controller.get);
|
||||
|
||||
export default router;
|
1
src/types/ejs-mate.d.ts
vendored
Normal file
1
src/types/ejs-mate.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module "ejs-mate";
|
29
src/utils/avatarGen.js
Normal file
29
src/utils/avatarGen.js
Normal file
@ -0,0 +1,29 @@
|
||||
// Generate an avatar from a given string
|
||||
|
||||
const { createCanvas } = require("canvas");
|
||||
|
||||
const generateDefaultAvatar = (inputString, colour=null, bgColour=null, width=200, height=200, fontSize=null) => {
|
||||
if (!inputString) { return; }
|
||||
|
||||
const words = inputString.split(" ").slice(0, 2);
|
||||
const initials = words.map(word => word[0].toUpperCase()).join("");
|
||||
|
||||
const canvas = createCanvas(width, height);
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
// // Background
|
||||
// context.fillStyle = bgColour ? bgColour : "#" + Math.floor(Math.random()*16777215).toString(16);
|
||||
// context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Text
|
||||
const fontSizePixels = fontSize || width * 0.3;
|
||||
context.fillStyle = colour || "#FFFFFF";
|
||||
context.font = `bold ${fontSizePixels}px sans-serif`;
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.fillText(initials, canvas.width / 2, canvas.height / 2);
|
||||
|
||||
return canvas.toDataURL();
|
||||
};
|
||||
|
||||
module.exports = generateDefaultAvatar;
|
73
src/utils/datatable.ts
Normal file
73
src/utils/datatable.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Knex } from "knex";
|
||||
import { db } from "@db/db";
|
||||
|
||||
export interface RequestQuery {
|
||||
length: string;
|
||||
start: string;
|
||||
order: { column: string; dir: string }[];
|
||||
columns: { [key: string]: { data: string; searchable: string } };
|
||||
search: { value: string };
|
||||
filters: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface ResponseQuery {
|
||||
query: Knex.QueryBuilder;
|
||||
recordsTotal: number | string;
|
||||
recordsFiltered: number | string;
|
||||
}
|
||||
|
||||
type FilterConfig = {
|
||||
[key: string]: (value: any) => Knex.QueryBuilder;
|
||||
};
|
||||
|
||||
export const buildDatatableQuery = async (request: { query: RequestQuery }, query: Knex.QueryBuilder, tableName: string): Promise<ResponseQuery> => {
|
||||
const size: number = parseInt(request.query.length) || 10;
|
||||
const start: number = parseInt(request.query.start);
|
||||
const order: string = (
|
||||
request.query.order && request.query.columns
|
||||
[request.query.order[0].column].data
|
||||
) || 'id'
|
||||
const direction: string = (request.query.order && request.query.order[0].dir) || "asc";
|
||||
const search: string = request.query.search.value;
|
||||
|
||||
const recordsTotalResult = await query.clone().count("* as count").first();
|
||||
const recordsTotal = recordsTotalResult ? recordsTotalResult.count : 0;
|
||||
|
||||
const filterConfig: FilterConfig = {
|
||||
active: (value: string) => query.where('active', '=', value),
|
||||
};
|
||||
|
||||
if (request.query.filters) {
|
||||
Object.keys(request.query.filters).forEach((filterName: any) => {
|
||||
const filterValue = request.query.filters[filterName];
|
||||
if (filterConfig[filterName] && filterValue) {
|
||||
query = filterConfig[filterName](filterValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (search) {
|
||||
console.debug("applying search: " + search)
|
||||
console.debug("columns: " + JSON.stringify(request.query.columns, null, 4));
|
||||
query = query.where(builder => {
|
||||
Object.values(request.query.columns)
|
||||
.filter(column => column.searchable === "true")
|
||||
.forEach((col, index) =>
|
||||
index === 0
|
||||
? builder.where(`${tableName}.${col.data}`, "like", `%${search}%`)
|
||||
: builder.orWhere(`${tableName}.${col.data}`, "like", `%${search}%`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const recordsFilteredResult = await query.clone().count("* as count").first();
|
||||
const recordsFiltered = recordsFilteredResult ? recordsFilteredResult.count : 0;
|
||||
|
||||
query = query.orderBy(`${tableName}.${order}`, direction).limit(size).offset(start);
|
||||
|
||||
return {
|
||||
query,
|
||||
recordsTotal,
|
||||
recordsFiltered
|
||||
};
|
||||
}
|
16
tailwind.config.js
Normal file
16
tailwind.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "selector",
|
||||
content: [
|
||||
"./src/client/**/*.{html,js,ejs}",
|
||||
"./node_modules/preline/dist/*.js"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms')({ strategy: "class" }),
|
||||
],
|
||||
|
||||
}
|
||||
|
120
tsconfig.client.json
Normal file
120
tsconfig.client.json
Normal file
@ -0,0 +1,120 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ES6", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@node_modules/*": ["node_modules/*"]
|
||||
},
|
||||
"plugins": [],
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"./node_modules/@types",
|
||||
"./src/client/public/types/globals.d.ts",
|
||||
"./src/client/public/types/preline.d.ts"
|
||||
],
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./src/client/public/js", /* Specify an output folder for all emitted files. */
|
||||
"removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src/client/**/*"]
|
||||
}
|
||||
|
5
tsconfig.json
Normal file
5
tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
// The purpose of this file is for vscode to correctly identify which tsconfig to use.
|
||||
// This file serves no other purpose.
|
||||
{
|
||||
"extends": "./tsconfig.server.json"
|
||||
}
|
124
tsconfig.server.json
Normal file
124
tsconfig.server.json
Normal file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
"@db/*": ["src/db/*"],
|
||||
"@server/*": ["src/server/*"],
|
||||
"@client/*": ["src/client/*"],
|
||||
"@bot/*": ["src/bot/*"],
|
||||
"@utils/*": ["src/utils/*"],
|
||||
"@node_modules/*": ["node_modules/*"],
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"transform": "@zerollup/ts-transform-paths"
|
||||
}
|
||||
],
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
"typeRoots": ["./node_modules/@types", "./src/types/"], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
12
web/app.py
12
web/app.py
@ -1,12 +0,0 @@
|
||||
import logging
|
||||
|
||||
from quart import Quart
|
||||
from .routes.dashboard import dashboard
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def create_app():
|
||||
log.info("Creating web app and registering blueprints")
|
||||
app = Quart(__name__)
|
||||
app.register_blueprint(dashboard, url_prefix="/dashboard")
|
||||
return app
|
@ -1,7 +0,0 @@
|
||||
from quart import Blueprint
|
||||
|
||||
dashboard = Blueprint("dashboard", __name__)
|
||||
|
||||
@dashboard.route("/")
|
||||
async def home():
|
||||
return "Dashboard page"
|
Loading…
x
Reference in New Issue
Block a user