Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
23ca09b4a2 | |||
b4aac14fb3 | |||
77c8d39142 | |||
ce51623637 | |||
a856925ab4 | |||
d8141e485c | |||
e0cb99974f | |||
9896f8e094 | |||
acde6e1bbb | |||
054cb6c017 | |||
95c55f3ba7 | |||
8aedc84280 | |||
5e8fc3c8aa | |||
ad268096f3 | |||
1e5c8a821e | |||
39e67c1088 | |||
db5178fef0 | |||
5323009fd8 | |||
637415b8ca | |||
31b9063365 | |||
3acd08d922 | |||
6ff4bacddf | |||
0a5d32e6e8 | |||
16134e1719 | |||
60cb0083f5 | |||
3774c0b6db | |||
9b5913cd77 | |||
1edc1d4016 | |||
336484c13a | |||
12dff02c6f | |||
f8724162ad |
11
.gitignore
vendored
11
.gitignore
vendored
@ -4,14 +4,9 @@ generated/prisma
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
.env
|
.env
|
||||||
|
|
||||||
#prisma local database
|
# Prisma local database
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
*.db-journal
|
||||||
|
|
||||||
# exclude this very large css file, it can be
|
# Exclude generated public files, which can be large with bundling
|
||||||
# built when needed with `npm run build:tailwind`
|
src/client/public/generated/
|
||||||
src/client/public/css/tailwind.css
|
|
||||||
|
|
||||||
# Contains bundled js files built from typescript
|
|
||||||
# they can be very large, build with `npm run build:client`
|
|
||||||
src/client/public/bundles
|
|
22
CHANGELOG.md
22
CHANGELOG.md
@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [0.1.4](https://gitea.cor.bz/corbz/relay/compare/v0.1.3...v0.1.4) (2025-04-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add '15' as a page size option for tables ([9b5913c](https://gitea.cor.bz/corbz/relay/commit/9b5913cd77e8ebe90bae3f3a6957a4927dc0fc15))
|
||||||
|
* add filters guild page with table (missing full features) ([6ff4bac](https://gitea.cor.bz/corbz/relay/commit/6ff4bacddf9200aa7d25284efee1c193ed3e76ae))
|
||||||
|
* allow a custom 'where' clause parameter for datatable queries. ([637415b](https://gitea.cor.bz/corbz/relay/commit/637415b8ca6843fa46dbb4e80d849c3a4324f0b6))
|
||||||
|
* basic search functionality for feed table ([f872416](https://gitea.cor.bz/corbz/relay/commit/f8724162ad167e53bf38cb5d1957d8905c59ab17))
|
||||||
|
* functional 'create' modal for feed records ([d8141e4](https://gitea.cor.bz/corbz/relay/commit/d8141e485cf60cede956b5a3846c1cd00962ff38))
|
||||||
|
* tag design for status and channels in feed table ([1edc1d4](https://gitea.cor.bz/corbz/relay/commit/1edc1d4016b0640c6f7da320e498b4cc0875a0f8))
|
||||||
|
* util for verifying feed channels ([5323009](https://gitea.cor.bz/corbz/relay/commit/5323009fd8562ab8f3997aeb829863fd5b3655ec))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api:** correctly handle arguments for channels and active ([9896f8e](https://gitea.cor.bz/corbz/relay/commit/9896f8e094c06a0980b835902df2d9cb384bbd3d))
|
||||||
|
* **api:** feed push fails if channels is undefined (left blank in UI) ([ce51623](https://gitea.cor.bz/corbz/relay/commit/ce51623637b21829ca3727a564b278b2282902fd))
|
||||||
|
* include filters on feed table ([db5178f](https://gitea.cor.bz/corbz/relay/commit/db5178fef086bba9a53ce6e8f1abcd66f211e322))
|
||||||
|
* redraw feed datatable when new entries are created ([77c8d39](https://gitea.cor.bz/corbz/relay/commit/77c8d391424671af6834517aab07e47be87b5d63))
|
||||||
|
* replace unusable <td> renders with className parameters for col configs. ([31b9063](https://gitea.cor.bz/corbz/relay/commit/31b90633652c493b7c87ef359736650574b0fb53))
|
||||||
|
|
||||||
### [0.1.3](https://gitea.cor.bz/corbz/relay/compare/v0.1.2...v0.1.3) (2025-04-26)
|
### [0.1.3](https://gitea.cor.bz/corbz/relay/compare/v0.1.2...v0.1.3) (2025-04-26)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
moving from knex to prisma
|
|
||||||
|
|
||||||
read more:
|
|
||||||
https://github.com/prisma/prisma-examples/blob/latest/orm/express/src/index.ts
|
|
||||||
https://www.prisma.io/docs/getting-started
|
|
@ -1,14 +1,14 @@
|
|||||||
// This file is for building client-side typescript found
|
// This file is for building client-side typescript found
|
||||||
// in './src/client/typescript' to './src/client/public/bundles'
|
// in './src/client/typescript' to './src/client/public/generated/js'
|
||||||
|
|
||||||
import { build } from "esbuild";
|
import { build } from "esbuild";
|
||||||
import glob from "fast-glob";
|
import glob from "fast-glob";
|
||||||
|
|
||||||
const entryPoints = await glob("./src/client/typescript/**/*");
|
const entryPoints = await glob("./src/client/src/ts/**/*");
|
||||||
|
|
||||||
build({
|
build({
|
||||||
entryPoints,
|
entryPoints,
|
||||||
outdir: "./src/client/public/bundles",
|
outdir: "./src/client/public/generated/js",
|
||||||
bundle: true,
|
bundle: true,
|
||||||
target: ["es6"],
|
target: ["es6"],
|
||||||
format: "iife",
|
format: "iife",
|
||||||
|
18
package.json
18
package.json
@ -1,18 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "relay",
|
"name": "relay",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./dist/app.js",
|
"start": "node ./dist/app.js",
|
||||||
"dryrun": "npx ts-node -r tsconfig-paths/register ./src/app.ts",
|
"dryrun": "npx ts-node -r tsconfig-paths/register ./src/app.ts",
|
||||||
"dev": "npm run build:tailwind && npm run build:client && npm run dryrun",
|
"dev": "npm run build:client && npm run dryrun",
|
||||||
"build": "sh ./scripts/build.sh",
|
"build": "sh ./scripts/build.sh",
|
||||||
"build:client": "node esbuild.mjs",
|
"build:client": "npm run build:css && node esbuild.mjs",
|
||||||
"build:server": "npx tsc -p ./tsconfig.json && npx tsc-alias -p ./tsconfig.json",
|
"build:server": "npx tsc -p ./tsconfig.json && npx tsc-alias -p ./tsconfig.json",
|
||||||
"build:tailwind": "npx @tailwindcss/cli -i ./src/client/public/css/main.css -o ./src/client/public/css/tailwind.css",
|
"build:css": "npx postcss \"./src/client/src/css/**/*.css\" --dir ./src/client/public/generated/css",
|
||||||
"db:migrate": "npx prisma migrate dev --name",
|
"db:migrate": "npx prisma migrate dev --name",
|
||||||
|
"db:push": "npx prisma db push",
|
||||||
"db:seed": "npx prisma db seed",
|
"db:seed": "npx prisma db seed",
|
||||||
"db:gen": "npx prisma generate",
|
"db:gen": "npx prisma generate",
|
||||||
|
"db:format": "npx prisma format",
|
||||||
|
"db:studio": "npx prisma studio",
|
||||||
|
"db:reset": "npx prisma migrate reset",
|
||||||
"release:patch": "npx standard-version --release-as patch",
|
"release:patch": "npx standard-version --release-as patch",
|
||||||
"release:minor": "npx standard-version --release-as minor",
|
"release:minor": "npx standard-version --release-as minor",
|
||||||
"release:major": "npx standard-version --release-as major"
|
"release:major": "npx standard-version --release-as major"
|
||||||
@ -22,14 +26,19 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/cli": "^4.1.4",
|
"@tailwindcss/cli": "^4.1.4",
|
||||||
|
"@tailwindcss/postcss": "^4.1.4",
|
||||||
"@types/ejs": "^3.1.5",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/express": "^5.0.1",
|
"@types/express": "^5.0.1",
|
||||||
"@types/jquery": "^3.5.32",
|
"@types/jquery": "^3.5.32",
|
||||||
"@types/node": "^22.14.1",
|
"@types/node": "^22.14.1",
|
||||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
"esbuild": "^0.25.2",
|
"esbuild": "^0.25.2",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"postcss-cli": "^11.0.1",
|
||||||
|
"postcss-import": "^16.1.0",
|
||||||
"prisma": "^6.6.0",
|
"prisma": "^6.6.0",
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
@ -41,6 +50,7 @@
|
|||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@preline/datatable": "^3.0.0",
|
"@preline/datatable": "^3.0.0",
|
||||||
"@preline/dropdown": "^3.0.1",
|
"@preline/dropdown": "^3.0.1",
|
||||||
|
"@preline/overlay": "^3.0.0",
|
||||||
"@preline/select": "^3.0.0",
|
"@preline/select": "^3.0.0",
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
8
postcss.config.js
Normal file
8
postcss.config.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require("postcss-import"),
|
||||||
|
require("@tailwindcss/postcss"),
|
||||||
|
require("autoprefixer")
|
||||||
|
]
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "TestModel" (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"name" TEXT
|
|
||||||
);
|
|
@ -1,10 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the `TestModel` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- DropTable
|
|
||||||
PRAGMA foreign_keys=off;
|
|
||||||
DROP TABLE "TestModel";
|
|
||||||
PRAGMA foreign_keys=on;
|
|
@ -1,18 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "Feed" (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"url" TEXT NOT NULL,
|
|
||||||
"guild_id" TEXT NOT NULL,
|
|
||||||
"active" BOOLEAN NOT NULL,
|
|
||||||
"created_at" DATETIME NOT NULL,
|
|
||||||
"updated_at" DATETIME NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "Channel" (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"channel_id" TEXT NOT NULL,
|
|
||||||
"feedId" INTEGER NOT NULL,
|
|
||||||
CONSTRAINT "Channel_feedId_fkey" FOREIGN KEY ("feedId") REFERENCES "Feed" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
|
||||||
);
|
|
@ -1,17 +0,0 @@
|
|||||||
-- RedefineTables
|
|
||||||
PRAGMA defer_foreign_keys=ON;
|
|
||||||
PRAGMA foreign_keys=OFF;
|
|
||||||
CREATE TABLE "new_Feed" (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"url" TEXT NOT NULL,
|
|
||||||
"guild_id" TEXT NOT NULL,
|
|
||||||
"active" BOOLEAN NOT NULL,
|
|
||||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" DATETIME NOT NULL
|
|
||||||
);
|
|
||||||
INSERT INTO "new_Feed" ("active", "created_at", "guild_id", "id", "name", "updated_at", "url") SELECT "active", "created_at", "guild_id", "id", "name", "updated_at", "url" FROM "Feed";
|
|
||||||
DROP TABLE "Feed";
|
|
||||||
ALTER TABLE "new_Feed" RENAME TO "Feed";
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
||||||
PRAGMA defer_foreign_keys=OFF;
|
|
45
prisma/migrations/20250428225814_init/migration.sql
Normal file
45
prisma/migrations/20250428225814_init/migration.sql
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Feed" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"guild_id" TEXT NOT NULL,
|
||||||
|
"active" BOOLEAN NOT NULL,
|
||||||
|
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Channel" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"channel_id" TEXT NOT NULL,
|
||||||
|
"feedId" INTEGER NOT NULL,
|
||||||
|
CONSTRAINT "Channel_feedId_fkey" FOREIGN KEY ("feedId") REFERENCES "Feed" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Filter" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"guild_id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"matching_algorithm" TEXT NOT NULL,
|
||||||
|
"is_insensitive" BOOLEAN NOT NULL,
|
||||||
|
"is_whitelist" BOOLEAN NOT NULL,
|
||||||
|
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_FeedToFilter" (
|
||||||
|
"A" INTEGER NOT NULL,
|
||||||
|
"B" INTEGER NOT NULL,
|
||||||
|
CONSTRAINT "_FeedToFilter_A_fkey" FOREIGN KEY ("A") REFERENCES "Feed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "_FeedToFilter_B_fkey" FOREIGN KEY ("B") REFERENCES "Filter" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_FeedToFilter_AB_unique" ON "_FeedToFilter"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_FeedToFilter_B_index" ON "_FeedToFilter"("B");
|
5
prisma/migrations/20250430104514_indexes/migration.sql
Normal file
5
prisma/migrations/20250430104514_indexes/migration.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Feed_guild_id_created_at_idx" ON "Feed"("guild_id", "created_at" DESC);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Filter_guild_id_created_at_idx" ON "Filter"("guild_id", "created_at" DESC);
|
@ -17,9 +17,12 @@ model Feed {
|
|||||||
url String
|
url String
|
||||||
guild_id String
|
guild_id String
|
||||||
active Boolean
|
active Boolean
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
channels Channel[]
|
channels Channel[]
|
||||||
|
filters Filter[]
|
||||||
|
|
||||||
|
@@index([guild_id, created_at(sort: Desc)])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Channel {
|
model Channel {
|
||||||
@ -28,3 +31,26 @@ model Channel {
|
|||||||
Feed Feed @relation(fields: [feedId], references: [id])
|
Feed Feed @relation(fields: [feedId], references: [id])
|
||||||
feedId Int
|
feedId Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Filter {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
guild_id String
|
||||||
|
name String
|
||||||
|
value String
|
||||||
|
matching_algorithm MatchingAlgorithms
|
||||||
|
is_insensitive Boolean
|
||||||
|
is_whitelist Boolean
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
updated_at DateTime @updatedAt
|
||||||
|
feeds Feed[]
|
||||||
|
|
||||||
|
@@index([guild_id, created_at(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MatchingAlgorithms {
|
||||||
|
ANY
|
||||||
|
ALL
|
||||||
|
EXACT
|
||||||
|
REGEX
|
||||||
|
FUZZY
|
||||||
|
}
|
||||||
|
@ -2,23 +2,63 @@ import Prisma, { PrismaClient } from "../generated/prisma";
|
|||||||
|
|
||||||
const client = new PrismaClient();
|
const client = new PrismaClient();
|
||||||
|
|
||||||
async function createManyFeeds() {
|
function getRandomDate(from: Date, to: Date) {
|
||||||
const records: any = { data: [] };
|
const fromDate = from.getTime();
|
||||||
|
return new Date(fromDate + Math.random() * (to.getTime() - fromDate));
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < 35; i++) {
|
function generateRandomChannelId() {
|
||||||
records.data.push(<Prisma.Prisma.FeedCreateInput>{
|
const min = 1000000000;
|
||||||
name: `News Network ${i}`,
|
const max = 9999999999;
|
||||||
url: `https://news-network-${i}.com/rss`,
|
return Math.floor(Math.random() * (max - min + 1)) + min + '';
|
||||||
guild_id: "1204426362794811453",
|
}
|
||||||
active: true
|
|
||||||
|
async function createManyChannels(feedId: number) {
|
||||||
|
const channelData: Prisma.Prisma.ChannelCreateManyInput[] = [ ];
|
||||||
|
|
||||||
|
if (Math.random() < .5) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.floor(Math.random() * 10) + 1; i++) {
|
||||||
|
channelData.push({
|
||||||
|
channel_id: generateRandomChannelId(),
|
||||||
|
feedId: feedId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.feed.createMany(records);
|
await client.channel.createMany({ data: channelData });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAllFeeds() {
|
||||||
|
await client.feed.deleteMany()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createManyFeeds() {
|
||||||
|
const feedData: Prisma.Prisma.FeedCreateManyInput[] = [ ];
|
||||||
|
|
||||||
|
for (let i = 0; i < 35; i++) {
|
||||||
|
feedData.push({
|
||||||
|
name: `News Network ${i}`,
|
||||||
|
url: `https://news-network-${i}.com/rss`,
|
||||||
|
guild_id: "1204426362794811453",
|
||||||
|
active: Math.random() < .5,
|
||||||
|
created_at: getRandomDate(
|
||||||
|
new Date("2020-01-01T00:00:00.000Z"),
|
||||||
|
new Date("2025-04-27T00:00:00.000Z")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedIds = await client.feed.createManyAndReturn({
|
||||||
|
data: feedData,
|
||||||
|
select: { id: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
feedIds.forEach(async feed => await createManyChannels(feed.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await createManyFeeds();
|
// await deleteAllFeeds();
|
||||||
|
// await createManyFeeds();
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
@ -7,7 +7,6 @@ echo "Compiling backend..."
|
|||||||
npm run build:server
|
npm run build:server
|
||||||
|
|
||||||
echo "Compiling frontend..."
|
echo "Compiling frontend..."
|
||||||
npm run build:tailwind
|
|
||||||
npm run build:client
|
npm run build:client
|
||||||
|
|
||||||
echo "Copying client files..."
|
echo "Copying client files..."
|
||||||
|
15
src/app.ts
15
src/app.ts
@ -6,6 +6,7 @@ import express from "express";
|
|||||||
import engine from "ejs-mate";
|
import engine from "ejs-mate";
|
||||||
|
|
||||||
import "@bot/bot";
|
import "@bot/bot";
|
||||||
|
import prisma from "@server/prisma";
|
||||||
import homeRouter from "@server/routers/home.router";
|
import homeRouter from "@server/routers/home.router";
|
||||||
import guildRouter from "@server/routers/guild.router";
|
import guildRouter from "@server/routers/guild.router";
|
||||||
import { attachGuilds } from "@server/middleware/attachGuilds";
|
import { attachGuilds } from "@server/middleware/attachGuilds";
|
||||||
@ -17,8 +18,9 @@ app.engine("ejs", engine);
|
|||||||
app.set("view engine", "ejs");
|
app.set("view engine", "ejs");
|
||||||
app.set("views", path.resolve(__dirname, "client/views"));
|
app.set("views", path.resolve(__dirname, "client/views"));
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
app.use("/static", express.static(path.resolve(__dirname, "client/public")));
|
app.use("/public", express.static(path.resolve(__dirname, "client/public")));
|
||||||
|
|
||||||
app.use("/guild", attachGuilds, guildTabHelper, guildRouter);
|
app.use("/guild", attachGuilds, guildTabHelper, guildRouter);
|
||||||
app.use("/", attachGuilds, homeRouter);
|
app.use("/", attachGuilds, homeRouter);
|
||||||
@ -26,6 +28,15 @@ app.use("/", attachGuilds, homeRouter);
|
|||||||
const HOST = process.env.HOST || "localhost";
|
const HOST = process.env.HOST || "localhost";
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
const server = app.listen(PORT, () => {
|
||||||
console.log(`Server is listening on port http://${HOST}:${PORT}`);
|
console.log(`Server is listening on port http://${HOST}:${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
console.log("\nShutdown signal received...");
|
||||||
|
|
||||||
|
prisma.$disconnect();
|
||||||
|
server.close(error => {
|
||||||
|
process.exit(error ? 1 : 0);
|
||||||
|
});
|
||||||
});
|
});
|
@ -1,5 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "preline/variants.css";
|
@import "./preline";
|
||||||
|
|
||||||
@config "../../../../tailwind.config.js";
|
@config "../../../../tailwind.config.js";
|
||||||
|
|
||||||
@ -26,7 +26,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 200, 700;
|
font-weight: 200, 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url("/static/fonts/inter-variablefont.ttf");
|
src: url("/public/fonts/inter-variablefont.ttf");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Datatables */
|
/* Datatables */
|
||||||
@ -45,9 +45,32 @@
|
|||||||
@apply border-none bg-gray-50 dark:bg-neutral-800;
|
@apply border-none bg-gray-50 dark:bg-neutral-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cj-table-header {
|
||||||
|
@apply px-6 py-3 text-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-table-header-content {
|
||||||
|
@apply flex justify-between items-center gap-x-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-table-header-content > span {
|
||||||
|
@apply text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-table-header-content > svg {
|
||||||
|
@apply size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-table-header-content > svg > path:nth-child(1) {
|
||||||
|
@apply hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-table-header-content > svg > path:nth-child(2) {
|
||||||
|
@apply hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500;
|
||||||
|
}
|
||||||
|
|
||||||
.cj-table-footer {
|
.cj-table-footer {
|
||||||
@apply px-6 py-4 gap-3 flex justify-between items-center border-t
|
@apply px-6 py-4 gap-3 flex justify-between items-center;
|
||||||
border-gray-200 dark:border-neutral-700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cj-table-paging-btn {
|
.cj-table-paging-btn {
|
||||||
@ -76,9 +99,7 @@
|
|||||||
@apply text-sm text-gray-500 dark:text-neutral-500 text-nowrap;
|
@apply text-sm text-gray-500 dark:text-neutral-500 text-nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Select Box */
|
.cj-table-paging-select-toggle {
|
||||||
|
|
||||||
.cj-select-toggle {
|
|
||||||
@apply form-select hs-select-disabled:pointer-events-none
|
@apply 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
|
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
|
cursor-pointer bg-white border border-gray-200 rounded-lg text-start text-sm
|
||||||
@ -87,7 +108,7 @@
|
|||||||
dark:text-neutral-200 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800;
|
dark:text-neutral-200 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cj-select-dropdown {
|
.cj-table-paging-select-dropdown {
|
||||||
@apply mt-2 z-50 w-20 max-h-72 p-1 space-y-0.5 bg-white border border-gray-200
|
@apply 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
|
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]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100
|
||||||
@ -96,12 +117,74 @@
|
|||||||
dark:border-neutral-700;
|
dark:border-neutral-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cj-select-option {
|
.cj-table-paging-select-option {
|
||||||
@apply py-2 px-3 w-full text-sm text-gray-800 cursor-pointer hover:bg-gray-100
|
@apply 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
|
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;
|
dark:hover:bg-neutral-800 dark:text-neutral-200 dark:focus:bg-neutral-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tag Select */
|
||||||
|
|
||||||
|
.cj-tag-select-wrapper {
|
||||||
|
@apply 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-tag-select-dropdown {
|
||||||
|
@apply
|
||||||
|
z-80
|
||||||
|
min-w-fit
|
||||||
|
max-h-72
|
||||||
|
p-1.5
|
||||||
|
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]: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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-tag-select-input {
|
||||||
|
@apply px-2 rounded-xs order-1 text-sm outline-hidden dark:bg-neutral-900 dark:placeholder-neutral-500
|
||||||
|
dark:text-neutral-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-tag-select-option {
|
||||||
|
@apply flex items-center rounded-lg cursor-pointer py-2 ps-2 pe-4 w-full
|
||||||
|
text-gray-500
|
||||||
|
hover:bg-gray-100
|
||||||
|
focus:bg-gray-100
|
||||||
|
|
||||||
|
dark:text-neutral-200
|
||||||
|
dark:bg-neutral-900
|
||||||
|
dark:hover:bg-neutral-800
|
||||||
|
dark:focus:bg-neutral-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-tag-select-option [data-icon] {
|
||||||
|
@apply size-8 me-2 flex shrink-0 items-center justify-center text-gray-500 dark:text-neutral-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-tag-select-option [data-title] {
|
||||||
|
@apply text-sm font-semibold text-gray-800 dark:text-neutral-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cj-tag-select-option [data-description] {
|
||||||
|
@apply text-xs text-gray-500 dark:text-neutral-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Layout Sidebar */
|
/* Layout Sidebar */
|
||||||
|
|
||||||
.sidebar-btn {
|
.sidebar-btn {
|
||||||
@ -122,4 +205,34 @@
|
|||||||
dark:hover:bg-neutral-700
|
dark:hover:bg-neutral-700
|
||||||
dark:focus:bg-neutral-700
|
dark:focus:bg-neutral-700
|
||||||
dark:text-neutral-200;
|
dark:text-neutral-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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
|
||||||
}
|
}
|
76
src/client/src/css/preline.css
Normal file
76
src/client/src/css/preline.css
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/* Custom written preline variants file */
|
||||||
|
/* The actual file in preline/variants has '@import' statements out of order, which fail with postcss. */
|
||||||
|
|
||||||
|
/* Preline */
|
||||||
|
@import 'preline/src/plugins/dropdown/variants.css';
|
||||||
|
@import 'preline/src/plugins/remove-element/variants.css';
|
||||||
|
@import 'preline/src/plugins/tooltip/variants.css';
|
||||||
|
@import 'preline/src/plugins/accordion/variants.css';
|
||||||
|
@import 'preline/src/plugins/tree-view/variants.css';
|
||||||
|
@import 'preline/src/plugins/collapse/variants.css';
|
||||||
|
@import 'preline/src/plugins/tabs/variants.css';
|
||||||
|
@import 'preline/src/plugins/overlay/variants.css';
|
||||||
|
@import 'preline/src/plugins/scrollspy/variants.css';
|
||||||
|
@import 'preline/src/plugins/carousel/variants.css';
|
||||||
|
@import 'preline/src/plugins/select/variants.css';
|
||||||
|
@import 'preline/src/plugins/input-number/variants.css';
|
||||||
|
@import 'preline/src/plugins/pin-input/variants.css';
|
||||||
|
@import 'preline/src/plugins/strong-password/variants.css';
|
||||||
|
@import 'preline/src/plugins/stepper/variants.css';
|
||||||
|
@import 'preline/src/plugins/combobox/variants.css';
|
||||||
|
@import 'preline/src/plugins/layout-splitter/variants.css';
|
||||||
|
@import 'preline/src/plugins/scroll-nav/variants.css';
|
||||||
|
@import 'preline/src/plugins/datatable/variants.css';
|
||||||
|
@import 'preline/src/plugins/range-slider/variants.css';
|
||||||
|
@import 'preline/src/plugins/file-upload/variants.css';
|
||||||
|
@import 'preline/src/plugins/datepicker/variants.css';
|
||||||
|
@import 'preline/src/plugins/theme-switch/variants.css';
|
||||||
|
|
||||||
|
/* States */
|
||||||
|
@custom-variant hs-success {
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success & {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@custom-variant hs-error {
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error & {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Apexcharts */
|
||||||
|
@custom-variant hs-apexcharts-tooltip-dark {
|
||||||
|
&.dark {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sortable.js */
|
||||||
|
@custom-variant hs-dragged {
|
||||||
|
&.dragged {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toastify */
|
||||||
|
@custom-variant hs-toastify-on {
|
||||||
|
|
||||||
|
&.toastify.on {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastify.on & {
|
||||||
|
@slot;
|
||||||
|
}
|
||||||
|
}
|
374
src/client/src/ts/guild/feeds.ts
Normal file
374
src/client/src/ts/guild/feeds.ts
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
import $ from "jquery";
|
||||||
|
import DataTable from "datatables.net";
|
||||||
|
import HSDropdown from "@preline/dropdown";
|
||||||
|
import HSOverlay, { IOverlayOptions } from "@preline/overlay";
|
||||||
|
import HSSelect, { ISelectOptions } from "@preline/select";
|
||||||
|
import HSDataTable, { IDataTableOptions } from "@preline/datatable";
|
||||||
|
import { ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
|
||||||
|
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
|
||||||
|
import { formatTimestamp, verifyChannels } from "../../../src/ts/main";
|
||||||
|
import prisma from "../../../../../generated/prisma";
|
||||||
|
|
||||||
|
declare let guildId: string;
|
||||||
|
declare let channels: Array<any>;
|
||||||
|
|
||||||
|
// #region DataTable
|
||||||
|
//
|
||||||
|
|
||||||
|
// Fix dependency bugs with preline
|
||||||
|
(window as any).DataTable = DataTable;
|
||||||
|
(window as any).$hsDataTableCollection = [];
|
||||||
|
|
||||||
|
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">
|
||||||
|
Refine your search or create a new feed.
|
||||||
|
Alternatively, use a template to deploy a ready-made feed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
||||||
|
<button type="button" class="open-edit-modal-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="#TODO">
|
||||||
|
<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 feed
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 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-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||||
|
Use a Template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const columnDefs: ConfigColumnDefs[] = [
|
||||||
|
// Select checkbox column
|
||||||
|
{
|
||||||
|
target: 0,
|
||||||
|
orderable: false,
|
||||||
|
searchable: false,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (_data: unknown, _type: unknown, row: prisma.Feed) => { return `
|
||||||
|
<div class="ps-6 py-4">
|
||||||
|
<label class="rowSelect${row.id}-js" class="flex">
|
||||||
|
<input type="checkbox" id="rowSelect${row.id}-js" class="cj-table-checkbox" data-hs-datatable-row-selecting-individual=""/>
|
||||||
|
<span class="sr-only">Select Row</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 1,
|
||||||
|
data: "name",
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (data: string) => { return `
|
||||||
|
<span class="cj-table-link max-w-[250px] truncate">
|
||||||
|
${data}
|
||||||
|
</span>
|
||||||
|
`}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 2,
|
||||||
|
data: "url",
|
||||||
|
orderable: true,
|
||||||
|
searchable: true,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (data: string) => { return `
|
||||||
|
<a href="${data}" class="cj-table-link max-w-[450px] truncate">
|
||||||
|
${data}
|
||||||
|
</a>
|
||||||
|
`}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 3,
|
||||||
|
data: "channels",
|
||||||
|
orderable: false,
|
||||||
|
searchable: false,
|
||||||
|
className: "size-px",
|
||||||
|
render: (data: prisma.Channel[], type: string, row: prisma.Feed) => {
|
||||||
|
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");
|
||||||
|
|
||||||
|
if (!verifyChannels(data, channels)) {
|
||||||
|
wrapper.text("invalid channels").addClass("whitespace-nowrap");
|
||||||
|
return wrapper.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstChannelName = "# " + channels.find(c => c.id === data[0].channel_id).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].channel_id).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(channel => {
|
||||||
|
const channelName = "# " + channels.find(c => c.id === channel.channel_id).name;
|
||||||
|
dropdownMenu.append(tag.clone().text(channelName));
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.append(dropdownMenu);
|
||||||
|
wrapper.append(dropdown);
|
||||||
|
return wrapper.get(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 4,
|
||||||
|
data: "filters",
|
||||||
|
orderable: false,
|
||||||
|
searchable: false,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (data: prisma.Filter[]) => { return `
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<span class="cj-table-text">
|
||||||
|
${data}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 5,
|
||||||
|
data: "message_style",
|
||||||
|
orderable: false, // both should be true, but message_style doesnt exist yet
|
||||||
|
searchable: false,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (data: string) => { return `
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<span class="cj-table-text">
|
||||||
|
${data}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 6,
|
||||||
|
data: "created_at",
|
||||||
|
orderable: true,
|
||||||
|
searchable: false,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (data: string) => { return `
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<span class="cj-table-text">
|
||||||
|
${formatTimestamp(data)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 7,
|
||||||
|
data: "active",
|
||||||
|
orderable: true,
|
||||||
|
searchable: false,
|
||||||
|
className: "size-px whitespace-nowrap",
|
||||||
|
render: (data: boolean) => {
|
||||||
|
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||||
|
const badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center gap-x-1 text-xs font-medium rounded-full");
|
||||||
|
const label = $("<span>");
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const ajaxSettings: AjaxSettings = {
|
||||||
|
url: `/guild/${guildId}/feeds/api/datatable`,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
|
dataSrc: "data",
|
||||||
|
data: (data: unknown) => {
|
||||||
|
if (data === undefined) return;
|
||||||
|
// TODO,
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableOptions: IDataTableOptions = {
|
||||||
|
ajax: ajaxSettings,
|
||||||
|
serverSide: true,
|
||||||
|
processing: true,
|
||||||
|
columnDefs: columnDefs,
|
||||||
|
pagingOptions: { pageBtnClasses: "hidden" },
|
||||||
|
rowSelectingOptions: { selectAllSelector: "#selectAllBox" },
|
||||||
|
language: {
|
||||||
|
zeroRecords: emptyTableHtml,
|
||||||
|
emptyTable: emptyTableHtml,
|
||||||
|
loadingRecords: "Placeholder loading message..."
|
||||||
|
},
|
||||||
|
drawCallback: () => HSDropdown.autoInit(),
|
||||||
|
rowCallback: (row: HTMLTableRowElement) => {
|
||||||
|
$(row).addClass("bg-white dark:bg-neutral-900");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const table: HSDataTable = new HSDataTable(
|
||||||
|
$("#table").get(0) as HTMLElement,
|
||||||
|
tableOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Page Size Select
|
||||||
|
// https://preline.co/plugins/html/advanced-select.html
|
||||||
|
|
||||||
|
(window as any).$hsSelectCollection = [];
|
||||||
|
(window as any)["FloatingUIDOM"] = {
|
||||||
|
computePosition: computePosition,
|
||||||
|
autoUpdate: autoUpdate,
|
||||||
|
offset: offset
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close on click.
|
||||||
|
window.addEventListener('click', (evt) => {
|
||||||
|
const evtTarget = evt.target;
|
||||||
|
|
||||||
|
HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageSelectOptions: ISelectOptions = {
|
||||||
|
toggleTag: '<button type="button" aria-expanded="false"></button>',
|
||||||
|
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>`,
|
||||||
|
toggleClasses: "cj-table-paging-select-toggle",
|
||||||
|
optionClasses: "cj-table-paging-select-option",
|
||||||
|
dropdownClasses: `cj-table-paging-select-dropdown`,
|
||||||
|
dropdownSpace: 10,
|
||||||
|
dropdownScope: "parent",
|
||||||
|
dropdownPlacement: "top",
|
||||||
|
dropdownVerticalFixedPlacement: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageSizeSelect: HSSelect = new HSSelect(
|
||||||
|
$("#selectPageSize-js").get(0) as HTMLElement,
|
||||||
|
pageSelectOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Edit Modal
|
||||||
|
|
||||||
|
(window as any).$hsOverlayCollection = [];
|
||||||
|
|
||||||
|
const editModalOptions: IOverlayOptions = {};
|
||||||
|
|
||||||
|
const editModal: HSOverlay = new HSOverlay(
|
||||||
|
$("#editModal").get(0) as HTMLElement,
|
||||||
|
editModalOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
$(document).on("click", ".open-edit-modal-js", async () => {
|
||||||
|
await openEditModal(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const openEditModal = async (id: number) => {
|
||||||
|
editModal.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditModal = () => {
|
||||||
|
editModal.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const channelsSelectOptions: ISelectOptions = {
|
||||||
|
placeholder: "Select option....",
|
||||||
|
mode: "tags",
|
||||||
|
|
||||||
|
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>
|
||||||
|
`,
|
||||||
|
optionTemplate: `
|
||||||
|
<div class="cj-tag-select-option">
|
||||||
|
<div data-icon></div>
|
||||||
|
<div>
|
||||||
|
<div data-title></div>
|
||||||
|
<div 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>
|
||||||
|
`,
|
||||||
|
tagsInputId: "formChannelsInput",
|
||||||
|
wrapperClasses: "cj-tag-select-wrapper",
|
||||||
|
dropdownClasses: "cj-tag-select-dropdown w-full",
|
||||||
|
tagsInputClasses: "cj-tag-select-input",
|
||||||
|
|
||||||
|
dropdownScope: "window",
|
||||||
|
dropdownSpace: 10,
|
||||||
|
dropdownPlacement: "bottom",
|
||||||
|
dropdownVerticalFixedPlacement: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const channelSelect: HSSelect = new HSSelect(
|
||||||
|
$("#formChannels").get(0) as HTMLElement,
|
||||||
|
channelsSelectOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
$("#editForm").on("submit", async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const form = $(event.target).get(0) as HTMLFormElement;
|
||||||
|
$(form).addClass("submitted");
|
||||||
|
|
||||||
|
if (!form.checkValidity()) return;
|
||||||
|
|
||||||
|
await $.ajax({
|
||||||
|
url: `/guild/${guildId}/feeds/api`,
|
||||||
|
method: "post",
|
||||||
|
dataType: "json",
|
||||||
|
data: $(event.target).serializeArray(),
|
||||||
|
success: () => {
|
||||||
|
(table as any).dataTable.draw() // is this okay? dataTable is private, but there is no other method I know of to redraw...
|
||||||
|
closeEditModal();
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
alert(JSON.stringify(error, null, 4));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// #endregion
|
@ -1,11 +1,10 @@
|
|||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import DataTable from "datatables.net";
|
|
||||||
import HSDropdown from "@preline/dropdown";
|
|
||||||
import HSSelect, { ISelectOptions } from "@preline/select";
|
import HSSelect, { ISelectOptions } from "@preline/select";
|
||||||
import HSDataTable, { IDataTableOptions } from "@preline/datatable";
|
import HSDataTable, { IDataTableOptions } from "@preline/datatable";
|
||||||
|
import DataTable from "datatables.net";
|
||||||
import { ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
|
import { ConfigColumnDefs, AjaxSettings } from "datatables.net-dt";
|
||||||
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
|
import { autoUpdate, computePosition, offset } from "@floating-ui/dom";
|
||||||
import { formatTimestamp } from "../main";
|
import { formatTimestamp } from "../../../src/ts/main";
|
||||||
|
|
||||||
// #region DataTable
|
// #region DataTable
|
||||||
//
|
//
|
||||||
@ -17,19 +16,20 @@ import { formatTimestamp } from "../main";
|
|||||||
const emptyTableHtml: string = `
|
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="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">
|
<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>
|
<svg class="shrink-0 size-6 text-gray-600 dark:text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" /></svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="mt-5 font-semibold text-gray-800 dark:text-white">
|
<h2 class="mt-5 font-semibold text-gray-800 dark:text-white">
|
||||||
No results found
|
No results found
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
<p class="mt-2 text-sm text-gray-600 dark:text-neutral-400">
|
||||||
Create a feed and it will appear here.
|
Refine your search or create a new filter.
|
||||||
|
Alternatively, use a template to deploy a ready-made filter.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
<div class="mt-5 flex flex-col sm:flex-row gap-2">
|
||||||
<button type="button" class="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="#TODO">
|
<button type="button" class="open-edit-modal-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="#TODO">
|
||||||
<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>
|
<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 feed
|
Create a filter
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 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-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
<button type="button" onclick="alert('not implemented');" class="py-2 px-3 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-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
|
||||||
Use a Template
|
Use a Template
|
||||||
@ -44,7 +44,7 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
target: 0,
|
target: 0,
|
||||||
orderable: false,
|
orderable: false,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
render(_data: unknown, _type: unknown, row: any) { return `
|
render: (_data: unknown, _type: unknown, row: any) => { return `
|
||||||
<td class="size-px whitespace-nowrap">
|
<td class="size-px whitespace-nowrap">
|
||||||
<div class="ps-6 py-4">
|
<div class="ps-6 py-4">
|
||||||
<label class="rowSelect${row.id}-js" class="flex">
|
<label class="rowSelect${row.id}-js" class="flex">
|
||||||
@ -60,9 +60,9 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
data: "name",
|
data: "name",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
render(data: string) { return `
|
render: (data: string) => { return `
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
<td class="size-px whitespace-nowrap align-top">
|
||||||
<span class="cj-table-link">
|
<span class="cj-table-link max-w-[250px] truncate">
|
||||||
${data}
|
${data}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@ -70,62 +70,111 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 2,
|
target: 2,
|
||||||
data: "url",
|
data: "value",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
render(data: string) { return `
|
render: (data: string) => { return `
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
<td class="size-px whitespace-nowrap align-top">
|
||||||
<span class="cj-table-link">
|
<div class="px-6 py-4 max-w-[600px] truncate">
|
||||||
${data}
|
<span class="cj-table-text">
|
||||||
</span>
|
${data}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
`}
|
`}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 3,
|
target: 3,
|
||||||
data: "channels",
|
data: "matching_algorithm",
|
||||||
orderable: false,
|
orderable: true,
|
||||||
searchable: false,
|
searchable: true,
|
||||||
render(data: string) {
|
render: (data: string) => {
|
||||||
return `${data}`
|
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||||
|
const label = $("<span>").addClass("cj-table-text");
|
||||||
|
let description: string;
|
||||||
|
|
||||||
|
switch (data) {
|
||||||
|
case "ANY":
|
||||||
|
description = "Any Word";
|
||||||
|
break;
|
||||||
|
case "ALL":
|
||||||
|
description = "All Words";
|
||||||
|
break;
|
||||||
|
case "EXACT":
|
||||||
|
description = "Exact Match";
|
||||||
|
break;
|
||||||
|
case "REGEX":
|
||||||
|
description = "Regular Expression";
|
||||||
|
break;
|
||||||
|
case "FUZZY":
|
||||||
|
description = "Fuzzy Match";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.text(description);
|
||||||
|
wrapper.append(label);
|
||||||
|
return wrapper.get(0);
|
||||||
}
|
}
|
||||||
|
// render: (data: string) => { return `
|
||||||
|
// <td class="size-px whitespace-nowrap align-top">
|
||||||
|
// <div class="px-6 py-4">
|
||||||
|
// <span class="cj-table-text">
|
||||||
|
// ${data}
|
||||||
|
// </span>
|
||||||
|
// </div>
|
||||||
|
// </td>
|
||||||
|
// `}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 4,
|
target: 4,
|
||||||
data: "filters",
|
data: "is_insensitive",
|
||||||
orderable: false,
|
orderable: true,
|
||||||
searchable: false,
|
searchable: true,
|
||||||
render(data: string) { return `
|
render: data => {
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||||
<div class="px-6 py-4">
|
const badge = $("<span>").addClass("py-1 px-1.5 inline-flex items-center text-xs font-medium rounded-full");
|
||||||
<span class="cj-table-text">
|
const label = $("<span>");
|
||||||
${data}
|
|
||||||
</span>
|
if (data) {
|
||||||
</div>
|
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||||
</td>
|
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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 5,
|
target: 5,
|
||||||
data: "message_style",
|
data: "is_whitelist",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
render(data: string) { return `
|
render: data => {
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
const wrapper = $("<div>").addClass("px-6 py-4");
|
||||||
<div class="px-6 py-4">
|
const badge = $("<span>").addClass("py-1 px-2 inline-flex items-center text-xs font-medium rounded-full");
|
||||||
<span class="cj-table-text">
|
const label = $("<span>");
|
||||||
${data}
|
|
||||||
</span>
|
if (data) {
|
||||||
</div>
|
badge.addClass("bg-teal-100 text-teal-800 dark:bg-teal-500/10 dark:text-teal-500");
|
||||||
</td>
|
badge.append(label.text("Whitelist"));
|
||||||
`}
|
} else {
|
||||||
|
badge.addClass("bg-red-100 text-red-800 dark:bg-red-500/10 dark:text-red-500");
|
||||||
|
badge.append(label.text("Blacklist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.append(badge);
|
||||||
|
return wrapper.get(0);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 6,
|
target: 6,
|
||||||
data: "created_at",
|
data: "created_at",
|
||||||
orderable: true,
|
orderable: true,
|
||||||
searchable: false,
|
searchable: false,
|
||||||
render(data: string) { return `
|
render: (data: string) => { return `
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
<td class="size-px whitespace-nowrap align-top">
|
||||||
<div class="px-6 py-4">
|
<div class="px-6 py-4">
|
||||||
<span class="cj-table-text">
|
<span class="cj-table-text">
|
||||||
@ -134,30 +183,18 @@ const columnDefs: ConfigColumnDefs[] = [
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
`}
|
`}
|
||||||
},
|
|
||||||
{
|
|
||||||
target: 7,
|
|
||||||
data: "active",
|
|
||||||
orderable: true,
|
|
||||||
searchable: false,
|
|
||||||
render(data: string) { return `
|
|
||||||
<td class="size-px whitespace-nowrap align-top">
|
|
||||||
<div class="px-6 py-4">
|
|
||||||
<span class="cj-table-text">
|
|
||||||
${data}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
`}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const ajaxSettings: AjaxSettings = {
|
const ajaxSettings: AjaxSettings = {
|
||||||
url: `/guild/${1204426362794811453}/feeds/api/datatable`,
|
url: `/guild/${1204426362794811453}/filters/api/datatable`,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json",
|
||||||
dataSrc: "data",
|
dataSrc: "data",
|
||||||
data: (data: unknown) => {
|
data: (data: unknown) => {
|
||||||
if (data === undefined) return;
|
if (data === undefined) return;
|
||||||
// TODO
|
// TODO,
|
||||||
|
return JSON.stringify(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -173,7 +210,6 @@ const tableOptions: IDataTableOptions = {
|
|||||||
emptyTable: emptyTableHtml,
|
emptyTable: emptyTableHtml,
|
||||||
loadingRecords: "Placeholder loading message..."
|
loadingRecords: "Placeholder loading message..."
|
||||||
},
|
},
|
||||||
drawCallback: () => HSDropdown.autoInit(),
|
|
||||||
rowCallback: (row: HTMLTableRowElement) => {
|
rowCallback: (row: HTMLTableRowElement) => {
|
||||||
$(row).addClass("bg-white dark:bg-neutral-900");
|
$(row).addClass("bg-white dark:bg-neutral-900");
|
||||||
}
|
}
|
||||||
@ -186,7 +222,7 @@ const table: HSDataTable = new HSDataTable(
|
|||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Page Size Select Box
|
// #region Page Size Select
|
||||||
// https://preline.co/plugins/html/advanced-select.html
|
// https://preline.co/plugins/html/advanced-select.html
|
||||||
|
|
||||||
(window as any).$hsSelectCollection = [];
|
(window as any).$hsSelectCollection = [];
|
||||||
@ -205,9 +241,9 @@ const pageSelectOptions: ISelectOptions = {
|
|||||||
<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>
|
<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>
|
</span>
|
||||||
</div>`,
|
</div>`,
|
||||||
toggleClasses: "cj-select-toggle",
|
toggleClasses: "cj-table-paging-select-toggle",
|
||||||
optionClasses: "cj-select-option",
|
optionClasses: "cj-table-paging-select-option",
|
||||||
dropdownClasses: `cj-select-dropdown`,
|
dropdownClasses: `cj-table-paging-select-dropdown`,
|
||||||
dropdownSpace: 10,
|
dropdownSpace: 10,
|
||||||
dropdownScope: "parent",
|
dropdownScope: "parent",
|
||||||
dropdownPlacement: "top",
|
dropdownPlacement: "top",
|
@ -1,3 +1,7 @@
|
|||||||
|
import "preline";
|
||||||
|
import { Channel } from "discord.js";
|
||||||
|
import prisma from "../../../../generated/prisma";
|
||||||
|
|
||||||
// Preline: necessary for header events.
|
// Preline: necessary for header events.
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
const inputs = document.querySelectorAll('.dt-container thead input');
|
const inputs = document.querySelectorAll('.dt-container thead input');
|
||||||
@ -22,15 +26,21 @@ export const formatTimestamp = (timestamp: string | number) => {
|
|||||||
? timestamp.replace(" ", "T")
|
? timestamp.replace(" ", "T")
|
||||||
: timestamp
|
: timestamp
|
||||||
);
|
);
|
||||||
const now = new Date();
|
|
||||||
const difference = now.getTime() - date.getTime();
|
|
||||||
|
|
||||||
// Day and short month (example: 21 Oct)
|
// Day and short month (example: 21 Oct)
|
||||||
const result = `${date.getDate()} ${date.toLocaleString("en-GB", { month: "short" })}`
|
const result = `${date.getDate()} ${date.toLocaleString("en-GB", { month: "short" })}`
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
// Difference is less than a year: 'DD MMM, HH:mm'
|
// Difference is less than a year: 'DD MMM, HH:mm'
|
||||||
// Or, difference is more than a year: 'DD MMM YYYY'
|
// Or, difference is more than a year: 'DD MMM YYYY'
|
||||||
return difference < 31536000000
|
return now.getFullYear() === date.getFullYear()
|
||||||
? result + `, ${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`
|
? result + `, ${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`
|
||||||
: result + ` ${date.getFullYear()}`;
|
: result + ` ${date.getFullYear()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const verifyChannels = (data: prisma.Channel[], channels: Channel[]) => {
|
||||||
|
return data.some(item => {
|
||||||
|
return channels.map(channel => channel.id).includes(item.channel_id);
|
||||||
|
});
|
||||||
|
};
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES5",
|
"target": "ES5",
|
||||||
"outDir": "./public/js",
|
"outDir": "./public/generated/js",
|
||||||
"rootDir": "./typescript",
|
"rootDir": "./src/ts",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"noImplicitAny": true
|
"noImplicitAny": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./typescript/**/*"
|
"./src/ts/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -9,8 +9,28 @@
|
|||||||
<div class="bg-white border border-gray-200 rounded-lg shadow-xs dark:bg-neutral-900 dark:border-neutral-700">
|
<div class="bg-white border border-gray-200 rounded-lg shadow-xs dark:bg-neutral-900 dark:border-neutral-700">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div>
|
<div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center">
|
||||||
placeholder header content
|
|
||||||
|
<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" 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>
|
||||||
|
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<button type="button" class="open-edit-modal-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>
|
||||||
|
Create
|
||||||
|
<span class="hidden sm:inline">a feed</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table -->
|
<!-- Table -->
|
||||||
@ -18,86 +38,64 @@
|
|||||||
<table class="cj-table">
|
<table class="cj-table">
|
||||||
<thead class="cj-thead">
|
<thead class="cj-thead">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="ps-6 py-3 text-start --exclude-from-ordering">
|
<th scope="col" class="cj-table-header --exclude-from-ordering">
|
||||||
<label for="selectAllBox" class="">
|
<label for="selectAllBox" class="flex">
|
||||||
<input type="checkbox" id="selectAllBox" class="cj-table-checkbox">
|
<input type="checkbox" id="selectAllBox" class="cj-table-checkbox">
|
||||||
<span class="sr-only">Checkbox</span>
|
<span class="sr-only">Checkbox</span>
|
||||||
</label>
|
</label>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="name" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="name" class="cj-table-header">
|
||||||
<div class="flex justify-between item-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Name</span>
|
||||||
Name
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="url" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="url" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>URL</span>
|
||||||
URL
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="channels" class="px-6 py-3 text-start --exclude-from-ordering">
|
<th scope="col" data-dt-column="channels" class="cj-table-header --exclude-from-ordering">
|
||||||
<div class="flex justify-between items-center gap-x-2">
|
<div class="cj-table-header-content">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Channels</span>
|
||||||
Channels
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="filters" class="px-6 py-3 text-start --exclude-from-ordering">
|
<th scope="col" data-dt-column="filters" class="cj-table-header --exclude-from-ordering">
|
||||||
<div class="flex justify-between items-center gap-x-2">
|
<div class="cj-table-header-content">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Filters</span>
|
||||||
Filters
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="style" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="style" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Style</span>
|
||||||
Style
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="created_at" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="created_at" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap">
|
<span>Created at</span>
|
||||||
Created at
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" data-dt-column="active" class="px-6 py-3 text-start">
|
<th scope="col" data-dt-column="active" class="cj-table-header">
|
||||||
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
|
<div class="cj-table-header-content cursor-pointer">
|
||||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
|
<span>Status</span>
|
||||||
Status
|
<svg 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">
|
||||||
</span>
|
<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>
|
||||||
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -106,6 +104,7 @@
|
|||||||
<select id="selectPageSize-js" data-hs-datatable-page-entities="">
|
<select id="selectPageSize-js" data-hs-datatable-page-entities="">
|
||||||
<option value="5">5</option>
|
<option value="5">5</option>
|
||||||
<option value="10" selected>10</option>
|
<option value="10" selected>10</option>
|
||||||
|
<option value="15">15</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
@ -125,7 +124,7 @@
|
|||||||
<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>
|
<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
|
Prev
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center space-x-1 " data-hs-datatable-paging-pages=""></div>
|
<div class="flex items-center space-x-1" data-hs-datatable-paging-pages=""></div>
|
||||||
<button type="button" class="cj-table-paging-btn" data-hs-datatable-paging-next="">
|
<button type="button" class="cj-table-paging-btn" data-hs-datatable-paging-next="">
|
||||||
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>
|
<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>
|
||||||
@ -138,4 +137,98 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% block("scripts").append('<script src="/static/bundles/guild/feeds.js"></script>'); %>
|
<div id="editModal" 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">
|
||||||
|
<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">Feed</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="editForm" 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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<label for="formChannels" class="text-input-label">Channels</label>
|
||||||
|
<select name="channels" id="formChannels" class="--prevent-on-load-init" multiple>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</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="#editModal">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="editForm" 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>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var guildId = "<%- guild.id %>";
|
||||||
|
var channels = JSON.parse(`<%- 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 src="/public/generated/js/guild/feeds.js"></script>'); %>
|
@ -2,4 +2,185 @@
|
|||||||
|
|
||||||
<%- include("header") -%>
|
<%- include("header") -%>
|
||||||
|
|
||||||
Filters page placeholder
|
<div id="table" class="--prevent-on-load-init max-w-full px-4 sm:px-6">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="-m-1.5">
|
||||||
|
<div class="max-w-full min-w-full p-1.5 inline-block align-middle">
|
||||||
|
<div class="bg-white border border-gray-200 rounded-lg shadow-xs dark:bg-neutral-900 dark:border-neutral-700">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center">
|
||||||
|
|
||||||
|
<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" 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>
|
||||||
|
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<button type="button" class="open-edit-modal-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>
|
||||||
|
Create
|
||||||
|
<span class="hidden sm:inline">a filter</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="min-w-full overflow-x-auto">
|
||||||
|
<table class="cj-table">
|
||||||
|
<thead class="cj-thead">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="ps-6 py-3 text-start --exclude-from-ordering">
|
||||||
|
<label for="selectAllBox" class="">
|
||||||
|
<input type="checkbox" id="selectAllBox" class="cj-table-checkbox">
|
||||||
|
<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 item-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" data-dt-column="url" 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">
|
||||||
|
Value
|
||||||
|
</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" data-dt-column="url" 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" data-dt-column="url" 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">
|
||||||
|
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" data-dt-column="url" 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" data-dt-column="created_at" 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>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cj-table-footer">
|
||||||
|
<div class="max-w-sm space-y-3">
|
||||||
|
<select id="selectPageSize-js" data-hs-datatable-page-entities="">
|
||||||
|
<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>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="sm:inline-flex items-center gap-x-2" data-hs-datatable-info="">
|
||||||
|
<p class="text-sm text-gray dark:text-neutral-400">
|
||||||
|
<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="cj-table-paging-btn" 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="cj-table-paging-btn" 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editModal" 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">Filters</h2>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-neutral-400">
|
||||||
|
Manage your filters.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form id="editForm" 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">
|
||||||
|
placeholder form content
|
||||||
|
</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="#editModal">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="editForm" 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>
|
||||||
|
|
||||||
|
<% block("scripts").append('<script src="/public/generated/js/guild/filters.js"></script>'); %>
|
@ -4,16 +4,16 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><%= title %></title>
|
<title><%= title %></title>
|
||||||
<link rel="stylesheet" href="/static/css/tailwind.css">
|
<link rel="stylesheet" href="/public/generated/css/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-neutral-100 dark:bg-neutral-900 text-gray-600 dark:text-gray-400 min-h-screen font-[Inter]">
|
<body class="dark bg-neutral-100 dark:bg-neutral-900 text-gray-600 dark:text-gray-400 min-h-screen font-[Inter]">
|
||||||
<%- include("sidebar") -%>
|
<%- include("sidebar") -%>
|
||||||
|
|
||||||
<div class="w-full lg:ps-64">
|
<div class="w-full lg:ps-64">
|
||||||
<%- body -%>
|
<%- body -%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/bundles/main.js"></script>
|
<script src="/public/generated/js/main.js"></script>
|
||||||
<%- block("scripts").toString() %>
|
<%- block("scripts").toString() %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
98
src/server/controllers/guild/api/dt.module.ts
Normal file
98
src/server/controllers/guild/api/dt.module.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import prisma, { Prisma } from "@server/prisma";
|
||||||
|
import { AjaxData, AjaxResponse } from "datatables.net-dt";
|
||||||
|
|
||||||
|
type ModelDelegateFindManyArgs = {
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
orderBy?: any;
|
||||||
|
where?: any;
|
||||||
|
include?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModelDelegate = {
|
||||||
|
findMany(args: ModelDelegateFindManyArgs): Promise<any[]>;
|
||||||
|
count(args?: { where?: any }): Promise<number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DatatableQuery extends AjaxData {
|
||||||
|
filters: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const datatableRequest = async <TOrderBy, TWhere>(
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
model: ModelDelegate,
|
||||||
|
defaultOrderBy: TOrderBy,
|
||||||
|
include?: object,
|
||||||
|
where?: object
|
||||||
|
) => {
|
||||||
|
const query = request.body as unknown as DatatableQuery;
|
||||||
|
|
||||||
|
const orderBy = query.order?.length
|
||||||
|
? { [query.columns[query.order[0].column].data]: query.order[0].dir } as unknown as TOrderBy
|
||||||
|
: defaultOrderBy;
|
||||||
|
|
||||||
|
const searchWhere = query.search?.value
|
||||||
|
? {
|
||||||
|
OR: Object.values(query.columns)
|
||||||
|
.filter(col => col.searchable)
|
||||||
|
.map(col => ({
|
||||||
|
[col.data]: { contains: query.search.value }
|
||||||
|
})) as TWhere
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
where = { ...where, ...searchWhere };
|
||||||
|
|
||||||
|
const data = await model.findMany({
|
||||||
|
skip: query.start,
|
||||||
|
take: query.length,
|
||||||
|
orderBy,
|
||||||
|
where,
|
||||||
|
include,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recordsFiltered = await model.count({ where });
|
||||||
|
const recordsTotal = await model.count();
|
||||||
|
|
||||||
|
response.json({
|
||||||
|
data,
|
||||||
|
recordsFiltered,
|
||||||
|
recordsTotal,
|
||||||
|
draw: query.draw,
|
||||||
|
} as AjaxResponse);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const oldDatatable = async (request: Request, response: Response) => {
|
||||||
|
const query = request.body as unknown as DatatableQuery;
|
||||||
|
|
||||||
|
const orderBy: Prisma.FeedOrderByWithRelationInput = query.order?.length
|
||||||
|
? { [query.columns[query.order[0].column].data]: query.order[0].dir }
|
||||||
|
: { id: "asc" };
|
||||||
|
|
||||||
|
const where: Prisma.FeedWhereInput = query.search?.value
|
||||||
|
? {
|
||||||
|
OR: Object.values(query.columns)
|
||||||
|
.filter(col => col.searchable)
|
||||||
|
.map(col => ({
|
||||||
|
[col.data]: { contains: query.search.value }
|
||||||
|
})) as Prisma.FeedWhereInput[]
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const data = await prisma.feed.findMany({
|
||||||
|
skip: query.start,
|
||||||
|
take: query.length,
|
||||||
|
orderBy: orderBy,
|
||||||
|
where: where,
|
||||||
|
include: { channels: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
response.json(<AjaxResponse>{
|
||||||
|
data: data,
|
||||||
|
recordsFiltered: await prisma.feed.count({ where: where }),
|
||||||
|
recordsTotal: await prisma.feed.count(),
|
||||||
|
draw: query.draw
|
||||||
|
});
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import prisma, { Prisma } from "@server/prisma";
|
import prisma, { Prisma } from "@server/prisma";
|
||||||
|
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
||||||
|
|
||||||
export const get = async (request: Request, response: Response) => {
|
export const get = async (request: Request, response: Response) => {
|
||||||
if (!request.query.id) {
|
if (!request.query.id) {
|
||||||
@ -24,6 +25,11 @@ export const post = async (request: Request, response: Response) => {
|
|||||||
const guildId = request.params.guildId;
|
const guildId = request.params.guildId;
|
||||||
const { name, url, active, channels } = request.body;
|
const { name, url, active, channels } = request.body;
|
||||||
|
|
||||||
|
// channels comes through as either String[] or String
|
||||||
|
const formattedChannels = Array.isArray(channels)
|
||||||
|
? channels.map((channelId) => ({ channel_id: channelId }))
|
||||||
|
: [{ channel_id: channels }]
|
||||||
|
|
||||||
let feed;
|
let feed;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -32,8 +38,8 @@ export const post = async (request: Request, response: Response) => {
|
|||||||
name: name,
|
name: name,
|
||||||
url: url,
|
url: url,
|
||||||
guild_id: guildId,
|
guild_id: guildId,
|
||||||
active: active,
|
active: active === "on",
|
||||||
channels: channels
|
channels: channels !== undefined ? { create: formattedChannels } : channels
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -76,62 +82,15 @@ export const del = async (request: Request, response: Response) => {
|
|||||||
response.status(204).json(null);
|
response.status(204).json(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DataTableResponse {
|
|
||||||
data: any;
|
|
||||||
recordsFiltered: number;
|
|
||||||
recordsTotal: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DatatableQuery {
|
|
||||||
length: string;
|
|
||||||
start: string;
|
|
||||||
order: { column: string; dir: string }[];
|
|
||||||
columns: { [key: string]: { data: string; searchable: string }};
|
|
||||||
search: { value: string };
|
|
||||||
filters: { [key: string]: any };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const datatable = async (request: Request, response: Response) => {
|
export const datatable = async (request: Request, response: Response) => {
|
||||||
const query = request.query as unknown as DatatableQuery;
|
return await datatableRequest(
|
||||||
|
request,
|
||||||
const size: number = Number(query.length) || 10;
|
response,
|
||||||
const start: number = Number(query.start);
|
prisma.feed,
|
||||||
const order: string = (query.order && query.columns[query.order[0].column].data) || "id";
|
{ id: "asc" },
|
||||||
const direction: string = (query.order && query.order[0].dir) || "asc";
|
{ channels: true, filters: true },
|
||||||
const search: string = query.search?.value || "";
|
{ guild_id: request.params.guildId } // TODO: verify authenticated user can access this guild
|
||||||
|
);
|
||||||
let dbQuery: any = {};
|
|
||||||
|
|
||||||
// TODO: filter request
|
|
||||||
|
|
||||||
if (search) {
|
|
||||||
Object.values(query.columns)
|
|
||||||
.filter(column => column.searchable === "true")
|
|
||||||
.forEach((col: any) => {
|
|
||||||
dbQuery["where"][col.data] = {
|
|
||||||
contains: search,
|
|
||||||
mode: "insensitive"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderBy: any = {};
|
|
||||||
orderBy[order] = direction;
|
|
||||||
|
|
||||||
const data = await prisma.feed.findMany({
|
|
||||||
...dbQuery,
|
|
||||||
skip: start,
|
|
||||||
take: size,
|
|
||||||
orderBy: orderBy,
|
|
||||||
include: { channels: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
response.json(<DataTableResponse>{
|
|
||||||
data: data,
|
|
||||||
recordsFiltered: await prisma.feed.count({...dbQuery}),
|
|
||||||
recordsTotal: await prisma.feed.count()
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { get, post, patch, del, datatable };
|
export default { get, post, patch, del, datatable };
|
89
src/server/controllers/guild/api/filter.controller.ts
Normal file
89
src/server/controllers/guild/api/filter.controller.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import prisma, { Prisma } from "@server/prisma";
|
||||||
|
import { datatableRequest } from "@server/controllers/guild/api/dt.module";
|
||||||
|
|
||||||
|
export const get = async (request: Request, response: Response) => {
|
||||||
|
if (!request.query.id) {
|
||||||
|
response.status(400).json({ error: "missing 'id' query" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = await prisma.filter.findUnique({
|
||||||
|
where: { id: Number(request.query.id) }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filter) {
|
||||||
|
response.status(404).json({ message: "no result found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.json(filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const post = async (request: Request, response: Response) => {
|
||||||
|
const guildId = request.params.guildId;
|
||||||
|
const { name, value, matching_algorithm, is_insensitive, is_whitelist } = request.body;
|
||||||
|
|
||||||
|
let filter;
|
||||||
|
|
||||||
|
try {
|
||||||
|
filter = await prisma.filter.create({
|
||||||
|
data: {
|
||||||
|
name: name,
|
||||||
|
guild_id: guildId,
|
||||||
|
value: value,
|
||||||
|
matching_algorithm: matching_algorithm,
|
||||||
|
is_insensitive: is_insensitive,
|
||||||
|
is_whitelist: is_whitelist
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
response.status(500).json({ error: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(201).json(filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const patch = async () => {} // TODO ...
|
||||||
|
|
||||||
|
export const del = async (request: Request, response: Response) => {
|
||||||
|
const { ids } = request.body;
|
||||||
|
const guildId = request.params.guildId;
|
||||||
|
|
||||||
|
if (!ids || !Array.isArray(ids)) {
|
||||||
|
response.status(400).json({ error: "invalid request body" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.filter.deleteMany({ where: {
|
||||||
|
id: { in: ids },
|
||||||
|
guild_id: guildId
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
response.status(500).json({ error: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(204).json(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const datatable = async (request: Request, response: Response) => {
|
||||||
|
return await datatableRequest(
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
prisma.filter,
|
||||||
|
{ id: "asc" }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { get, post, patch, del, datatable };
|
@ -6,6 +6,7 @@ import styleController from "@server/controllers/guild/style.controller";
|
|||||||
import contentController from "@server/controllers/guild/content.controller";
|
import contentController from "@server/controllers/guild/content.controller";
|
||||||
|
|
||||||
import feedApiController from "@server/controllers/guild/api/feed.controller";
|
import feedApiController from "@server/controllers/guild/api/feed.controller";
|
||||||
|
import filterApiController from "@server/controllers/guild/api/filter.controller";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@ -24,10 +25,16 @@ router.get("/:guildId/content", contentController.get);
|
|||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
|
|
||||||
router.get("/:guildId/feeds/api/datatable", feedApiController.datatable);
|
router.post("/:guildId/feeds/api/datatable", feedApiController.datatable);
|
||||||
router.get("/:guildId/feeds/api", feedApiController.get);
|
router.get("/:guildId/feeds/api", feedApiController.get);
|
||||||
router.post("/:guildId/feeds/api", feedApiController.post);
|
router.post("/:guildId/feeds/api", feedApiController.post);
|
||||||
router.patch("/:guildId/feeds/api", feedApiController.patch);
|
router.patch("/:guildId/feeds/api", feedApiController.patch);
|
||||||
router.delete("/:guildId/feeds/api", feedApiController.del);
|
router.delete("/:guildId/feeds/api", feedApiController.del);
|
||||||
|
|
||||||
|
router.post("/:guildId/filters/api/datatable", filterApiController.datatable);
|
||||||
|
router.get("/:guildId/filters/api", filterApiController.get);
|
||||||
|
router.post("/:guildId/filters/api", filterApiController.post);
|
||||||
|
router.patch("/:guildId/filters/api", filterApiController.patch);
|
||||||
|
router.delete("/:guildId/filters/api", filterApiController.del);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
10
src/types/client.d.ts
vendored
Normal file
10
src/types/client.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Channel } from "discord.js";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
guildId: string;
|
||||||
|
channels: Channel[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
@ -2,7 +2,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: "selector",
|
darkMode: "selector",
|
||||||
content: [
|
content: [
|
||||||
"./src/client/**/*.{html,js,ejs,ts}",
|
"./src/client/src/**/*.{js,ts}",
|
||||||
|
"./src/client/views/**/*.{html,ejs}",
|
||||||
"./node_modules/preline/dist/*.js"
|
"./node_modules/preline/dist/*.js"
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user