dashboard: use webpack for builds; use tailwindcss instead of bulma; all sorts of tweaks

This commit is contained in:
Dragory 2019-10-10 21:58:00 +03:00
parent 028786d348
commit 577500af92
42 changed files with 4813 additions and 3174 deletions

View file

@ -1,8 +0,0 @@
{
"plugins": [
["transform-runtime", {
"regenerator": true
}],
"transform-object-rest-spread"
]
}

File diff suppressed because it is too large Load diff

View file

@ -4,33 +4,45 @@
"description": "", "description": "",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "rimraf dist && parcel build src/index.html --no-source-maps --out-dir dist", "build": "rimraf dist && cross-env NODE_ENV=production webpack --config webpack.config.js",
"build-with-report": "cross-env GENERATE_BUNDLE_SIZE_REPORT=1 npm run build", "build-debug": "rimraf dist && cross-env NODE_ENV=development webpack --config webpack.config.js",
"build-debug": "rimraf dist && cross-env NODE_ENV=development parcel build src/index.html --no-minify --out-dir dist", "watch": "cross-env NODE_ENV=development webpack-dev-server"
"watch": "parcel src/index.html"
}, },
"devDependencies": { "devDependencies": {
"@vue/component-compiler-utils": "^3.0.0", "@babel/core": "^7.6.3",
"babel-core": "^6.26.3", "@babel/preset-env": "^7.6.3",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "@babel/preset-typescript": "^7.6.0",
"babel-plugin-transform-runtime": "^6.23.0", "babel-loader": "^8.0.6",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"parcel-bundler": "^1.12.3", "css-loader": "^3.2.0",
"parcel-plugin-bundle-visualiser": "git://github.com/Dragory/parcel-plugin-bundle-visualiser.git#explicit-env-var", "cssnano": "^4.1.10",
"sass": "^1.21.0", "dotenv-webpack": "^1.7.0",
"vue-template-compiler": "^2.6.10" "file-loader": "^4.2.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^4.0.0-beta.8",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-nesting": "^7.0.1",
"postcss-preset-env": "^6.7.0",
"source-map-loader": "^0.2.4",
"tailwindcss": "^1.1.2",
"ts-loader": "^6.2.0",
"vue-loader": "^15.7.1",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.41.0",
"webpack-cli": "^3.3.9",
"webpack-dev-server": "^3.8.2",
"webpack-merge": "^4.2.2"
}, },
"dependencies": { "dependencies": {
"buefy": "^0.7.10",
"bulma": "^0.7.5",
"bulmaswatch": "^0.7.2",
"highlight.js": "^9.15.10", "highlight.js": "^9.15.10",
"js-cookie": "^2.2.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"marked": "^0.7.0", "marked": "^0.7.0",
"modern-css-reset": "^1.0.4",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance", "vue-highlightjs": "git://github.com/Dragory/vue-highlightjs.git#pass-hljs-instance",
"vue-hot-reload-api": "^2.3.3", "vue-material-design-icons": "^4.1.0",
"vue-router": "^3.0.6", "vue-router": "^3.0.6",
"vue2-ace-editor": "0.0.14", "vue2-ace-editor": "0.0.14",
"vuex": "^3.1.1" "vuex": "^3.1.1"

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();
next("/"); window.location.href = `${process.env.API_URL}/auth/login`;
}; };
export const loginCallbackGuard: NavigationGuard = async (to, from, next) => { export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
@ -19,12 +19,7 @@ export const loginCallbackGuard: NavigationGuard = async (to, from, next) => {
await RootStore.dispatch("auth/setApiKey", to.query.apiKey); await RootStore.dispatch("auth/setApiKey", to.query.apiKey);
next("/dashboard"); next("/dashboard");
} else { } else {
next({ window.location.href = `/?error=noAccess`;
path: "/",
query: {
error: "noaccess",
},
});
} }
}; };

View file

@ -1,6 +1,3 @@
<template> <template>
<router-view></router-view> <router-view></router-view>
</template> </template>
<script>
</script>

View file

@ -0,0 +1,119 @@
<template>
<div class="expandable mb-4 bg-gray-800 border border-gray-600 rounded overflow-hidden" ref="root" v-bind:class="{'shadow-xl': isOpen}">
<div role="button" class="title p-2" v-on:click="toggle">
<chevron-down class="icon" v-bind:class="{'icon-open': isOpen}" />
<span class="title-text"><slot name="title"></slot></span>
</div>
<div class="content border-t border-gray-700" ref="content">
<div class="p-4 pb-0">
<slot name="content"></slot>
</div>
</div>
</div>
</template>
<style scoped>
:root {
--animation-time: 400ms;
--target-height: auto;
}
.expandable {
transition: box-shadow var(--animation-time);
}
.title {
&:hover {
& .title-text {
@apply underline;
}
}
}
.icon {
transition: transform var(--animation-time);
transform-origin: 50% 60%;
}
.icon-open {
transform: rotate(179deg);
}
.content {
overflow: hidden;
display: none;
}
@keyframes open {
0% { height: 0; }
100% { height: var(--target-height); }
}
@keyframes close {
100% { height: 0; }
0% { height: var(--target-height); }
}
.opening {
animation: open var(--animation-time) ease-in-out;
}
.closing {
animation: close var(--animation-time) ease-in-out;
}
</style>
<script type="ts">
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue';
const ANIMATION_TIME = 400;
export default {
components: { ChevronDown },
mounted() {
this.$refs.root.style.setProperty('--animation-time', `${ANIMATION_TIME}ms`);
},
data() {
return {
isOpen: false,
animating: false,
};
},
methods: {
toggle() {
if (this.isOpen) this.close();
else this.open();
},
open() {
if (this.animating) return;
this.animating = true;
this.isOpen = true;
this.$refs.content.style.display = 'block';
const targetHeight = this.$refs.content.clientHeight;
this.$refs.content.style.setProperty('--target-height', `${targetHeight}px`);
this.$refs.content.classList.add('opening');
setTimeout(() => {
this.$refs.content.classList.remove('opening');
this.animating = false;
}, ANIMATION_TIME);
},
close() {
if (this.animating) return;
this.animating = true;
this.isOpen = false;
const targetHeight = this.$refs.content.clientHeight;
this.$refs.content.style.setProperty('--target-height', `${targetHeight}px`);
this.$refs.content.classList.add('closing');
setTimeout(() => {
this.$refs.content.classList.remove('closing');
this.$refs.content.style.display = 'none';
this.animating = false;
}, ANIMATION_TIME);
},
},
};
</script>

View file

@ -1,35 +0,0 @@
<template>
<div class="splash">
<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">
<router-link class="btn" to="/login">Dashboard</router-link>
<router-link class="btn" to="/docs">Documentation</router-link>
</div>
<div class="error" v-if="error">
<strong>Error</strong>
<div v-if="error === 'noaccess'">No access</div>
</div>
</div>
</div>
</div>
</template>
<script>
import "../style/splash.scss";
export default {
computed: {
error() {
return this.$route.query.error;
},
},
}
</script>

View file

@ -0,0 +1,11 @@
<template>
<li class="py-2 px-4 hover:text-gray-200" :class="{'pb-1 border-b border-gray-400 text-gray-200': active, 'text-gray-500': !active}">
<slot></slot>
</li>
</template>
<script lang="ts">
export default {
props: ["active"],
};
</script>

View file

@ -0,0 +1,5 @@
<template>
<ul class="list-none flex border-b border-gray-600 mb-4">
<slot></slot>
</ul>
</template>

View file

@ -20,6 +20,15 @@
</div> </div>
</template> </template>
<style scoped>
.ace_editor {
box-shadow: 0 2px 16px -4px #0000009e;
border-radius: 8px;
border: 1px solid #181818;
margin: 16px 0;
}
</style>
<script> <script>
import {mapState} from "vuex"; import {mapState} from "vuex";
import {ApiError} from "../../api"; import {ApiError} from "../../api";

View file

@ -45,7 +45,7 @@
</style> </style>
<script> <script>
import "../../style/dashboard.scss"; // import "../../style/dashboard.scss";
export default { export default {
methods: { methods: {

View file

@ -1,83 +1,83 @@
<template> <template>
<div> <div>
<h1 class="z-title is-1 mb-1">Argument Types</h1> <h1>Argument Types</h1>
<p class="mb-1"> <p>
This page details the different argument types available for commands. This page details the different argument types available for commands.
</p> </p>
<h2 id="string" class="z-title is-2 mt-2 mb-1">string</h2> <h2 id="string">string</h2>
<p class="mb-1"> <p>
Any text Any text
</p> </p>
<h2 id="number" class="z-title is-2 mt-2 mb-1">number</h2> <h2 id="number">number</h2>
<p class="mb-1"> <p>
Any number Any number
</p> </p>
<h2 id="user" class="z-title is-2 mt-2 mb-1">user</h2> <h2 id="user">user</h2>
<p class="mb-1"> <p>
Anything that uniquely identifies a user. This includes: Anything that uniquely identifies a user. This includes:
</p> </p>
<ul class="z-list z-ul mb-1"> <ul>
<li>User ID <code>108552944961454080</code></li> <li>User ID <code>108552944961454080</code></li>
<li>User mention <code>@Dark#1010</code></li> <li>User mention <code>@Dark#1010</code></li>
<li>Loose user mention <code>Dark#1010</code></li> <li>Loose user mention <code>Dark#1010</code></li>
</ul> </ul>
<h2 id="userId" class="z-title is-2 mt-2 mb-1">userId</h2> <h2 id="userId">userId</h2>
<p class="mb-1"> <p>
A valid user ID, e.g. <code>108552944961454080</code> A valid user ID, e.g. <code>108552944961454080</code>
</p> </p>
<h2 id="channel" class="z-title is-2 mt-2 mb-1">channel</h2> <h2 id="channel">channel</h2>
<p class="mb-1"> <p>
Anything that uniquely identifies a channel. This includes: Anything that uniquely identifies a channel. This includes:
</p> </p>
<ul class="z-list z-ul mb-1"> <ul>
<li>Channel ID <code>473087035574321152</code></li> <li>Channel ID <code>473087035574321152</code></li>
<li>Channel mention <code>#my-channel</code></li> <li>Channel mention <code>#my-channel</code></li>
</ul> </ul>
<h2 id="channelId" class="z-title is-2 mt-2 mb-1">channelId</h2> <h2 id="channelId">channelId</h2>
<p class="mb-1"> <p>
A valid channel ID, e.g. <code>473087035574321152</code> A valid channel ID, e.g. <code>473087035574321152</code>
</p> </p>
<h2 id="role" class="z-title is-2 mt-2 mb-1">role</h2> <h2 id="role">role</h2>
<p class="mb-1"> <p>
Anything that uniquely identifies a role. This includes: Anything that uniquely identifies a role. This includes:
</p> </p>
<ul class="z-list z-ul mb-1"> <ul>
<li>Role ID <code>473085927053590538</code></li> <li>Role ID <code class="inline-code">473085927053590538</code></li>
<li>Role mention <code>@MyRole</code></li> <li>Role mention <code>@MyRole</code></li>
</ul> </ul>
<h2 id="member" class="z-title is-2 mt-2 mb-1">member</h2> <h2 id="member">member</h2>
<p class="mb-1"> <p>
Anything that uniquely identifies a member currently on the server. This includes: Anything that uniquely identifies a member currently on the server. This includes:
</p> </p>
<ul class="z-list z-ul mb-1"> <ul>
<li>User ID <code>108552944961454080</code></li> <li>User ID <code>108552944961454080</code></li>
<li>User Mention <code>@Dark#1010</code></li> <li>User Mention <code>@Dark#1010</code></li>
<li>Loose user mention <code>Dark#1010</code></li> <li>Loose user mention <code>Dark#1010</code></li>
</ul> </ul>
<h2 id="resolvedMember" class="z-title is-2 mt-2 mb-1">resolvedMember</h2> <h2 id="resolvedMember">resolvedMember</h2>
<p class="mb-1"> <p>
See <code>member</code> above See <code>member</code> above
</p> </p>
<h2 id="delay" class="z-title is-2 mt-2 mb-1">delay</h2> <h2 id="delay">delay</h2>
<p class="mb-1"> <p>
A delay is used to specify an amount of time. It uses simple letters to specify time durations.<br> A delay is used to specify an amount of time. It uses simple letters to specify time durations.<br>
For example, <code>2d15h27m3s</code> would be 2 days, 15 hours, 27 minutes and 3 seconds. For example, <code>2d15h27m3s</code> would be 2 days, 15 hours, 27 minutes and 3 seconds.
</p> </p>
<p class="mb-1"> <p>
Note that the delay should always be written as 1 word, without spaces! Note that the delay should always be written as 1 word, without spaces!
</p> </p>
<b-collapse :open="false" class="card mb-1"> <div :open="false" class="card mb-1"> <!-- b-collapse -->
<div slot="trigger" slot-scope="props" class="card-header" role="button"> <div slot="trigger" slot-scope="props" class="card-header" role="button">
<p class="card-header-title">Additional Information</p> <p class="card-header-title">Additional Information</p>
<a class="card-header-icon"> <a class="card-header-icon">
@ -103,7 +103,7 @@
</ul> </ul>
</div> </div>
</div> </div>
</b-collapse> </div>
</div> </div>
</template> </template>

View file

@ -1,20 +1,7 @@
<template> <template>
<pre class="codeblock" v-highlightjs><code :class="lang" v-trim-code="trim"><slot></slot></code></pre> <pre class="codeblock" v-highlightjs><code :class="lang" v-trim-indents="trim"><slot></slot></code></pre>
</template> </template>
<style scoped>
.codeblock {
border-radius: 3px;
padding: 16px;
max-width: 970px; /* FIXME: temp fix for overflowing code blocks, look into properly later */
}
.hljs {
background: transparent;
padding: 0;
}
</style>
<script> <script>
export default { export default {
props: ["lang", "trim"], props: ["lang", "trim"],

View file

@ -1,15 +1,17 @@
<template> <template>
<div> <div>
<h1 class="z-title is-1 mb-1">Configuration format</h1> <h1>Configuration format</h1>
<p class="mb-1"> <p>
This is the basic format of the bot configuration for a guild. The basic breakdown is: This is the basic format of the bot configuration for a guild. The basic breakdown is:
</p> </p>
<ol class="z-list mb-1">
<ul>
<li>Prefix (i.e. what character is preceding each command)</li> <li>Prefix (i.e. what character is preceding each command)</li>
<li>Permission levels (see <router-link to="/docs/permissions">Permissions</router-link> for more info)</li> <li>Permission levels (see <router-link to="/docs/permissions">Permissions</router-link> for more info)</li>
<li>Plugin-specific configuration (see <router-link to="/docs/plugin-configuration">Plugin configuration</router-link> for more info)</li> <li>Plugin-specific configuration (see <router-link to="/docs/plugin-configuration">Plugin configuration</router-link> for more info)</li>
</ol> </ul>
<CodeBlock lang="yaml" trim="4">
<CodeBlock lang="yaml" trim="start">
prefix: "!" prefix: "!"
# role id: level # role id: level
@ -33,8 +35,8 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import CodeBlock from "./CodeBlock"; import CodeBlock from "./CodeBlock.vue";
export default { export default {
components: { CodeBlock }, components: { CodeBlock },

View file

@ -1,168 +1,134 @@
<template> <template>
<div class="docs docs-cloak"> <div class="docs">
<nav class="navbar" role="navigation" aria-label="main navigation"> <div class="container mx-auto px-4 py-2">
<div class="container"> <!-- Top bar -->
<div class="navbar-brand"> <nav class="flex items-stretch pl-4 pr-2 py-1 border border-gray-700 rounded bg-gray-800 shadow-xl">
<div class="navbar-item"> <div class="flex-initial flex items-center">
<img class="docs-logo" src="../../img/logo.png" alt="" aria-hidden="true"> <img class="flex-auto w-10 mr-5" src="../../img/logo.png" alt="" aria-hidden="true">
<h1 class="docs-title">Zeppelin Documentation</h1> <h1 class="flex-auto">Zeppelin Documentation</h1>
</div>
</div> </div>
<div class="flex-1 flex items-center justify-end">
<router-link
to="/dashboard"
role="menuitem"
class="py-1 px-2 rounded hover:bg-gray-700">
Go to dashboard
</router-link>
</div>
</nav>
<div class="navbar-menu is-active"> <!-- WIP bar -->
<div class="navbar-end"> <div class="mt-6 px-3 py-2 rounded bg-gray-800 shadow-md">
<router-link to="/dashboard" class="navbar-item">Go to dashboard</router-link> <i class="mdi mdi-alert mr-1" title="Note"></i>
</div> This documentation is a work in progress.
</div>
</div> </div>
</nav>
<div class="wip-bar"> <!-- Content wrapper -->
<i class="mdi mdi-alert"></i> <div class="flex items-start mt-8">
<strong>Note!</strong> This documentation is a work in progress. <!-- Sidebar -->
</div> <nav class="docs-sidebar flex-none px-4 pt-2 pb-3 mr-8 border border-gray-700 rounded bg-gray-800 shadow-md">
<div role="none" v-for="(group, index) in menu">
<div class="wrapper"> <h1 class="font-bold" :aria-owns="'menu-group-' + index" :class="{'mt-4': index !== 0}">{{ group.label }}</h1>
<div class="docs-sidebar"> <ul v-bind:id="'menu-group-' + index" role="group" class="list-none pl-2">
<div class="docs-sidebar-content"> <li role="none" v-for="item in group.items">
<aside class="menu"> <router-link role="menuitem" :to="item.to" class="text-gray-300 hover:text-gray-500">{{ item.label }}</router-link>
<p class="menu-label">General</p>
<ul class="menu-list">
<li><router-link to="/docs/introduction">Introduction</router-link></li>
<li><router-link to="/docs/configuration-format">Configuration format</router-link></li>
<li><router-link to="/docs/plugin-configuration">Plugin configuration</router-link></li>
<li><router-link to="/docs/permissions">Permissions</router-link></li>
</ul>
<p class="menu-label">Reference</p>
<ul class="menu-list">
<li><router-link to="/docs/reference/argument-types">Argument types</router-link></li>
</ul>
<p class="menu-label">Setup guides</p>
<ul class="menu-list">
<li><router-link to="/docs/setup-guides/logs">Logs</router-link></li>
<li><router-link to="/docs/setup-guides/moderation">Moderation</router-link></li>
</ul>
<p class="menu-label">Plugins</p>
<ul class="menu-list">
<li v-for="plugin in plugins">
<router-link :to="'/docs/plugins/' + plugin.name">{{ plugin.info.prettyName || plugin.name }}</router-link>
</li> </li>
</ul> </ul>
</aside> </div>
</nav>
<!-- Content -->
<div class="docs-content flex-auto overflow-x-hidden">
<router-view :key="$route.fullPath"></router-view>
</div> </div>
</div> </div>
<div class="docs-main">
<router-view :key="$route.fullPath"></router-view>
</div>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <script lang="ts">
.docs-cloak {
/* Replaced by "visible" in docs.scss */
visibility: hidden;
}
.docs {
width: 100%;
max-width: 1280px;
margin: 20px auto;
}
.navbar {
border: 1px solid #4e5d6c;
border-radius: 3px;
margin-bottom: 24px;
padding: 0 16px;
}
.docs-logo {
margin-right: 12px;
}
.docs-title {
font-weight: 600;
}
.wip-bar {
padding: 4px 10px;
margin-bottom: 24px;
background-color: #2B3E50;
border-radius: 4px;
}
.wip-bar i {
color: #fdd7a5;
font-size: 24px;
vertical-align: -3px;
margin-right: 6px;
}
.wrapper {
display: flex;
}
.docs-sidebar {
flex: 0 0 280px;
}
.docs-sidebar-content {
/* can't scroll with a long list before reaching the end of the page, figure out */
/*position: sticky;*/
/*top: 20px;*/
}
.docs-sidebar .menu {
padding: 12px 16px;
border: 1px solid #4e5d6c;
background-color: #2b3e50;
border-radius: 3px;
}
.docs-sidebar .menu-label {
font-weight: 600;
}
.docs-main {
flex: 1 1 100%;
padding: 0 24px 24px;
}
.docs-main >>> h4 {
margin-top: 1.25em; /* ? */
}
</style>
<script>
import Vue from "vue"; import Vue from "vue";
import hljs from "highlight.js/lib/highlight.js";
import hljsYaml from "highlight.js/lib/languages/yaml.js";
import VueHighlightJS from "vue-highlightjs";
import Buefy from "buefy";
import {mapState} from "vuex"; import {mapState} from "vuex";
import "../../style/icons.scss"; type TMenuItem = {
import "buefy/dist/buefy.css"; to: string;
import "highlight.js/styles/ocean.css"; label: string;
import "../../directives/trim-code"; };
import "../../style/docs.scss"; type TMenuGroup = {
label: string;
items: TMenuItem[];
};
type TMenu = TMenuGroup[];
hljs.registerLanguage("yaml", hljsYaml); const menu: TMenu = [
Vue.use(VueHighlightJS, { hljs }); {
Vue.use(Buefy); label: 'General',
items: [
{
to: '/docs/introduction',
label: 'Introduction',
},
{
to: '/docs/configuration-format',
label: 'Configuration format',
},
{
to: '/docs/plugin-configuration',
label: 'Plugin configuration',
},
{
to: '/docs/permissions',
label: 'Permissions',
},
],
},
{
label: 'Reference',
items: [
{
to: '/docs/reference/argument-types',
label: 'Argument types',
},
],
},
{
label: 'Setup guides',
items: [
{
to: '/docs/setup-guides/logs',
label: 'Logs',
},
{
to: '/docs/setup-guides/moderation',
label: 'Moderation',
},
],
},
];
export default { export default {
async mounted() { async mounted() {
await this.$store.dispatch("docs/loadAllPlugins"); await this.$store.dispatch("docs/loadAllPlugins");
}, },
computed: { computed: {
...mapState('docs', { ...mapState('docs', {
plugins: 'allPlugins', plugins: 'allPlugins',
}), }),
menu() {
return [
...menu,
{
label: 'Plugins',
items: this.plugins.map(plugin => ({
label: plugin.info.prettyName || plugin.name,
to: `/docs/plugins/${plugin.name}`,
})),
}
];
},
}, },
}; };
</script> </script>

View file

@ -1,29 +1,29 @@
<template> <template>
<div> <div>
<h1 class="z-title is-1 mb-1">Introduction</h1> <h1>Introduction</h1>
<p class="mb-1"> <p>
Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind. Zeppelin is a private moderation bot for Discord, designed with large servers and reliability in mind.
</p> </p>
<h2 class="z-title is-2 mt-2 mb-1">Getting the bot</h2> <h2>Getting the bot</h2>
<p class="mb-1"> <p>
Since the bot is currently private, access to the bot is granted on a case by case basis.<br> Since the bot is currently private, access to the bot is granted on a case by case basis.<br>
There are plans to streamline this process in the future. There are plans to streamline this process in the future.
</p> </p>
<h2 class="z-title is-2 mt-2 mb-1">Configuration</h2> <h2>Configuration</h2>
<p class="mb-1"> <p>
All Zeppelin configuration is done through the dashboard by editing a YAML config file. By default, only the server All Zeppelin configuration is done through the dashboard by editing a YAML config file. By default, only the server
owner has access to this, but they can give other users access as they see fit. See <router-link to="/docs/configuration-format">Configuration format</router-link> for more details. owner has access to this, but they can give other users access as they see fit. See <router-link to="/docs/configuration-format">Configuration format</router-link> for more details.
</p> </p>
<h2 class="z-title is-2 mt-2 mb-1">Plugins</h2> <h2>Plugins</h2>
<p class="mb-1"> <p>
Zeppelin is divided into plugins: grouped functionality that can be enabled/disabled as needed, and that have their own configurations. Zeppelin is divided into plugins: grouped functionality that can be enabled/disabled as needed, and that have their own configurations.
</p> </p>
<h2 class="z-title is-2 mt-2 mb-1">Commands</h2> <h2>Commands</h2>
<p class="mb-1"> <p>
The commands for each plugin are listed on the plugin's page (see "Plugins" on the menu). On these pages, the command prefix is assumed to be <code>!</code> but this can be changed on a per-server basis. The commands for each plugin are listed on the plugin's page (see "Plugins" on the menu). On these pages, the command prefix is assumed to be <code>!</code> but this can be changed on a per-server basis.
</p> </p>
</div> </div>

View file

@ -1,23 +1,23 @@
<template> <template>
<div> <div>
<h1 class="z-title is-1 mb-1">Permissions</h1> <h1>Permissions</h1>
<p class="mb-1"> <p>
Permissions in Zeppelin are simply values in plugin configuration that are checked when the command is used. Permissions in Zeppelin are simply values in plugin configuration that are checked when the command is used.
These values can be changed with overrides (see <router-link to="/docs/plugin-configuration">Plugin configuration</router-link> for more info) These values can be changed with overrides (see <router-link to="/docs/plugin-configuration">Plugin configuration</router-link> for more info)
and can depend on e.g. user id, role id, channel id, category id, or <strong>permission level</strong>. and can depend on e.g. user id, role id, channel id, category id, or <strong>permission level</strong>.
</p> </p>
<h2 class="z-title is-2 mt-2 mb-1">Permission levels</h2> <h2>Permission levels</h2>
<p class="mb-1"> <p>
The simplest way to control access to bot commands and features is via permission levels. The simplest way to control access to bot commands and features is via permission levels.
These levels are simply a number (usually between 0 and 100), based on the user's roles or user id, that can then These levels are simply a number (usually between 0 and 100), based on the user's roles or user id, that can then
be used in permission overrides. By default, several commands are "moderator only" (level 50 and up) or "admin only" (level 100 and up). be used in permission overrides. By default, several commands are "moderator only" (level 50 and up) or "admin only" (level 100 and up).
</p> </p>
<p class="mb-1"> <p>
Additionally, having a higher permission level means that certain commands (such as !ban) can't be used against Additionally, having a higher permission level means that certain commands (such as !ban) can't be used against
you by users with a lower or equal permission level (so e.g. moderators can't ban each other or admins above them). you by users with a lower or equal permission level (so e.g. moderators can't ban each other or admins above them).
</p> </p>
<p class="mb-1"> <p>
Permission levels are defined in the config in the <strong>levels</strong> section. For example: Permission levels are defined in the config in the <strong>levels</strong> section. For example:
</p> </p>
@ -28,14 +28,14 @@
"172950000412655616": 50 # Example mod "172950000412655616": 50 # Example mod
</CodeBlock> </CodeBlock>
<h2 class="z-title is-2 mt-2 mb-1">Examples</h2> <h2>Examples</h2>
<h3 class="z-title is-3 mb-1">Basic overrides</h3> <h3>Basic overrides</h3>
<p class="mb-1"> <p>
For this example, let's assume we have a plugin called <code>cats</code> which has a command <code>!cat</code> locked behind the permission <code>can_cat</code>. For this example, let's assume we have a plugin called <code>cats</code> which has a command <code>!cat</code> locked behind the permission <code>can_cat</code>.
Let's say that by default, the plugin allows anyone to use <code>!cat</code>, but we want to restrict it to moderators only. Let's say that by default, the plugin allows anyone to use <code>!cat</code>, but we want to restrict it to moderators only.
</p> </p>
<p class="mb-1"> <p>
Here's what the configuration for this would look like: Here's what the configuration for this would look like:
</p> </p>
@ -51,7 +51,7 @@
can_cat: true can_cat: true
</CodeBlock> </CodeBlock>
<h3 class="z-title is-3 mt-2 mb-1">Replacing defaults</h3> <h3>Replacing defaults</h3>
<p class="mb-1"> <p class="mb-1">
In this example, let's assume you don't want to use the default permission levels of 50 and 100 for mods and admins respectively. In this example, let's assume you don't want to use the default permission levels of 50 and 100 for mods and admins respectively.
Let's say you're using various incremental levels instead: 10, 20, 30, 40, 50...<br> Let's say you're using various incremental levels instead: 10, 20, 30, 40, 50...<br>

View file

@ -3,21 +3,19 @@
Loading... Loading...
</div> </div>
<div v-else> <div v-else>
<h1 class="z-title is-1 mb-1">{{ data.info.prettyName || data.name }}</h1> <h1>{{ data.info.prettyName || data.name }}</h1>
<!-- Description --> <!-- Description -->
<MarkdownBlock :content="data.info.description" class="content"></MarkdownBlock> <MarkdownBlock :content="data.info.description" class="content"></MarkdownBlock>
<div class="tabs"> <Tabs>
<ul> <Tab :active="tab === 'usage'">
<li v-bind:class="{'is-active': tab === 'usage'}"> <router-link v-bind:to="'/docs/plugins/' + pluginName + '/usage'">Usage</router-link>
<router-link v-bind:to="'/docs/plugins/' + pluginName + '/usage'">Usage</router-link> </Tab>
</li> <Tab :active="tab === 'configuration'">
<li v-bind:class="{'is-active': tab === 'configuration'}"> <router-link v-bind:to="'/docs/plugins/' + pluginName + '/configuration'">Configuration</router-link>
<router-link v-bind:to="'/docs/plugins/' + pluginName + '/configuration'">Configuration</router-link> </Tab>
</li> </Tabs>
</ul>
</div>
<!-- Usage tab --> <!-- Usage tab -->
<div class="usage" v-if="tab === 'usage'"> <div class="usage" v-if="tab === 'usage'">
@ -28,38 +26,33 @@
<!-- Usage guide --> <!-- Usage guide -->
<div v-if="data.info.usageGuide"> <div v-if="data.info.usageGuide">
<h2 id="usage-guide" class="z-title is-2 mt-2 mb-1">Usage guide</h2> <h2 id="usage-guide">Usage guide</h2>
<MarkdownBlock :content="data.info.usageGuide" class="content"></MarkdownBlock> <MarkdownBlock :content="data.info.usageGuide" class="content"></MarkdownBlock>
</div> </div>
<!-- Command list --> <!-- Command list -->
<div v-if="data.commands.length"> <div v-if="data.commands.length">
<h2 id="commands" class="z-title is-2 mt-2 mb-1">Commands</h2> <h2 id="commands">Commands</h2>
<div v-for="command in data.commands"> <div v-for="command in data.commands" class="mb-4">
<h3 class="z-title is-3 mt-2 mb-1">!{{ command.trigger }}</h3> <h3 class="text-xl">!{{ command.trigger }}</h3>
<div v-if="command.config.extra.requiredPermission"> <div v-if="command.config.extra.requiredPermission">
Permission: <code>{{ command.config.extra.requiredPermission }}</code> Permission: <code class="inline-code">{{ command.config.extra.requiredPermission }}</code>
</div> </div>
<div v-if="command.config.extra.info && command.config.extra.info.basicUsage"> <div v-if="command.config.extra.info && command.config.extra.info.basicUsage">
Basic usage: <code>{{ command.config.extra.info.basicUsage }}</code> Basic usage: <code class="inline-code">{{ command.config.extra.info.basicUsage }}</code>
</div> </div>
<div v-if="command.config.aliases && command.config.aliases.length"> <div v-if="command.config.aliases && command.config.aliases.length">
Shortcut: Shortcut:
<code style="margin-right: 4px" v-for="alias in command.config.aliases">!{{ alias }}</code> <code class="inline-code" style="margin-right: 4px" v-for="alias in command.config.aliases">!{{ alias }}</code>
</div> </div>
<MarkdownBlock v-if="command.config.info && command.config.info.description" :content="command.config.info.description" class="content mt-1 mb-1"></MarkdownBlock> <MarkdownBlock v-if="command.config.info && command.config.info.description" :content="command.config.info.description" class="content mb-4"></MarkdownBlock>
<b-collapse :open="false" class="card mt-1 mb-1"> <Expandable class="mt-4">
<div slot="trigger" slot-scope="props" class="card-header" role="button"> <template v-slot:title>Additional information</template>
<p class="card-header-title">Additional information</p> <template v-slot:content>
<a class="card-header-icon">
<b-icon :icon="props.open ? 'menu-down' : 'menu-up'"></b-icon>
</a>
</div>
<div class="card-content">
Signatures: Signatures:
<ul class="z-list z-ul"> <ul>
<li> <li>
<code> <code>
!{{ command.trigger }} !{{ command.trigger }}
@ -70,7 +63,7 @@
<div class="mt-2" v-if="command.parameters.length"> <div class="mt-2" v-if="command.parameters.length">
Command arguments: Command arguments:
<ul class="z-list z-ul"> <ul>
<li v-for="param in command.parameters"> <li v-for="param in command.parameters">
<code>{{ renderParameter(param) }}</code> <code>{{ renderParameter(param) }}</code>
<router-link :to="'/docs/reference/argument-types#' + (param.type || 'string')">{{ param.type || 'string' }}</router-link> <router-link :to="'/docs/reference/argument-types#' + (param.type || 'string')">{{ param.type || 'string' }}</router-link>
@ -95,8 +88,8 @@
</li> </li>
</ul> </ul>
</div> </div>
</div> </template>
</b-collapse> </Expandable>
</div> </div>
</div> </div>
</div> </div>
@ -104,26 +97,26 @@
<!-- Configuration tab --> <!-- Configuration tab -->
<div class="configuration" v-if="tab === 'configuration'"> <div class="configuration" v-if="tab === 'configuration'">
<!-- Basic config info --> <!-- Basic config info -->
<p class="mb-1"> <p>
Name in config: <code>{{ data.name }}</code> Name in config: <code>{{ data.name }}</code>
</p> </p>
<p class="mt-1 mb-1"> <p>
To enable this plugin with default configuration, add <code>{{ data.name }}: {}</code> to the <code>plugins</code> list in config To enable this plugin with default configuration, add <code>{{ data.name }}: {}</code> to the <code>plugins</code> list in config
</p> </p>
<!-- Configuration guide --> <!-- Configuration guide -->
<div v-if="data.info.configurationGuide"> <div v-if="data.info.configurationGuide">
<h2 id="configuration-guide" class="z-title is-2 mt-2 mb-1">Configuration guide</h2> <h2 id="configuration-guide">Configuration guide</h2>
<MarkdownBlock :content="data.info.configurationGuide" class="content"></MarkdownBlock> <MarkdownBlock :content="data.info.configurationGuide" class="content"></MarkdownBlock>
</div> </div>
<!-- Default configuration --> <!-- Default configuration -->
<h2 id="default-configuration" class="z-title is-2 mt-2 mb-1">Default configuration</h2> <h2 id="default-configuration">Default configuration</h2>
<CodeBlock lang="yaml">{{ renderConfiguration(data.defaultOptions) }}</CodeBlock> <CodeBlock lang="yaml">{{ renderConfiguration(data.defaultOptions) }}</CodeBlock>
<!-- Config schema --> <!-- Config schema -->
<h2 id="config-schema" class="z-title is-2 mt-2 mb-1">Config schema</h2> <h2 id="config-schema">Config schema</h2>
<b-collapse :open="false" class="card mt-1 mb-1"> <div :open="false" class="card mt-1 mb-1"> <!-- b-collapse -->
<div slot="trigger" slot-scope="props" class="card-header" role="button"> <div slot="trigger" slot-scope="props" class="card-header" role="button">
<p class="card-header-title">Click to expand</p> <p class="card-header-title">Click to expand</p>
<a class="card-header-icon"> <a class="card-header-icon">
@ -133,23 +126,27 @@
<div class="card-content"> <div class="card-content">
<CodeBlock lang="plain">{{ data.configSchema }}</CodeBlock> <CodeBlock lang="plain">{{ data.configSchema }}</CodeBlock>
</div> </div>
</b-collapse> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts">
import Vue from "vue"; import Vue from "vue";
import {mapState} from "vuex"; import {mapState} from "vuex";
import yaml from "js-yaml"; import yaml from "js-yaml";
import CodeBlock from "./CodeBlock"; import CodeBlock from "./CodeBlock.vue";
import MarkdownBlock from "./MarkdownBlock"; import MarkdownBlock from "./MarkdownBlock.vue";
import Tabs from "../Tabs.vue";
import Tab from "../Tab.vue";
import Expandable from "../Expandable.vue";
import { DocsState } from "../../store/types";
const validTabs = ['usage', 'configuration']; const validTabs = ['usage', 'configuration'];
const defaultTab = 'usage'; const defaultTab = 'usage';
export default { export default {
components: { CodeBlock, MarkdownBlock }, components: { CodeBlock, MarkdownBlock, Tabs, Tab, Expandable },
async mounted() { async mounted() {
this.loading = true; this.loading = true;
@ -201,7 +198,7 @@
}, },
computed: { computed: {
...mapState("docs", { ...mapState("docs", {
data(state) { data(state: DocsState) {
return state.plugins[this.pluginName]; return state.plugins[this.pluginName];
}, },
hasUsageInfo() { hasUsageInfo() {

View file

@ -1,20 +1,20 @@
<template> <template>
<div> <div>
<h1 class="z-title is-1 mb-1">Plugin configuration</h1> <h1>Plugin configuration</h1>
<p class="mb-1"> <p>
Each plugin in Zeppelin has its own configuration options. In the config editor, you can both set the default config Each plugin in Zeppelin has its own configuration options. In the config editor, you can both set the default config
and overrides based on specific conditions. Permissions are also just values in the plugin's config, and thus follow and overrides based on specific conditions. Permissions are also just values in the plugin's config, and thus follow
the same rules with overrides etc. as other options (see <router-link to="/docs/permissions">Permissions</router-link> for more info). the same rules with overrides etc. as other options (see <router-link to="/docs/permissions">Permissions</router-link> for more info).
</p> </p>
<p class="mb-1"> <p>
Information about each plugin's options can be found on the plugin's page on the sidebar. See <router-link to="/docs/configuration-format">Configuration format</router-link> for an example of a full config. Information about each plugin's options can be found on the plugin's page on the sidebar. See <router-link to="/docs/configuration-format">Configuration format</router-link> for an example of a full config.
</p> </p>
<h2 class="z-title is-2 mt-2 mb-1">Overrides</h2> <h2>Overrides</h2>
<p class="mb-1"> <p>
Overrides are the primary mechanism of changing options and permissions based on permission levels, roles, channels, user ids, etc. Overrides are the primary mechanism of changing options and permissions based on permission levels, roles, channels, user ids, etc.
</p> </p>
<p class="mb-1"> <p>
Here's an example demonstrating different types of overrides: Here's an example demonstrating different types of overrides:
</p> </p>
@ -74,8 +74,8 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import CodeBlock from "./CodeBlock"; import CodeBlock from "./CodeBlock.vue";
export default { export default {
components: { CodeBlock }, components: { CodeBlock },

View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<h1 class="z-title is-1 mb-1">Work in progress</h1> <h1>Work in progress</h1>
<p class="mb-1"> <p>
This page is a work in progress. This page is a work in progress.
</p> </p>
</div> </div>

View file

@ -1,11 +0,0 @@
import Vue from "vue";
Vue.directive("trim-code", {
bind(el, binding) {
el.innerHTML = el.innerHTML
.replace(/(^\n+|\n+$)/g, "")
.split("\n")
.map(line => line.slice(binding.value))
.join("\n");
},
});

View file

@ -0,0 +1,25 @@
import Vue from "vue";
Vue.directive("trim-indents", {
bind(el, binding) {
const withoutStartEndWhitespace = el.innerHTML.replace(/(^\n+|\n+$)/g, "");
const mode = binding.value != null ? binding.value : "start";
let spacesToTrim;
if (mode === "start") {
const match = withoutStartEndWhitespace.match(/^\s+/);
spacesToTrim = match ? match[0].length : 0;
} else if (mode === "end") {
const match = withoutStartEndWhitespace.match(/\s+$/);
spacesToTrim = match ? match[0].length : 0;
} else {
spacesToTrim = parseInt(mode, 10);
}
el.innerHTML = withoutStartEndWhitespace
.split("\n")
.map(line => line.slice(spacesToTrim))
.join("\n");
},
});

View file

@ -8,8 +8,11 @@
<title>Zeppelin Dashboard</title> <title>Zeppelin Dashboard</title>
</head> </head>
<body> <body>
<div id="app"></div> <noscript>
<h1>Zeppelin</h1>
The Zeppelin dashboard requires JavaScript to load.
</noscript>
<script src="./main.ts"></script> <div id="app"></div>
</body> </body>
</html> </html>

40
dashboard/src/init-vue.ts Normal file
View file

@ -0,0 +1,40 @@
import "./style/app.pcss";
import Vue from "vue";
import hljs from "highlight.js/lib/highlight.js";
import hljsYaml from "highlight.js/lib/languages/yaml.js";
import VueHighlightJS from "vue-highlightjs";
import "highlight.js/styles/ocean.css";
import { RootStore } from "./store";
import { router } from "./routes";
import "./directives/trim-indents";
import App from "./components/App.vue";
// Set up a read-only global variable to access specific env vars
Vue.mixin({
data() {
return {
get env() {
return Object.freeze({
API_URL: process.env.API_URL,
});
},
};
},
});
hljs.registerLanguage("yaml", hljsYaml);
Vue.use(VueHighlightJS, { hljs });
const app = new Vue({
router,
store: RootStore,
el: "#app",
render(h) {
return h(App);
},
});

View file

@ -1,29 +1,32 @@
import "./style/base.scss"; import "./style/initial.pcss";
const splashHtml = require("./splash.html");
import Vue from "vue"; if (window.location.pathname !== "/") {
import { RootStore } from "./store"; import("./init-vue");
import { router } from "./routes"; } else {
// @ts-ignore
document.querySelector("#app").innerHTML = splashHtml;
// Set up a read-only global variable to access specific env vars const queryParams: any = window.location.search
Vue.mixin({ .slice(1)
data() { .split("&")
return { .reduce((map, str) => {
get env() { const pair = str.split("=");
return Object.freeze({ map[pair[0]] = pair[1];
API_URL: process.env.API_URL, 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.",
}; };
},
});
import App from "./components/App.vue"; const errorMessageElem = document.createElement("div");
errorMessageElem.classList.add("message");
const app = new Vue({ errorMessageElem.innerText = errorMessages[queryParams.error] || "Unexpected error";
router, errorElement.appendChild(errorMessageElem);
store: RootStore, }
el: "#app", }
render(h) {
return h(App);
},
});

View file

@ -1,6 +1,5 @@
import Vue from "vue"; import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router"; import VueRouter, { RouteConfig } from "vue-router";
import Splash from "./components/Splash.vue";
import { authGuard, authRedirectGuard, loginCallbackGuard } from "./auth"; import { authGuard, authRedirectGuard, loginCallbackGuard } from "./auth";
Vue.use(VueRouter); Vue.use(VueRouter);
@ -8,7 +7,6 @@ Vue.use(VueRouter);
export const router = new VueRouter({ export const router = new VueRouter({
mode: "history", mode: "history",
routes: [ routes: [
{ path: "/", component: Splash },
{ path: "/login", beforeEnter: authRedirectGuard }, { path: "/login", beforeEnter: authRedirectGuard },
{ path: "/login-callback", beforeEnter: loginCallbackGuard }, { path: "/login-callback", beforeEnter: loginCallbackGuard },

18
dashboard/src/splash.html Normal file
View file

@ -0,0 +1,18 @@
<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="/login">Dashboard</a>
<a class="btn" href="/docs">Documentation</a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,18 @@
@import "~tailwindcss/base.css";
@import "~tailwindcss/components.css";
@import "~tailwindcss/utilities.css";
@import "~vue-material-design-icons/styles.css";
@import "components.pcss";
@import "docs.pcss";
body {
overflow-y: scroll;
@apply bg-gray-900;
@apply text-gray-300;
@apply text-base;
@apply p-4;
}

View file

@ -0,0 +1,4 @@
body {
font: normal 18px/1.5 sans-serif;
font-family: system, -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif;
}

View file

@ -1,6 +0,0 @@
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400,600&display=swap');
@import "~bulma/sass/base/minireset";
body {
font: normal 16px/1.4 'Open Sans', sans-serif;
}

View file

@ -0,0 +1,21 @@
.inline-code {
@apply inline-block;
@apply bg-gray-800;
@apply px-1;
@apply rounded;
@apply text-sm;
}
.codeblock {
@apply bg-gray-800;
@apply p-3;
@apply mb-4;
@apply rounded;
@apply text-sm;
@apply shadow-md;
& .hljs {
@apply bg-transparent;
@apply p-0;
}
}

View file

@ -1,9 +1,78 @@
@import "~buefy/dist/buefy.css";
@import "~highlight.js/styles/ocean.css";
$bulmaswatch-import-font: false;
$family-primary: 'Open Sans', sans-serif; $family-primary: 'Open Sans', sans-serif;
$list-background-color: transparent;
$size-1: 2.5rem;
$size-2: 2rem;
$size-3: 1.5rem;
$size-4: 1.25rem;
@import "~bulmaswatch/superhero/_variables"; @import "~bulmaswatch/superhero/_variables";
$tabs-link-color: $grey-light;
$tabs-link-active-color: $grey-lighter;
$tabs-link-active-border-bottom-color: $grey-lighter;
@import "~bulma/bulma"; @import "~bulma/bulma";
@import "~bulmaswatch/superhero/_overrides"; @import "~bulmaswatch/superhero/_overrides";
.dashboard-cloak {
.init-cloak {
visibility: visible !important; visibility: visible !important;
} }
.z-title {
line-height: 1.125;
&.is-1 { font-size: $size-1; }
&.is-2 { font-size: $size-2; }
&.is-3 { font-size: $size-3; }
&.is-4 { font-size: $size-4; }
&.is-5 { font-size: $size-5; }
&.is-6 { font-size: $size-6; }
}
.z-list {
margin-left: 1.5rem;
}
.z-ul {
list-style: disc;
}
.mt-1 { margin-top: 1rem; }
.mt-2 { margin-top: 1.5rem; }
.mt-3 { margin-top: 2rem; }
.mb-1 { margin-bottom: 1rem; }
.mb-2 { margin-bottom: 1.5rem; }
.mb-3 { margin-bottom: 2rem; }
.codeblock,
.content .codeblock {
border-radius: 3px;
padding: 16px;
max-width: 970px; /* FIXME: temp fix for overflowing code blocks, look into properly later */
}
.codeblock .hljs {
background: transparent;
padding: 0;
}
.menu-label {
&:not(:first-child) {
margin-top: 1.4em;
}
&:not(:last-child) {
margin-bottom: 0.4em;
}
}
.menu-list .router-link-active {
text-decoration: underline;
}

View file

@ -0,0 +1,60 @@
.docs-sidebar {
& .router-link-active {
@apply underline;
}
}
.docs-content {
& h1 {
@apply text-5xl;
@apply font-semibold;
@apply leading-none;
@apply pb-4;
}
& h2 {
@apply text-2xl;
@apply font-semibold;
@apply pt-2;
@apply pb-1;
}
& h3 {
@apply font-semibold;
@apply pb-1;
}
& p {
@apply pb-4;
& code {
@apply inline-code;
}
}
& a:not([class]) {
@apply text-blue-400;
@apply underline;
&:hover {
@apply text-blue-200;
}
}
& ul:not([class]) {
@apply list-disc;
@apply mb-4;
& li {
@apply ml-6;
}
& code {
@apply inline-code;
}
}
& .expandable {
max-width: 600px;
}
}

View file

@ -1,77 +0,0 @@
@import url('https://cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css');
$bulmaswatch-import-font: false;
$family-primary: 'Open Sans', sans-serif;
$list-background-color: transparent;
$size-1: 2.5rem;
$size-2: 2rem;
$size-3: 1.5rem;
$size-4: 1.25rem;
@import "~bulmaswatch/superhero/_variables";
$tabs-link-color: $grey-light;
$tabs-link-active-color: $grey-lighter;
$tabs-link-active-border-bottom-color: $grey-lighter;
@import "~bulma/bulma";
@import "~bulmaswatch/superhero/_overrides";
.docs-cloak {
visibility: visible !important;
}
.z-title {
line-height: 1.125;
&.is-1 { font-size: $size-1; }
&.is-2 { font-size: $size-2; }
&.is-3 { font-size: $size-3; }
&.is-4 { font-size: $size-4; }
&.is-5 { font-size: $size-5; }
&.is-6 { font-size: $size-6; }
}
.z-list {
margin-left: 1.5rem;
}
.z-ul {
list-style: disc;
}
.mt-1 { margin-top: 1rem; }
.mt-2 { margin-top: 1.5rem; }
.mt-3 { margin-top: 2rem; }
.mb-1 { margin-bottom: 1rem; }
.mb-2 { margin-bottom: 1.5rem; }
.mb-3 { margin-bottom: 2rem; }
.codeblock,
.content .codeblock {
border-radius: 3px;
padding: 16px;
max-width: 970px; /* FIXME: temp fix for overflowing code blocks, look into properly later */
}
.codeblock .hljs {
background: transparent;
padding: 0;
}
.menu-label {
&:not(:first-child) {
margin-top: 1.4em;
}
&:not(:last-child) {
margin-bottom: 0.4em;
}
}
.menu-list .router-link-active {
text-decoration: underline;
}

View file

@ -1 +0,0 @@
@import url("https://cdn.materialdesignicons.com/2.5.94/css/materialdesignicons.min.css");

View file

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

View file

@ -0,0 +1,48 @@
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Remove default padding */
ul,
ol {
padding: 0;
}
/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
ul,
ol,
li,
figure,
figcaption,
blockquote,
dl,
dd {
margin: 0;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}
/* Remove all animations and transitions for people that prefer not to see them */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View file

@ -1,58 +1,82 @@
.splash { .splash {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
padding: 16px;
background-color: #7289da; background-color: #7289da;
background-image: linear-gradient(225deg, #7289da 0%, #5d70b4 100%); background-image: linear-gradient(225deg, #7289da 0%, #5d70b4 100%);
color: #fff; color: #fff;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
justify-content: center; justify-content: flex-start;
align-items: flex-start; align-items: center;
a { & a {
color: #fff; color: #fff;
} }
.wrapper { & > #error {
flex: 0 1 750px; width: 100%;
max-width: 750px;
flex-direction: row;
justify-content: center;
&.has-error {
display: flex;
}
& .message {
flex: 0 1 auto;
text-align: left;
padding: 8px 12px;
background-color: #404040;
border-radius: 4px;
box-shadow: 0 3px 12px -2px hsla(0, 0%, 0%, 0.2);
}
}
& .wrapper {
flex: 0 0 auto;
width: 750px;
max-width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: start; align-items: flex-start;
.logo-column { & .logo-column {
flex: 0 0 auto; flex: 0 0 auto;
} }
.info-column { & .info-column {
flex: 1 1 100%; flex: 1 1 100%;
} }
.logo { & .logo {
width: 300px; width: 300px;
height: 300px; height: 300px;
margin-right: 64px; margin-right: 48px;
} }
h1 { & h1 {
font-size: 80px; font-size: 80px;
font-weight: 300; font-weight: 300;
margin-top: 40px margin-top: 40px
} }
.description { & .description {
color: #f1f5ff; color: #f1f5ff;
} }
.actions { & .actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 8px; margin-top: 8px;
margin-left: -12px; // Negative button margin margin-left: -12px;
.btn { & .btn {
margin: 12px; margin: 12px;
text-decoration: none; text-decoration: none;
padding: 8px 24px; padding: 8px 24px;
@ -73,7 +97,7 @@
} }
} }
.error { & .error {
margin-top: 8px; margin-top: 8px;
background-color: hsl(224, 52%, 32%); background-color: hsl(224, 52%, 32%);
padding: 12px; padding: 12px;

View file

@ -0,0 +1,7 @@
module.exports = {
theme: {
extend: {}
},
variants: {},
plugins: []
}

View file

@ -2,28 +2,20 @@
"compilerOptions": { "compilerOptions": {
"moduleResolution": "node", "moduleResolution": "node",
"module": "esnext", "module": "esnext",
"target": "esnext",
"sourceMap": true, "sourceMap": true,
"noImplicitAny": false, "noImplicitAny": false,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"target": "esnext", "strict": false,
"strict": true,
"lib": [ "lib": [
"es2017",
"esnext", "esnext",
"dom" "dom"
], ],
"baseUrl": "./", "baseUrl": ".",
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"outDir": "./dist" "allowJs": true
}, }
"include": [
"src/**/*.ts",
"src/**/*.vue"
],
"files": [
"ts-vue-shim.d.ts"
]
} }

169
dashboard/webpack.config.js Normal file
View file

@ -0,0 +1,169 @@
require('dotenv').config();
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const DotenvPlugin = require('dotenv-webpack');
const merge = require('webpack-merge');
const targetDir = path.normalize(path.join(__dirname, 'dist'));
if (! process.env.NODE_ENV) {
console.error('Please set NODE_ENV');
process.exit(1);
}
const babelOpts = {
presets: [
'@babel/preset-env',
],
};
let config = {
entry: './src/main.ts',
output: {
filename: '[name].[hash].js',
path: targetDir,
publicPath: '/',
},
module: {
rules: [
// Vue / Babel / Typescript
{
test: /\.vue$/,
use: ["vue-loader"],
},
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: babelOpts,
},
{
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
},
],
},
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: babelOpts,
},
},
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre",
},
// Stylesheets
{
test: /\.p?css$/,
use: [
"vue-style-loader",
{
loader: "css-loader",
options: {
importLoaders: 1,
},
},
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: loader => {
const plugins = [
require('postcss-import')({
resolve(id, base, options) {
// Since WebStorm doesn't resolve imports from node_modules without a tilde (~) prefix,
// strip the tilde here to get the best of both worlds (webstorm support + postcss-import support)
if (id[0] === '~') id = id.slice(1);
// Call the original resolver after stripping the tilde
return require('postcss-import/lib/resolve-id')(id, base, options);
},
}),
require('postcss-nesting')(),
require('tailwindcss')(),
];
if (process.env.NODE_ENV === "production") {
plugins.push(
require('postcss-preset-env')(),
require('cssnano')(),
);
}
return plugins;
},
},
},
],
},
// Images/files
{
test: /\.(png|jpg)$/i,
use: ["file-loader"],
},
// HTML
{
test: /\.html$/,
use: [
{
loader: "html-loader",
options: {
root: path.resolve(__dirname, 'src'),
attrs: ['img:src', 'link:href'],
...(process.env.NODE_ENV === 'production' && {
minimize: true,
removeComments: true,
collapseWhitespace: true,
}),
},
},
],
},
],
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: 'src/index.html',
files: {
"css": ["./src/style/initial.pcss"],
"js": ["./src/main.ts"],
},
}),
new DotenvPlugin(),
],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.mjs', '.vue'],
},
};
if (process.env.NODE_ENV === 'production') {
config = merge(config, {
mode: 'production',
devtool: 'source-map',
});
} else {
config = merge(config, {
mode: 'development',
devtool: 'eval',
devServer: {
...(process.env.DEV_HOST ? { host: process.env.DEV_HOST } : undefined),
historyApiFallback: true,
port: 1234,
},
});
}
module.exports = config;

View file

@ -117,7 +117,7 @@ export function initAuth(app: express.Express) {
if (req.user && req.user.apiKey) { if (req.user && req.user.apiKey) {
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`); res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?apiKey=${req.user.apiKey}`);
} else { } else {
res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?error=noaccess`); res.redirect(`${process.env.DASHBOARD_URL}/login-callback/?error=noAccess`);
} }
}, },
); );