db integration and scripts

This commit is contained in:
Corban-Lee Jones 2025-02-02 00:56:55 +00:00
parent 4577bb72b9
commit 31ce135b3a
33 changed files with 415 additions and 112 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
node_modules/
package-lock.json
config.json
dist/
dist/
*.sqlite

View File

@ -5,7 +5,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"tailwind": "npx tailwindcss -i ./src/client/public/css/main.css -o ./src/client/public/css/tailwind.css",
"build": "./build.sh",
"build": "./scripts/build.sh",
"dev": "nodemon -r tsconfig-paths/register ./src/app.ts",
"start": "node dist/app.js"
},
@ -28,10 +28,12 @@
"express": "^4.21.2",
"express-session": "^1.18.1",
"jquery": "^3.7.1",
"knex": "^3.1.0",
"ncp": "^2.0.0",
"passport": "^0.7.0",
"passport-discord": "^0.1.4",
"preline": "^2.7.0",
"sqlite3": "^5.1.7",
"tsconfig-paths": "^4.2.0"
},
"devDependencies": {

View File

@ -1,5 +1,7 @@
cd "$(dirname "$0")/../"
echo "Erasing previous dist folder"
rm -r ./dist
rm -rf ./dist
echo "Compiling tailwind css ..."
npx tailwindcss -i ./src/client/public/css/main.css -o ./src/client/public/css/tailwind.css
@ -7,6 +9,9 @@ npx tailwindcss -i ./src/client/public/css/main.css -o ./src/client/public/css/t
echo "Compiling typescript ..."
npx tsc --project ./tsconfig.json
echo "Copying client files"
cp -r src/client dist/client
echo "Building typescript path aliases ..."
npx tsc-alias -p ./tsconfig.json

2
scripts/migrate.sh Executable file
View File

@ -0,0 +1,2 @@
cd "$(dirname "$0")/../"
npx knex migrate:latest --knexfile ./src/knexfile.ts

2
scripts/seeds.sh Executable file
View File

@ -0,0 +1,2 @@
cd "$(dirname "$0")/../"
npx knex seed:run --knexfile ./src/knexfile.ts

View File

@ -1,3 +1,4 @@
import path from "path";
import express from "express";
import engine from "ejs-mate";
import passport from "passport";
@ -7,28 +8,29 @@ import dotenv from "dotenv";
dotenv.config();
import "@bot/bot";
import { setupPassport } from "@server/controllers/auth";
import { setupPassport } from "@server/controllers/auth.web.controller";
import { attachUser } from "@server/middleware/attachUser";
import { flashMiddleware } from "@server/middleware/flash";
import { ensureAuthenticated } from "@server/middleware/authenticated";
// import routers & middleware
import { attachGuilds } from "@server/middleware/attachGuilds";
import { router as homeRouter } from "@server/routes/home";
import { router as guildRouter } from "@server/routes/guild";
import { router as authRouter } from "@server/routes/auth";
import homeWebRouter from "@server/routes/home.web.routes";
import guildApiRouter from "@server/routes/guild.api.routes";
import guildWebRouter from "@server/routes/guild.web.routes";
import authWebRouter from "@server/routes/auth.web.routes";
const app = express();
app.engine("ejs", engine);
app.set("views", "./src/client/views");
app.set("views", path.resolve(__dirname, "client/views"));
app.set("view engine", "ejs");
// Public files, including 3rd party resources (foreign)
app.use("/static", express.static("./src/client/public"));
app.use("/static/foreign/preline.js", express.static("./node_modules/preline/dist/preline.js"));
app.use("/static/foreign/jquery.js", express.static("./node_modules/jquery/dist/jquery.min.js"));
app.use("/static/foreign/dataTables.js", express.static("./node_modules/datatables.net-dt/js/dataTables.dataTables.min.js"));
app.use("/static", express.static(path.resolve(__dirname, "client/public")));
app.use("/static/foreign/preline.js", express.static(path.resolve(__dirname, "../node_modules/preline/dist/preline.js")));
app.use("/static/foreign/jquery.js", express.static(path.resolve(__dirname, "../node_modules/jquery/dist/jquery.min.js")));
app.use("/static/foreign/dataTables.js", express.static(path.resolve(__dirname, "../node_modules/datatables.net/js/dataTables.min.js")));
// User authentication & sessions
app.use(session({
@ -45,9 +47,9 @@ app.use(flash());
app.use(flashMiddleware);
// register routers & middleware
app.use("/auth", authRouter);
app.use("/guild", ensureAuthenticated, attachUser, attachGuilds, guildRouter);
app.use("/", ensureAuthenticated, attachUser, attachGuilds, homeRouter);
app.use("/auth", authWebRouter);
app.use("/guild", ensureAuthenticated, attachUser, attachGuilds, guildWebRouter, guildApiRouter);
app.use("/", ensureAuthenticated, attachUser, attachGuilds, homeWebRouter);
const PORT = process.env.PORT || 3000;

View File

@ -14,7 +14,7 @@ client.on("ready", () => {
throw Error("Client is null");
}
client.user.setActivity("Set Activity", { type: ActivityType.Watching });
client.user.setActivity("new sources", { type: ActivityType.Watching });
console.log(`Discord Bot '${client.user.displayName}' is online!`)
});

View File

@ -918,6 +918,10 @@ select {
margin-inline-start: auto !important;
}
.-me-0\.5 {
margin-inline-end: -0.125rem;
}
.-mt-px {
margin-top: -1px;
}
@ -1151,10 +1155,6 @@ select {
width: 18rem;
}
.w-\[100rem\] {
width: 100rem;
}
.w-\[260px\] {
width: 260px;
}
@ -1543,6 +1543,11 @@ select {
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.bg-yellow-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 249 195 / var(--tw-bg-opacity, 1));
}
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
@ -1872,6 +1877,11 @@ select {
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.text-yellow-800 {
--tw-text-opacity: 1;
color: rgb(133 77 14 / var(--tw-text-opacity, 1));
}
.opacity-0 {
opacity: 0;
}
@ -2562,6 +2572,26 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
display: block;
}
.dt-ordering-asc .hs-datatable-ordering-asc\:text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
}
.dt-ordering-asc.hs-datatable-ordering-asc\:text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
}
.dt-ordering-desc .hs-datatable-ordering-desc\:text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
}
.dt-ordering-desc.hs-datatable-ordering-desc\:text-blue-600 {
--tw-text-opacity: 1;
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
}
.complete .hs-file-upload-complete\:bg-green-600 {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
@ -2830,6 +2860,10 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
background-color: rgb(20 184 166 / 0.1);
}
.dark\:bg-yellow-500\/10:where(.dark, .dark *) {
background-color: rgb(234 179 8 / 0.1);
}
.dark\:bg-opacity-80:where(.dark, .dark *) {
--tw-bg-opacity: 0.8;
}
@ -2900,6 +2934,11 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
color: rgb(255 255 255 / 0.6);
}
.dark\:text-yellow-500:where(.dark, .dark *) {
--tw-text-opacity: 1;
color: rgb(234 179 8 / var(--tw-text-opacity, 1));
}
.dark\:placeholder-neutral-500:where(.dark, .dark *)::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(115 115 115 / var(--tw-placeholder-opacity, 1));
@ -2994,6 +3033,26 @@ hs-accordion-toggle w-full text-start flex items-center gap-x-3.5 py-2 px-2.5 te
--tw-ring-offset-color: #1f2937;
}
.dt-ordering-asc .dark\:hs-datatable-ordering-asc\:text-blue-500:where(.dark, .dark *) {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
}
.dt-ordering-asc.dark\:hs-datatable-ordering-asc\:text-blue-500:where(.dark, .dark *) {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
}
.dt-ordering-desc .dark\:hs-datatable-ordering-desc\:text-blue-500:where(.dark, .dark *) {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
}
.dt-ordering-desc.dark\:hs-datatable-ordering-desc\:text-blue-500:where(.dark, .dark *) {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
}
.\[\&\:\:-webkit-scrollbar-thumb\]\:rounded-full::-webkit-scrollbar-thumb {
border-radius: 9999px;
}

View File

@ -0,0 +1,18 @@
// Filter by status
(function () {
const radioButtons = document.querySelectorAll('input[type="radio"][name="filter"]');
const { dataTable } = new HSDataTable('#table');
dataTable.search.fixed('status', function (searchStr, data, index) {
const status = data[2].trim().toLowerCase(); // Adjust index based on your dataset
if (radioButtons[0].checked && status === 'active') return true;
if (radioButtons[1].checked && status === 'inactive') return true;
return false;
});
radioButtons.forEach(radio => {
radio.addEventListener('change', () => dataTable.draw());
});
})();

View File

@ -1,5 +1,5 @@
<nav class="bg-white dark:bg-neutral-900 border-b dark:border-none">
<div class="max-w-[100rem] w-full mx-auto sm:flex sm:flex-row sm:justify-between sm:items-center sm:gap-x-3 py-3 sm:py-5 px-4 sm:px-6 lg:px-8">
<div class="w-full mx-auto sm:flex sm:flex-row sm:justify-between sm:items-center sm:gap-x-3 py-3 sm:py-5 px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center gap-x-3">
<div class="grow flex items-center gap-x-4">
<% if (guild.icon) { %>

View File

@ -3,14 +3,19 @@
<%- include("guildHeader") -%>
<!-- Table Section -->
<div class="max-w-full w-[100rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto overflow-hidden">
<div id="table" class="max-w-full overflow-hidden p-4 sm:p-6"> <!-- px-4 py-10 sm:px-6 lg:px-8 lg:py-14 -->
<!-- Card -->
<div class="flex flex-col">
<div class="-m-1.5 "> <!-- overflow-x-auto -->
<div class="flex flex-col" data-hs-datatable='{
"selecting": true,
"rowSelectingOptions": {
"selectAllSelector": "#selectAllBox"
}
}'>
<div class="-m-1.5">
<div class="max-w-full p-1.5 min-w-full inline-block align-middle">
<div class="bg-white border border-gray-200 rounded-md shadow-sm overflow-hidden dark:bg-neutral-900 dark:border-neutral-700">
<!-- Header -->
<div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center border-b border-gray-200 dark:border-neutral-700">
<div class="px-6 py-4 gap-3 flex flex-nowrap justify-between items-center border-gray-200 dark:border-neutral-700">
<!-- Input -->
<div class="hidden sm:block sm:col-span-1">
<label for="hs-as-table-product-review-search" class="sr-only">Search</label>
@ -78,66 +83,94 @@
<table class="min-w-full divide-y divide-gray-200 dark:divide-neutral-700">
<thead class="bg-gray-50 dark:bg-neutral-800">
<tr>
<th scope="col" class="ps-6 py-3 text-start">
<th scope="col" class="ps-6 py-3 text-start --exclude-from-ordering">
<label for="hs-at-with-checkboxes-main" class="flex ml-[1px]">
<input type="checkbox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-at-with-checkboxes-main">
<input type="checkbox" id="selectAllBox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800">
<span class="sr-only">Checkbox</span>
</label>
</th>
<th scope="col" class="px-6 py-3 text-start">
<div class="flex items-center gap-x-2">
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
Name
</span>
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
</svg>
</div>
</th>
<th scope="col" class="px-6 py-3 text-start">
<div class="flex items-center gap-x-2">
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
URL
</span>
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
</svg>
</div>
</th>
<th scope="col" class="px-6 py-3 text-start">
<div class="flex items-center gap-x-2">
<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">
Channels
</span>
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
</svg>
</div>
</th>
<th scope="col" class="px-6 py-3 text-start">
<div class="flex items-center gap-x-2">
<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">
Filters
</span>
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
</svg>
</div>
</th>
<th scope="col" class="px-6 py-3 text-start">
<div class="flex items-center gap-x-2">
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
Style
</span>
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
</svg>
</div>
</th>
<th scope="col" class="px-6 py-3 text-start">
<div class="flex items-center gap-x-2">
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200 text-nowrap">
Created at
</span>
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
</svg>
</div>
</th>
<th scope="col" class="px-6 py-3 text-start">
<div class="flex items-center gap-x-2">
<div class="flex justify-between items-center gap-x-2 cursor-pointer">
<span class="text-xs font-semibold uppercase tracking-wide text-gray-800 dark:text-neutral-200">
Status
</span>
<svg class="size-3.5 ms-1 -me-0.5 text-gray-400 dark:text-neutral-500" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path class="hs-datatable-ordering-desc:text-blue-600 dark:hs-datatable-ordering-desc:text-blue-500" d="m7 15 5 5 5-5"></path>
<path class="hs-datatable-ordering-asc:text-blue-600 dark:hs-datatable-ordering-asc:text-blue-500" d="m7 9 5-5 5 5"></path>
</svg>
</div>
</th>
</tr>
@ -151,7 +184,7 @@
<td class="size-px whitespace-nowrap">
<div class="ps-6 py-3">
<label for="hs-at-with-checkboxes-1" class="flex">
<input type="checkbox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-at-with-checkboxes-1">
<input type="checkbox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-at-with-checkboxes-1" data-hs-datatable-row-selecting-individual="">
<span class="sr-only">Checkbox</span>
</label>
</div>
@ -206,8 +239,8 @@
<td class="size-px whitespace-nowrap">
<div class="ps-6 py-3">
<label for="hs-at-with-checkboxes-1" class="flex">
<input type="checkbox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-at-with-checkboxes-1">
<label for="hs-at-with-checkboxes-2" class="flex">
<input type="checkbox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-at-with-checkboxes-2" data-hs-datatable-row-selecting-individual="">
<span class="sr-only">Checkbox</span>
</label>
</div>
@ -215,13 +248,13 @@
<td class="size-px whitespace-nowrap align-top">
<a href="#" class="block p-6 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500">
BBC News · Top Stories
Sky News
</a>
</td>
<td class="size-px whitespace-nowrap align-top">
<a href="#" class="block p-6 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500">
https://bbci.co.uk/feeds/news/rss.xml
https://sky.co.uk/feeds/news/rss.xml
</a>
</td>
@ -262,8 +295,8 @@
<td class="size-px whitespace-nowrap">
<div class="ps-6 py-3">
<label for="hs-at-with-checkboxes-1" class="flex">
<input type="checkbox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-at-with-checkboxes-1">
<label for="hs-at-with-checkboxes-3" class="flex">
<input type="checkbox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-at-with-checkboxes-3" data-hs-datatable-row-selecting-individual="">
<span class="sr-only">Checkbox</span>
</label>
</div>
@ -271,13 +304,13 @@
<td class="size-px whitespace-nowrap align-top">
<a href="#" class="block p-6 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500">
BBC News · Top Stories
Fox News
</a>
</td>
<td class="size-px whitespace-nowrap align-top">
<a href="#" class="block p-6 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500">
https://bbci.co.uk/feeds/news/rss.xml
https://fox.com/feeds/news/rss.xml
</a>
</td>
@ -313,6 +346,62 @@
</td>
</tr>
<tr class="bg-white hover:bg-gray-50 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<td class="size-px whitespace-nowrap">
<div class="ps-6 py-3">
<label for="hs-at-with-checkboxes-1" class="flex">
<input type="checkbox" class="shrink-0 border-gray-300 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-600 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-at-with-checkboxes-1" data-hs-datatable-row-selecting-individual="">
<span class="sr-only">Checkbox</span>
</label>
</div>
</td>
<td class="size-px whitespace-nowrap align-top">
<a href="#" class="block p-6 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500">
News Agency 40
</a>
</td>
<td class="size-px whitespace-nowrap align-top">
<a href="#" class="block p-6 text-blue-500 hover:text-blue-600 focus:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500">
https://news.co.uk/feeds/news/rss.xml
</a>
</td>
<td>
</td>
<td>
</td>
<td>
</td>
<td class="size-px whitespace-nowrap align-top">
<div class="p-6">
<span class="text-sm text-gray-500 dark:text-neutral-500">
30th, Jan 2025
</span>
</div>
</td>
<td class="size-px whitespace-nowrap align-top">
<div class="p-6">
<span class="py-1 px-1.5 inline-flex items-center gap-x-1 text-xs font-medium bg-yellow-100 text-yellow-800 rounded-full dark:bg-yellow-500/10 dark:text-yellow-500">
<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="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"></path>
</svg>
Warning
</span>
</div>
</td>
</tr>
<!-- <tr class="bg-white hover:bg-gray-50 dark:bg-neutral-900 dark:hover:bg-neutral-800">
<td class="size-px whitespace-nowrap align-top">
@ -424,4 +513,6 @@
<!-- End Card -->
</div>
<!-- End Table Section -->
<!-- End Table Section -->
<% block("scripts").append('<script src="/static/js/guild/subscriptions.js"></script>'); %>

View File

@ -16,9 +16,10 @@
</div>
<!-- End Content -->
<script src="/static/foreign/preline.js"></script>
<script src="/static/foreign/jquery.js"></script>
<script src="/static/foreign/dataTables.js"></script>
<script src="/static/foreign/preline.js"></script>
<script src="/static/js/main.js"></script>
<%- block("scripts").toString() %>
</body>
</html>

4
src/db/db.ts Normal file
View File

@ -0,0 +1,4 @@
import knex from "knex";
import knexConfig from "@server/../knexfile";
export const db = knex(knexConfig);

View File

@ -0,0 +1,19 @@
import type { Knex } from "knex";
const TABLE = "subscriptions";
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TABLE, table => {
table.increments("id").primary();
table.string("name").notNullable();
table.string("url").notNullable();
table.string("guild_id").notNullable();
table.boolean("active").notNullable().defaultTo(true);
table.timestamps(true, true);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TABLE);
}

View File

@ -0,0 +1,11 @@
import { Url } from "url";
export interface Subscription {
id: number;
name: string;
url: Url;
guild_id: string;
active: boolean;
created_at: Date;
updated_at: Date;
}

17
src/db/seeds/test_sub.ts Normal file
View File

@ -0,0 +1,17 @@
import { Knex } from "knex";
const TABLE = "subscriptions";
export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries
await knex(TABLE).del();
// Inserts seed entries
await knex(TABLE).insert([
{ name: "My First Subscription", url: "https://bbci.co.uk/feeds/news.xml", guild_id: "899773845223927878", active: true },
{ name: "My Second Sub", url: "https://bbci.co.uk/feeds/news.xml", guild_id: "899773845223927878", active: true },
{ name: "My Third Sub", url: "https://bbci.co.uk/feeds/news.xml", guild_id: "1204426362794811453", active: true },
{ name: "My Fourth Sub", url: "https://bbci.co.uk/feeds/news.xml", guild_id: "899773845223927878", active: true },
{ name: "My Fith Sub", url: "https://bbci.co.uk/feeds/news.xml", guild_id: "1204426362794811453", active: true },
]);
};

47
src/knexfile.ts Normal file
View File

@ -0,0 +1,47 @@
import path from "path";
import dotenv from "dotenv";
dotenv.config(); // I env vars are already loaded, but this file is invoked directly sometimes.
const {
DB_CLIENT = "sqlite", // Default:
SQLITE_FILE = path.resolve(__dirname, "../db.sqlite"), // use sqlite if not specified
PG_HOST = "",
PG_PORT = "",
PG_USER = "",
PG_PASSWORD = "",
PG_DATABASE = ""
} = process.env;
const dbConfig = {
sqlite: {
client: "sqlite3",
connection: {
filename: SQLITE_FILE
},
useNullAsDefault: true
},
postgresql: {
client: "pg",
connection: {
host: PG_HOST,
port: PG_PORT,
user: PG_USER,
password: PG_PASSWORD,
database: PG_DATABASE
}
}
}
const knexConfig = {
...dbConfig[DB_CLIENT as keyof typeof dbConfig],
migrations: {
tableName: "knex_migrations",
directory: path.resolve(__dirname, "./db/migrations")
},
seeds: {
directory: path.resolve(__dirname, "./db/seeds")
}
}
export default knexConfig;

View File

@ -50,3 +50,5 @@ export const setupPassport = (passport: PassportStatic) => {
done(null, user);
});
}
export default { get, authenticate, logout };

View File

@ -14,4 +14,6 @@ export const get = async (request: Request, response: Response) => {
title: `${guild.name} - Relay`,
guild: guild,
});
};
};
export default { get };

View File

@ -14,4 +14,6 @@ export const get = async (request: Request, response: Response) => {
title: `${guild.name} - Relay`,
guild: guild,
});
};
};
export default { get };

View File

@ -14,4 +14,6 @@ export const get = async (request: Request, response: Response) => {
title: `${guild.name} - Relay`,
guild: guild,
});
};
};
export default { get };

View File

@ -14,4 +14,6 @@ export const get = async (request: Request, response: Response) => {
title: `${guild.name} - Relay`,
guild: guild,
});
};
};
export default { get };

View File

@ -0,0 +1,29 @@
import { Request, Response } from "express";
import { Subscription } from "@db/models/subs.model";
import { db } from "@db/db";
export const get = async (request: Request, response: Response) => {
try {
const subscriptions = await db<Subscription>("subscriptions")
.select("*")
.where({ guild_id: request.params.guildId });
response.json(subscriptions);
}
catch (error) {
console.error(error);
response.status(500).json({ error: "Failed to fetch subscriptions" });
}
}
export const post = async (request: Request, response: Response) => {
try {
//
}
catch (error) {
console.error(error);
response.status(500).json({ error: "Failed to create subscription" });
}
}
export default { get, post }

View File

@ -14,4 +14,6 @@ export const get = async (request: Request, response: Response) => {
title: `${guild.name} - Relay`,
guild: guild,
});
};
}
export default { get }

View File

@ -5,3 +5,5 @@ export const get = async (_request: Request, response: Response) => {
title: "Dashboard - Relay"
});
};
export default { get };

View File

@ -1,34 +0,0 @@
const knex = require("knex");
const {
DB_CLIENT,
SQLITE_FILE,
PG_HOST,
PG_PORT,
PG_USER,
PG_PASSWORD,
PG_DATABASE
} = process.env;
const dbConfig = {
sqlite: {
client: "sqlite3",
connection: {
filename: SQLITE_FILE
},
useNullAsDefault: true
},
postgresql: {
client: "pg",
connection: {
host: PG_HOST,
port: PG_PORT,
user: PG_USER,
password: PG_PASSWORD,
database: PG_DATABASE
}
}
}
const db = knex(dbConfig[DB_CLIENT]);
module.exports = db;

View File

@ -1,9 +1,11 @@
import { Router } from "express";
import { ensureAuthenticated, forwardAuthenticated } from "@server/middleware/authenticated";
import * as controller from "@server/controllers/auth";
import controller from "@server/controllers/auth.web.controller";
export const router = Router();
const router = Router();
router.get("/login", forwardAuthenticated, controller.get);
router.get("/logout", ensureAuthenticated, controller.logout);
router.get("/api", forwardAuthenticated, controller.authenticate);
router.get("/api", forwardAuthenticated, controller.authenticate);
export default router;

View File

@ -0,0 +1,9 @@
import { Router } from "express";
import subApiController from "@server/controllers/guild/sub.api.controller";
const router = Router();
router.get("/:guildId/subscriptions/api", subApiController.get);
router.post("/:guildId/subscriptions/api", subApiController.post);
export default router;

View File

@ -1,15 +0,0 @@
import { Router } from "express";
import * as overviewController from "@server/controllers/guild/overview";
import * as subscriptionController from "@server/controllers/guild/subscriptions";
import * as filterController from "@server/controllers/guild/filters";
import * as styleController from "@server/controllers/guild/styles";
import * as contentController from "@server/controllers/guild/content";
import { getGuildPage } from "@server/middleware/guildPage";
export const router = Router();
router.get("/:guildId", getGuildPage, overviewController.get);
router.get("/:guildId/subscriptions", getGuildPage, subscriptionController.get);
router.get("/:guildId/filters", getGuildPage, filterController.get);
router.get("/:guildId/styles", getGuildPage, styleController.get);
router.get("/:guildId/content", getGuildPage, contentController.get);

View File

@ -0,0 +1,17 @@
import { Router } from "express";
import { getGuildPage } from "@server/middleware/guildPage";
import indexWebController from "@server/controllers/guild/index.web.controller";
import subWebController from "@server/controllers/guild/sub.web.controller";
import filterWebController from "@server/controllers/guild/filter.web.controller";
import styleWebController from "@server/controllers/guild/style.web.controller";
import contentWebController from "@server/controllers/guild/content.web.controller";
const router = Router();
router.get("/:guildId", getGuildPage, indexWebController.get);
router.get("/:guildId/subscriptions", getGuildPage, subWebController.get);
router.get("/:guildId/filters", getGuildPage, filterWebController.get);
router.get("/:guildId/style", getGuildPage, styleWebController.get);
router.get("/:guildId/content", getGuildPage, contentWebController.get);
export default router;

View File

@ -1,6 +0,0 @@
import { Router } from "express";
import * as controller from "@server/controllers/home";
export const router = Router();
router.get("/", controller.get);

View File

@ -0,0 +1,8 @@
import { Router } from "express";
import controller from "@server/controllers/home.web.controller";
const router = Router();
router.get("/", controller.get);
export default router;

View File

@ -30,13 +30,11 @@
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@/*": ["src/*"],
"@db/*": ["src/db/*"],
"@server/*": ["src/server/*"],
"@client/*": ["src/client/*"],
"@bot/*": ["src/bot/*"],
"@utils/*": ["src/utils/*"],
"@views/*": ["src/client/views/*"],
"@public/*": ["src/client/public/*"],
"@node_modules/*": ["node_modules/*"],
},
"plugins": [