3
0
Fork 0
mirror of https://github.com/ZeppelinBot/Zeppelin.git synced 2025-07-05 10:17:20 +00:00

feat: vite dashboard tweaks/fixes

This commit is contained in:
Dragory 2025-06-01 19:37:10 +00:00
parent aaac328138
commit 177f13d1fc
No known key found for this signature in database
16 changed files with 94 additions and 128 deletions

View file

@ -14,9 +14,10 @@
<h1>Zeppelin</h1> <h1>Zeppelin</h1>
The Zeppelin website requires JavaScript to load. The Zeppelin website requires JavaScript to load.
</noscript> </noscript>
<div id="app"></div>
<script type="text/javascript" src="/env.js"></script> <script type="text/javascript" src="/env.js"></script>
<div id="app"></div> <script type="module" src="./src/index.ts"></script>
<script type="module" src="./src/main.ts"></script>
</body> </body>
</html> </html>

View file

@ -6,7 +6,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc -b && vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {

View file

@ -2,20 +2,26 @@ import Fastify from "fastify";
import fastifyStatic from "@fastify/static"; import fastifyStatic from "@fastify/static";
import path from "node:path"; import path from "node:path";
const fastify = Fastify({ logger: true }); const fastify = Fastify({
// We already get logs from nginx, so disable here
logger: false,
});
fastify.get("/env.js", (req, reply) => { fastify.addHook("preHandler", (req, reply, done) => {
reply.header("Content-Type", "application/javascript; charset=utf8"); if (req.url === "/env.js") {
reply.send(`window.API_URL = ${JSON.stringify(process.env.API_URL)}`); reply.header("Content-Type", "application/javascript; charset=utf8");
reply.send(`window.API_URL = ${JSON.stringify(process.env.API_URL)};`);
}
done();
}); });
fastify.register(fastifyStatic, { fastify.register(fastifyStatic, {
root: path.join(__dirname, "dist"), root: path.join(import.meta.dirname, "dist"),
wildcard: false, wildcard: false,
}); });
fastify.get("*", (req, reply) => { fastify.get("*", (req, reply) => {
reply.header("Content-Type", "text/html; charset=utf8").send(indexContent); reply.sendFile("index.html");
}); });
fastify.listen({ port: 3002, host: '0.0.0.0' }, (err, address) => { fastify.listen({ port: 3002, host: '0.0.0.0' }, (err, address) => {

View file

@ -1,5 +1,4 @@
import { RootStore } from "./store"; import { RootStore } from "./store";
const apiUrl = window.API_URL;
type QueryParamObject = { [key: string]: string | null }; type QueryParamObject = { [key: string]: string | null };
@ -28,7 +27,7 @@ function buildQueryString(params: QueryParamObject) {
} }
export function request(resource, fetchOpts: RequestInit = {}) { export function request(resource, fetchOpts: RequestInit = {}) {
return fetch(`${apiUrl}/${resource}`, fetchOpts).then(async (res) => { return fetch(`${window.API_URL}/${resource}`, fetchOpts).then(async (res) => {
if (!res.ok) { if (!res.ok) {
if (res.status === 401) { if (res.status === 401) {
RootStore.dispatch("auth/expiredLogin"); RootStore.dispatch("auth/expiredLogin");
@ -74,7 +73,7 @@ type FormPostOpts = {
export function formPost(resource: string, body: Record<any, any> = {}, opts: FormPostOpts = {}) { export function formPost(resource: string, body: Record<any, any> = {}, opts: FormPostOpts = {}) {
body["X-Api-Key"] = RootStore.state.auth.apiKey; body["X-Api-Key"] = RootStore.state.auth.apiKey;
const form = document.createElement("form"); const form = document.createElement("form");
form.action = `${apiUrl}/${resource}`; form.action = `${window.API_URL}/${resource}`;
form.method = "POST"; form.method = "POST";
form.enctype = "multipart/form-data"; form.enctype = "multipart/form-data";
if (opts.target != null) { if (opts.target != null) {

View file

@ -11,7 +11,7 @@ const isAuthenticated = async () => {
export const authGuard: NavigationGuard = async (to, from, next) => { export const authGuard: NavigationGuard = async (to, from, next) => {
if (await isAuthenticated()) return next(); if (await isAuthenticated()) return next();
window.location.href = `${process.env.API_URL}/auth/login`; window.location.href = `${window.API_URL}/auth/login`;
}; };
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => { export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
@ -26,6 +26,6 @@ export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
export const authRedirectGuard: NavigationGuard = async (to, form, next) => { export const authRedirectGuard: NavigationGuard = async (to, form, next) => {
if (await isAuthenticated()) return next("/dashboard"); if (await isAuthenticated()) return next("/dashboard");
window.location.href = `${process.env.API_URL}/auth/login`; window.location.href = `${window.API_URL}/auth/login`;
return next(); return next();
}; };

View file

@ -0,0 +1,53 @@
<template>
<div class="splash">
<div class="error" v-if="error">
<div class="message">{{ error }}</div>
</div>
<div class="wrapper">
<div class="logo-column">
<img class="logo" src="/img/logo.png" alt="Zeppelin Logo" />
</div>
<div class="info-column">
<h1>Zeppelin</h1>
<div class="description">
Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
</div>
<div class="actions">
<a class="btn" href="/dashboard">Dashboard</a>
<a class="btn" href="/docs">Documentation</a>
</div>
<ul class="links">
<li>
<a href="https://discord.gg/zeppelin">Official Discord Server</a>
</li>
<li>
<a href="https://github.com/Dragory/ZeppelinBot">GitHub</a>
</li>
<li>
<a href="/privacy-policy">Privacy Policy</a>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
const errorMessages = {
noAccess: "No dashboard access. If you think this is a mistake, please contact your server owner.",
expiredLogin: "Dashboard login expired. Please log in again.",
};
const route = useRoute();
const error = ref<string | null>(null);
watch(
() => route.query.error,
(value) => {
error.value = errorMessages[String(value)] || null;
},
);
</script>

View file

@ -2,8 +2,8 @@ import "./style/app.css";
import { createApp } from "vue"; import { createApp } from "vue";
import VueHighlightJS from "vue3-highlightjs";
import "highlight.js/styles/base16/ocean.css"; import "highlight.js/styles/base16/ocean.css";
import VueHighlightJS from "vue3-highlightjs";
import { router } from "./routes"; import { router } from "./routes";
import { RootStore } from "./store"; import { RootStore } from "./store";
@ -13,24 +13,14 @@ import "./directives/trim-indents";
import App from "./components/App.vue"; import App from "./components/App.vue";
import { trimIndents } from "./directives/trim-indents"; import { trimIndents } from "./directives/trim-indents";
if (!window.API_URL) {
throw new Error("Missing API_URL");
}
const app = createApp(App); const app = createApp(App);
app.use(router); app.use(router);
app.use(RootStore); app.use(RootStore);
// Set up a read-only global variable to access specific env vars
app.mixin({
data() {
return {
get env() {
return Object.freeze({
API_URL: process.env.API_URL,
});
},
};
},
});
app.use(VueHighlightJS); app.use(VueHighlightJS);
app.directive("trim-indents", trimIndents); app.directive("trim-indents", trimIndents);

View file

@ -1,33 +0,0 @@
import "./style/initial.css";
import splashHtml from "./splash.html";
if (window.location.pathname !== "/") {
import("./init-vue");
} else {
// @ts-ignore
document.querySelector("#app").innerHTML = splashHtml;
const queryParams: any = window.location.search
.slice(1)
.split("&")
.reduce((map, str) => {
const pair = str.split("=");
map[pair[0]] = pair[1];
return map;
}, {});
if (queryParams.error) {
const errorElement = document.querySelector("#error") as HTMLElement;
errorElement.classList.add("has-error");
const errorMessages = {
noAccess: "No dashboard access. If you think this is a mistake, please contact your server owner.",
expiredLogin: "Dashboard login expired. Please log in again.",
};
const errorMessageElem = document.createElement("div");
errorMessageElem.classList.add("message");
errorMessageElem.innerText = errorMessages[queryParams.error] || "Unexpected error";
errorElement.appendChild(errorMessageElem);
}
}

View file

@ -1,9 +1,12 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import { authGuard, authRedirectGuard, loginCallbackGuard } from "./auth"; import { authGuard, authRedirectGuard, loginCallbackGuard } from "./auth";
import Splash from "./components/Splash.vue";
export const router = createRouter({ export const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [ routes: [
{ path: "/", component: Splash },
{ path: "/login", components: {}, beforeEnter: authRedirectGuard }, { path: "/login", components: {}, beforeEnter: authRedirectGuard },
{ path: "/login-callback", component: {}, beforeEnter: loginCallbackGuard }, { path: "/login-callback", component: {}, beforeEnter: loginCallbackGuard },

View file

@ -1,32 +0,0 @@
<div class="splash">
<div id="error"></div>
<div class="wrapper">
<div class="logo-column">
<img class="logo" src="/img/logo.png" alt="Zeppelin Logo" />
</div>
<div class="info-column">
<h1>Zeppelin</h1>
<div class="description">
Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
</div>
<div class="actions">
<a class="btn" href="/dashboard">Dashboard</a>
<a class="btn" href="/docs">Documentation</a>
</div>
<ul class="links">
<li>
<a href="https://github.com/Dragory/ZeppelinBot">GitHub</a>
</li>
<li>
<a href="https://discord.com/invite/w8njuNu">Discord</a>
</li>
<li>
<a href="https://www.patreon.com/zeppelinbot">Patreon</a>
</li>
<li>
<a href="/privacy-policy">Privacy Policy</a>
</li>
</ul>
</div>
</div>
</div>

View file

@ -1,9 +1,11 @@
@import "tailwindcss"; @import "./reset.css";
@import "./base.css";
@import "./splash.css";
@import "tailwindcss";
@import "vue-material-design-icons/styles.css"; @import "vue-material-design-icons/styles.css";
@import "./content.css"; @import "./content.css";
@import "./docs.css"; @import "./docs.css";
/* Reset some icon default styles for more predictable alignment */ /* Reset some icon default styles for more predictable alignment */

View file

@ -66,15 +66,14 @@
} }
} }
@screen lg { @media (width >= theme(--breakpoint-lg)) {
.main-content { .main-content {
& h1 { & h1 {
@apply text-5xl; @apply text-5xl;
} }
} }
} }
@media (width >= theme(--breakpoint-xl)) {
@screen xl {
.main-content { .main-content {
& a:not([class]), & a:not([class]),
& a[class=""] { & a[class=""] {

View file

@ -1,3 +0,0 @@
@import "./reset.css";
@import "./base.css";
@import "./splash.css";

View file

@ -15,17 +15,14 @@
color: #fff; color: #fff;
} }
& > #error { & > .error {
display: flex;
width: 100%; width: 100%;
max-width: 750px; max-width: 750px;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
margin-top: 16px; margin-top: 16px;
&.has-error {
display: flex;
}
& .message { & .message {
flex: 0 1 auto; flex: 0 1 auto;
text-align: left; text-align: left;
@ -154,7 +151,3 @@
} }
} }
} }
@media screen and (min-width: 1024px) {
}

View file

@ -4,3 +4,7 @@ declare module '*.html' {
const value: string; const value: string;
export default value; export default value;
} }
interface Window {
API_URL: string;
}

View file

@ -1,22 +1,7 @@
import { defineConfig, Plugin } from "vite"; import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import tailwind from "@tailwindcss/vite"; import tailwind from "@tailwindcss/vite";
function htmlImport(): Plugin {
return {
name: "html-import",
transform(code, id) {
if (id.endsWith(".html")) {
return {
code: `export default ${JSON.stringify(code)};`,
map: null,
};
}
return null;
},
};
}
export default defineConfig((configEnv) => { export default defineConfig((configEnv) => {
return { return {
server: { server: {
@ -34,7 +19,6 @@ export default defineConfig((configEnv) => {
}, },
}), }),
tailwind(), tailwind(),
htmlImport(),
], ],
}; };
}); });