Compare commits

...

7 Commits

20 changed files with 226 additions and 677 deletions

6
.gitignore vendored
View File

@ -10,4 +10,8 @@ package-lock.json
# exclude this very large css file, it can be
# built when needed with `npm run build:tailwind`
src/client/public/css/tailwind.css
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

View File

@ -2,4 +2,18 @@
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.1](https://gitea.cor.bz/corbz/relay/compare/v0.1.0...v0.1.1) (2025-04-22)
### Features
* connect routers for accessing the new guild tabs ([d02597b](https://gitea.cor.bz/corbz/relay/commit/d02597b4fd9a5b040b0c499b726b1812a4144ac7))
### Bug Fixes
* add tab helper for highlighting the active guild tab ([119339c](https://gitea.cor.bz/corbz/relay/commit/119339c0a3c7a3d0d63db2be91f07d06798a7133))
## 0.1.0 (2025-04-22)
Initial version

View File

@ -1,3 +1,6 @@
// This file is for building client-side typescript found
// in './src/client/typescript' to './src/client/public/bundles'
import { build } from "esbuild";
import glob from "fast-glob";
@ -6,7 +9,7 @@ const entryPoints = await glob("./src/client/typescript/**/*");
build({
entryPoints,
// outBase: "",
outdir: "./src/client/public/js",
outdir: "./src/client/public/bundles",
bundle: true,
target: ["es6"],
format: "iife",

View File

@ -1,6 +1,6 @@
{
"name": "relay",
"version": "0.1.0",
"version": "0.1.1",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@ -37,8 +37,11 @@
"typescript": "^5.8.3"
},
"dependencies": {
"@preline/datatable": "^3.0.0",
"@preline/dropdown": "^3.0.1",
"@prisma/client": "^6.6.0",
"@tailwindcss/forms": "^0.5.10",
"datatables.net-dt": "^2.2.2",
"discord.js": "^14.18.0",
"dotenv": "^16.5.0",
"ejs": "^3.1.10",

View File

@ -7,7 +7,9 @@ import engine from "ejs-mate";
import "@bot/bot";
import homeRouter from "@server/routers/home.router";
import guildRouter from "@server/routers/guild.router";
import { attachGuilds } from "@server/middleware/attachGuilds";
import { guildTabHelper } from "@server/middleware/guildTabHelper";
const app = express();
@ -18,6 +20,7 @@ app.use(express.urlencoded({ extended: true }));
app.use("/static", express.static(path.resolve(__dirname, "client/public")));
app.use("/guild", attachGuilds, guildTabHelper, guildRouter);
app.use("/", attachGuilds, homeRouter);
const HOST = process.env.HOST || "localhost";

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,3 @@
import { HSOverlay } from 'preline';
import $ from 'jquery';
$(document).ready(() => {
console.log("ready!");
HSOverlay;
});

View File

@ -0,0 +1,5 @@
<% layout("layout/base") -%>
<%- include("header") -%>
Content page placeholder

View File

@ -0,0 +1,5 @@
<% layout("layout/base") -%>
<%- include("header") -%>
Feeds page placeholder

View File

@ -0,0 +1,5 @@
<% layout("layout/base") -%>
<%- include("header") -%>
Filters page placeholder

View File

@ -0,0 +1,65 @@
<div>
<div class="relative pt-5 before:w-full before:h-[200px] before:-z-10 before:top-0 before:start-0 before:absolute">
<div class="flex sm:items-center gap-5 p-4 sm:p-6 pb-1!">
<div class="shrink-0">
<div class="relative">
<% if (guild.icon) { %>
<img src="<%= guild.iconURL() %>" class="rounded-md sm:rounded-lg size-16 sm:size-20" alt="">
<% } else { %>
<div class="size-16 sm:size-20 rounded-md sm:rounded-lg flex shrink-0 justify-center items-center bg-white dark:bg-neutral-800">
<span class="text-2xl"><%= guild.nameAcronym %></span>
</div>
<% } %>
</div>
</div>
<div class="grow">
<h1 class="text-2xl md:text-3xl font-semibold text-gray-800 dark:text-neutral-200">
<%= guild.name %>
</h1>
<ul class="flex flex-wrap items-center gap-3 mt-2">
<li class="relative">
<span class="text-sm">ID:</span>
<span class="text-sm inline-flex items-center gap-2 text-gray-800 dark:text-neutral-200"><%= guild.id %></span>
</li>
<li class="relative">
<span class="text-sm">Members:</span>
<span class="text-sm inline-flex items-center gap-2 text-gray-800 dark:text-neutral-200"><%= guild.memberCount %></span>
</li>
<li class="relative">
<span class="text-sm">Channels:</span>
<span class="text-sm inline-flex items-center gap-2 text-gray-800 dark:text-neutral-200"><%= guild.channels.channelCountWithoutThreads %></span>
</li>
</ul>
</div>
<div class="ms-auto flex flex-row flex-wrap gap-2">
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
<svg class="shrink-0 size-4" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
Validate channels
</button>
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
<svg class="shrink-0 size-4" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
Copy data
</button>
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-red-500 shadow-xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:hover:bg-neutral-800 dark:focus:bg-neutral-800">
<svg class="shrink-0 size-4" viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
Remove bot
</button>
</div>
</div>
</div>
<div class="flex flex-col justify-center items-center mx-auto p-4 sm:p-6">
<div class="flex flex-row w-full pb-1 whitespace-nowrap overflow-x-auto overflow-y-hidden">
<!-- <a href="/guild/<%= guild.id %>" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 <%= !isNaN(+guildPage) ? 'bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Overview</a> -->
<a href="/guild/<%= guild.id %>/feeds" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 <%= guildPage === 'feeds' ? 'text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Feeds</a>
<a href="/guild/<%= guild.id %>/filters" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 <%= guildPage === 'filters' ? 'text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Filters</a>
<a href="/guild/<%= guild.id %>/styles" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 <%= guildPage === 'styles' ? 'text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Styles</a>
<a href="/guild/<%= guild.id %>/content" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500 <%= guildPage === 'content' ? 'text-gray-800 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400 bg-white dark:bg-neutral-800 dark:border-neutral-700! border shadow-xs' : 'mx-[1px]' %>">Content</a>
<!-- <a href="#" class="inline-flex items-center gap-2 whitespace-nowrap rounded-lg px-3 py-2 text-sm text-blue-500 dark:text-blue-400 dark:hover:text-blue-500 dark:focus:text-blue-500">Settings</a> -->
</div>
</div>
</div>

View File

@ -0,0 +1,5 @@
<% layout("layout/base") -%>
<%- include("header") -%>
Styles page placeholder

View File

@ -13,7 +13,7 @@
<%- body -%>
</div>
<script src="/static/js/main.js"></script>
<script src="/static/bundles/main.js"></script>
<%- block("scripts").toString() %>
</body>
</html>

View File

@ -0,0 +1,19 @@
import { Request, Response } from "express";
import { client as bot } from "@bot/bot";
export const get = async (request: Request, response: Response) => {
const guildId = request.params.guildId;
const guild = bot.guilds.cache.get(guildId);
if (!guild) {
response.status(404).send("404: guild not found");
return;
}
response.render("guild/content", {
title: `${guild.name} - Relay`,
guild: guild
});
};
export default { get }

View File

@ -0,0 +1,19 @@
import { Request, Response } from "express";
import { client as bot } from "@bot/bot";
export const get = async (request: Request, response: Response) => {
const guildId = request.params.guildId;
const guild = bot.guilds.cache.get(guildId);
if (!guild) {
response.status(404).send("404: guild not found");
return;
}
response.render("guild/feeds", {
title: `${guild.name} - Relay`,
guild: guild
});
};
export default { get }

View File

@ -0,0 +1,19 @@
import { Request, Response } from "express";
import { client as bot } from "@bot/bot";
export const get = async (request: Request, response: Response) => {
const guildId = request.params.guildId;
const guild = bot.guilds.cache.get(guildId);
if (!guild) {
response.status(404).send("404: guild not found");
return;
}
response.render("guild/filters", {
title: `${guild.name} - Relay`,
guild: guild
});
};
export default { get }

View File

@ -0,0 +1,19 @@
import { Request, Response } from "express";
import { client as bot } from "@bot/bot";
export const get = async (request: Request, response: Response) => {
const guildId = request.params.guildId;
const guild = bot.guilds.cache.get(guildId);
if (!guild) {
response.status(404).send("404: guild not found");
return;
}
response.render("guild/styles", {
title: `${guild.name} - Relay`,
guild: guild
});
};
export default { get }

View File

@ -1,6 +1,9 @@
import { Request, Response, NextFunction } from "express";
import { client as bot } from "@bot/bot";
// The purpose of this middleware is to attach an object containing cached
// Discord servers to the response, which is accessible in the DOM.
// This is primarily used for rendering servers on the sidebar.
export const attachGuilds = (_request: Request, response: Response, next: NextFunction) => {
response.locals.guilds = bot.guilds.cache.map(guild => guild);
next();

View File

@ -0,0 +1,10 @@
import { Request, Response, NextFunction } from "express";
// This middleware returns the last segment of the url path.
// It's used to identify which tab to highlight as active on
// the guild view at '/guild/<id>/<page>'
export const guildTabHelper = (request: Request, response: Response, next: NextFunction) => {
const currentPath = request.path.split("/")
response.locals.guildPage = currentPath[currentPath.length - 1]
next();
}

View File

@ -0,0 +1,21 @@
import { Request, Response, Router } from "express";
import feedController from "@server/controllers/guild/feed.controller";
import filterController from "@server/controllers/guild/filter.controller";
import styleController from "@server/controllers/guild/style.controller";
import contentController from "@server/controllers/guild/content.controller";
const router = Router();
router.get("/:guildId", (request: Request, response: Response) => {
const guildId = request.params.guildId;
response.redirect(`/guild/${guildId}/feeds`);
return;
});
router.get("/:guildId/feeds", feedController.get);
router.get("/:guildId/filters", filterController.get);
router.get("/:guildId/styles", styleController.get);
router.get("/:guildId/content", contentController.get);
export default router;