Browse Source

Add rule editor

ghorsington 3 years ago
parent
commit
c7f5761d10

+ 3 - 0
web/package.json

@@ -31,6 +31,7 @@
 		"compression": "^1.7.4",
 		"cookie-session": "^1.4.0",
 		"dotenv": "^8.2.0",
+		"easymde": "^2.11.0",
 		"express": "^4.17.1",
 		"google-protobuf": "^3.13.0",
 		"got": "^11.5.2",
@@ -38,6 +39,7 @@
 		"node-fetch": "^2.6.0",
 		"nodemailer": "^6.4.11",
 		"pg": "^8.3.3",
+		"postcss-nested": "^4.2.3",
 		"rpc_ts": "^2.1.0",
 		"sirv": "^1.0.5",
 		"svelte-awesome": "^2.3.0",
@@ -79,6 +81,7 @@
 		"postcss": "^7.0.32",
 		"postcss-import": "^12.0.1",
 		"postcss-load-config": "^2.1.0",
+		"postcss-preset-env": "^6.7.0",
 		"reflect-metadata": "^0.1.13",
 		"rollup": "^2.26.7",
 		"rollup-plugin-svelte": "^5.2.3",

+ 17 - 5
web/src/routes/_layout.svelte

@@ -1,9 +1,18 @@
 <script lang="typescript" context="module">
-	import type { AppSession, PageData } from "src/utils/session";
+	import type { AppSession, PageData, PreloadContext } from "src/utils/session";
+	import { goto } from "@sapper/app";
 
-	export async function preload({ path }: PageData, session: AppSession) {
-		console.log(path);
-		console.log(session);
+	export async function preload(this: PreloadContext, { path }: PageData, session: AppSession) {
+		if (path.startsWith("/login/")) {
+			if (session?.userId) {
+				return this.redirect(302, "/");
+			} else {
+				return {};
+			}
+		}
+		if (path != "/" && !session?.userId) {
+			return this.redirect(302, "/");
+		}
 		return {};
 	}
 </script>
@@ -32,4 +41,7 @@
 	</title>
 </svelte:head>
 
-<slot />
+
+<div class="flex items-center justify-center bg-gray-900 h-screen w-screen">
+	<slot />	
+</div>

+ 16 - 14
web/src/routes/index.svelte

@@ -1,18 +1,20 @@
 <script lang="typescript">
-	import Icon from "svelte-awesome/components/Icon.svelte";
-	import { faDiscord } from "@fortawesome/free-brands-svg-icons";
-	import ExampleComponent from "../components/ExampleComponent.svelte";
+  import Icon from "svelte-awesome/components/Icon.svelte";
+  import { faDiscord } from "@fortawesome/free-brands-svg-icons";
+  import ExampleComponent from "../components/ExampleComponent.svelte";
 
-	const discordIcon = faDiscord as unknown as undefined;
+  const discordIcon = (faDiscord as unknown) as undefined;
 </script>
 
-<div class="flex items-center justify-center bg-gray-900 h-screen w-screen">
-	<div class="bg-gray-800 rounded-sm text-center px-20 py-5 shadow-lg">
-		<h1 class="text-white text-4xl font-light">Login</h1>
-		<p class="px-10 py-5">
-			<a class="text-sm bg-blue-700 hover:bg-blue-800 p-2 text-white hover:text-gray-100 rounded-sm" href="/login/discord">
-				<Icon data={discordIcon} /> Login with Discord
-			</a>
-		</p>
-	</div>
-</div>
+<div class="bg-gray-800 rounded-sm text-center px-20 py-5 shadow-lg">
+  <h1 class="text-white text-4xl font-light">Login</h1>
+  <p class="px-10 py-5">
+    <a
+      class="text-sm bg-blue-700 hover:bg-blue-800 p-2 text-white
+        hover:text-gray-100 rounded-sm"
+      href="/login/discord">
+      <Icon data={discordIcon} />
+      Login with Discord
+    </a>
+  </p>
+</div>

+ 34 - 34
web/src/routes/login/discord_auth.svelte

@@ -1,39 +1,39 @@
 <script lang="typescript">
-	import Icon from "svelte-awesome/components/Icon.svelte";
-	import { faSpinner } from "@fortawesome/free-solid-svg-icons";
-    import { onMount, tick } from "svelte";
-    import { Option } from "@shared/common/async_utils";
-    import { goto } from "@sapper/app";
+  import Icon from "svelte-awesome/components/Icon.svelte";
+  import { faSpinner } from "@fortawesome/free-solid-svg-icons";
+  import { onMount, tick } from "svelte";
+  import { Option } from "@shared/common/async_utils";
+  import { goto } from "@sapper/app";
 
-    const spinner = faSpinner as unknown as undefined;
-    let error = "";
-    let ok = true;
-    
+  const spinner = (faSpinner as unknown) as undefined;
+  let error = "";
+  let ok = true;
 
-    onMount(
-        async () => {
-            const res = await fetch("/login/discord", {
-                method: "post"
-            });
-            const result = await res.json() as Option<unknown, {error: string}>;
-            if (!result.ok) {
-                ok = result.ok;
-                error = result.error;
-            } else {
-                await goto("/");
-            }
-        }
-    );
+  onMount(async () => {
+    const res = await fetch("/login/discord", {
+      method: "post",
+    });
+    const result = (await res.json()) as Option<unknown, { error: string }>;
+    if (!result.ok) {
+      ok = result.ok;
+      error = result.error;
+    } else {
+      await goto("/");
+    }
+  });
 </script>
 
-<div class="flex items-center justify-center bg-gray-900 h-screen w-screen">
-	<div class="bg-gray-800 rounded-sm text-center px-20 py-5 shadow-lg">
-        <h1 class="text-white text-4xl font-light">Logging in</h1>
-        {#if ok}
-            <div class="text-white"><Icon data={spinner} spin scale={1.5} /></div>
-        {:else}
-            <div class="text-red-600 max-w-md py-4">{error}</div>
-            <a class="text-sm bg-blue-700 hover:bg-blue-800 p-2 text-white hover:text-gray-100 rounded-sm" href="/">Back to login</a>
-        {/if}
-	</div>
-</div>
+<div class="bg-gray-800 rounded-sm text-center px-20 py-5 shadow-lg">
+  <h1 class="text-white text-4xl font-light">Logging in</h1>
+  {#if ok}
+    <div class="text-white">
+      <Icon data={spinner} spin scale={1.5} />
+    </div>
+  {:else}
+    <div class="text-red-600 max-w-md py-4">{error}</div>
+    <a
+      class="text-sm bg-blue-700 hover:bg-blue-800 p-2 text-white
+        hover:text-gray-100 rounded-sm"
+      href="/">Back to login</a>
+  {/if}
+</div>

+ 35 - 0
web/src/routes/rules/create.svelte

@@ -0,0 +1,35 @@
+<script lang="typescript">
+    import { onMount } from "svelte";
+    import "easymde/dist/easymde.min.css";
+
+    let vpHeight: number;
+    let textArea!: HTMLElement;
+    onMount(async () => {
+        const EasyMDE = (await import("easymde")).default;
+        let mde = new EasyMDE({
+            element: textArea,
+            maxHeight: `50vh`,
+            minHeight: `50vh`,
+            previewClass: "md-dark md-editor-preview md-body",
+        });
+    });
+</script>
+
+<style>
+    @import "./markdown-dark.css";
+    @import "./easymde-style.css";
+
+    .viewport {
+        @apply bg-gray-800 rounded-sm px-20 py-5 shadow-lg w-3/4;
+        height: 80vh;
+    }
+</style>
+
+<div class="viewport" bind:clientHeight={vpHeight}>
+    <h1 class="text-white text-4xl font-light py-2">Edit rules</h1>
+    <p class="text-white md-editor">
+        <textarea id="easy-mde" bind:this={textArea}></textarea>
+    </p>
+    <a class="text-md float-right bg-green-600 hover:bg-green-800 p-2 px-3 text-white hover:text-gray-100 rounded-sm" href="#">Save</a>
+  </div>
+

+ 17 - 0
web/src/routes/rules/easymde-style.css

@@ -0,0 +1,17 @@
+:global .md-editor {
+    .editor-toolbar.fullscreen {
+        @apply bg-gray-800;
+    }
+    
+    .editor-toolbar button.active, .editor-toolbar button:hover {
+        @apply bg-gray-900;
+    }
+    
+    .md-editor-preview {
+        @apply bg-gray-800;
+    }
+    
+    .editor-statusbar {
+        @apply text-white;
+    }
+}

+ 255 - 0
web/src/routes/rules/markdown-dark.css

@@ -0,0 +1,255 @@
+:global .md-dark {
+    body, &.md-body {
+        margin: 0 auto;
+        color: #EEE;
+        line-height: 1;
+        padding: 30px;
+    }
+    h1, h2, h3, h4 {
+        font-weight: 400;
+    }
+    h1, h2, h3, h4, h5, p {
+        margin-bottom: 24px;
+        padding: 0;
+    }
+    h1 {
+        font-size: 48px;
+    }
+    h2 {
+        font-size: 36px;
+        margin: 24px 0 6px;
+    }
+    h3 {
+        font-size: 24px;
+    }
+    h4 {
+        font-size: 21px;
+    }
+    h5 {
+        font-size: 18px;
+    }
+    a {
+        color: #61BFC1;
+        margin: 0;
+        padding: 0;
+        text-decoration: none;
+        vertical-align: baseline;
+    }
+    a:hover {
+        text-decoration: underline;
+    }
+    a:visited {
+        color: #466B6C;
+    }
+    ul, ol {
+        padding: 0;
+        margin: 0;
+    }
+    li {
+        line-height: 24px;
+    }
+    li ul, li ul {
+        margin-left: 24px;
+    }
+    p, ul, ol {
+        font-size: 16px;
+        line-height: 24px;
+        max-width: 540px;
+    }
+    pre {
+        padding: 0px 24px;
+        max-width: 800px;
+        white-space: pre-wrap;
+    }
+    code {
+        font-family: Consolas, Monaco, Andale Mono, monospace;
+        line-height: 1.5;
+        font-size: 13px;
+    }
+    aside {
+        display: block;
+        float: right;
+        width: 390px;
+    }
+    blockquote {
+        border-left:.5em solid #eee;
+        padding: 0 2em;
+        margin-left:0;
+        max-width: 476px;
+    }
+    blockquote  cite {
+        font-size:14px;
+        line-height:20px;
+        color:#bfbfbf;
+    }
+    blockquote cite:before {
+        content: '\2014 \00A0';
+    }
+    
+    blockquote p {  
+        color: rgb(185, 185, 185);
+        max-width: 460px;
+    }
+    hr {
+        width: 540px;
+        text-align: left;
+        margin: 0 auto 0 0;
+        color: #999;
+    }
+    
+    /* Code below this line is copyright Twitter Inc. */
+    
+    button,
+    input,
+    select,
+    textarea {
+      font-size: 100%;
+      margin: 0;
+      vertical-align: baseline;
+      *vertical-align: middle;
+    }
+    button, input {
+      line-height: normal;
+      *overflow: visible;
+    }
+    button::-moz-focus-inner, input::-moz-focus-inner {
+      border: 0;
+      padding: 0;
+    }
+    button,
+    input[type="button"],
+    input[type="reset"],
+    input[type="submit"] {
+      cursor: pointer;
+      -webkit-appearance: button;
+    }
+    input[type=checkbox], input[type=radio] {
+      cursor: pointer;
+    }
+    /* override default chrome & firefox settings */
+    input:not([type="image"]), textarea {
+      -webkit-box-sizing: content-box;
+      -moz-box-sizing: content-box;
+      box-sizing: content-box;
+    }
+    
+    input[type="search"] {
+      -webkit-appearance: textfield;
+      -webkit-box-sizing: content-box;
+      -moz-box-sizing: content-box;
+      box-sizing: content-box;
+    }
+    input[type="search"]::-webkit-search-decoration {
+      -webkit-appearance: none;
+    }
+    label,
+    input,
+    select,
+    textarea {
+      font-size: 13px;
+      font-weight: normal;
+      line-height: normal;
+      margin-bottom: 18px;
+    }
+    input[type=checkbox], input[type=radio] {
+      cursor: pointer;
+      margin-bottom: 0;
+    }
+    input[type=text],
+    input[type=password],
+    textarea,
+    select {
+      display: inline-block;
+      width: 210px;
+      padding: 4px;
+      font-size: 13px;
+      font-weight: normal;
+      line-height: 18px;
+      height: 18px;
+      color: #808080;
+      border: 1px solid #ccc;
+      -webkit-border-radius: 3px;
+      -moz-border-radius: 3px;
+      border-radius: 3px;
+    }
+    select, input[type=file] {
+      height: 27px;
+      line-height: 27px;
+    }
+    textarea {
+      height: auto;
+    }
+    
+    /* grey out placeholders */
+    :-moz-placeholder {
+      color: #bfbfbf;
+    }
+    ::-webkit-input-placeholder {
+      color: #bfbfbf;
+    }
+    
+    input[type=text],
+    input[type=password],
+    select,
+    textarea {
+      -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+      -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+      transition: border linear 0.2s, box-shadow linear 0.2s;
+      -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+      -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+      box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+    }
+    input[type=text]:focus, input[type=password]:focus, textarea:focus {
+      outline: none;
+      border-color: rgba(82, 168, 236, 0.8);
+      -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+      -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+      box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+    }
+    
+    /* buttons */
+    button {
+      display: inline-block;
+      padding: 4px 14px;
+      font-size: 13px;
+      line-height: 18px;
+      -webkit-border-radius: 4px;
+      -moz-border-radius: 4px;
+      border-radius: 4px;
+      -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+      -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+      background-color: #0064cd;
+      background-repeat: repeat-x;
+      background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));
+      background-image: -moz-linear-gradient(top, #049cdb, #0064cd);
+      background-image: -ms-linear-gradient(top, #049cdb, #0064cd);
+      background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));
+      background-image: -webkit-linear-gradient(top, #049cdb, #0064cd);
+      background-image: -o-linear-gradient(top, #049cdb, #0064cd);
+      background-image: linear-gradient(top, #049cdb, #0064cd);
+      color: #fff;
+      text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+      border: 1px solid #004b9a;
+      border-bottom-color: #003f81;
+      -webkit-transition: 0.1s linear all;
+      -moz-transition: 0.1s linear all;
+      transition: 0.1s linear all;
+      border-color: #0064cd #0064cd #003f81;
+      border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+    }
+    button:hover {
+      color: #fff;
+      background-position: 0 -15px;
+      text-decoration: none;
+    }
+    button:active {
+      -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+      -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+      box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+    }
+    button::-moz-focus-inner {
+      padding: 0;
+      border: 0;
+    }
+}

+ 56 - 53
web/src/service-worker.ts

@@ -10,77 +10,80 @@ const toCache = (shell as string[]).concat(files as string[]);
 const cached = new Set(toCache);
 
 self.addEventListener("install", <EventType extends ExtendableEvent>(event: EventType) => {
-	event.waitUntil(
-		caches
-			.open(ASSETS)
-			.then((cache) => cache.addAll(toCache))
-			.then(() => {
-				(self as any as ServiceWorkerGlobalScope).skipWaiting();
-			}),
-	);
+    event.waitUntil(
+        caches
+            .open(ASSETS)
+            .then((cache) => cache.addAll(toCache))
+            .then(() => {
+                (self as any as ServiceWorkerGlobalScope).skipWaiting();
+            }),
+    );
 });
 
 self.addEventListener("activate", <EventType extends ExtendableEvent>(event: EventType) => {
-	event.waitUntil(
-		caches.keys().then(async (keys) => {
-			// delete old caches
-			for (const key of keys) { // eslint-disable-line no-restricted-syntax
-				if (key !== ASSETS) await caches.delete(key); // eslint-disable-line no-await-in-loop
-			}
+    event.waitUntil(
+        caches.keys().then(async (keys) => {
+            // delete old caches
+            for (const key of keys) { // eslint-disable-line no-restricted-syntax
+                if (key !== ASSETS) {
+                    // eslint-disable-next-line no-await-in-loop
+                    await caches.delete(key);
+                }
+            }
 
-			(self as any as {clients: Clients}).clients.claim();
-		}),
-	);
+            (self as any as {clients: Clients}).clients.claim();
+        }),
+    );
 });
 
 self.addEventListener("fetch", <EventType extends FetchEvent>(event: EventType) => {
-	if (event.request.method !== "GET" || event.request.headers.has("range")) return;
+    if (event.request.method !== "GET" || event.request.headers.has("range")) return;
 
-	const url = new URL(event.request.url);
+    const url = new URL(event.request.url);
 
-	// don't try to handle e.g. data: URIs
-	if (!url.protocol.startsWith("http")) return;
+    // don't try to handle e.g. data: URIs
+    if (!url.protocol.startsWith("http")) return;
 
-	// ignore dev server requests
-	if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
+    // ignore dev server requests
+    if (url.hostname === self.location.hostname && url.port !== self.location.port) return;
 
-	// always serve static files and bundler-generated assets from cache
-	if (url.host === self.location.host && cached.has(url.pathname)) {
-		caches.match(event.request).then((match): void => {
-			if (match) event.respondWith(match);
-		});
-		return;
-	}
+    // always serve static files and bundler-generated assets from cache
+    if (url.host === self.location.host && cached.has(url.pathname)) {
+        caches.match(event.request).then((match): void => {
+            if (match) event.respondWith(match);
+        });
+        return;
+    }
 
-	// for pages, you might want to serve a shell `service-worker-index.html` file,
-	// which Sapper has generated for you. It's not right for every
-	// app, but if it's right for yours then uncomment this section
-	/*
+    // for pages, you might want to serve a shell `service-worker-index.html` file,
+    // which Sapper has generated for you. It's not right for every
+    // app, but if it's right for yours then uncomment this section
+    /*
 	if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
 		event.respondWith(caches.match('/service-worker-index.html'));
 		return;
 	}
 	*/
 
-	if (event.request.cache === "only-if-cached") return;
+    if (event.request.cache === "only-if-cached") return;
 
-	// for everything else, try the network first, falling back to
-	// cache if the user is offline. (If the pages never change, you
-	// might prefer a cache-first approach to a network-first one.)
-	event.respondWith(
-		caches
-			.open(`offline${timestamp}`)
-			.then(async (cache) => {
-				try {
-					const response = await fetch(event.request);
-					cache.put(event.request, response.clone());
-					return response;
-				} catch (err) {
-					const response = await cache.match(event.request);
-					if (response) return response;
+    // for everything else, try the network first, falling back to
+    // cache if the user is offline. (If the pages never change, you
+    // might prefer a cache-first approach to a network-first one.)
+    event.respondWith(
+        caches
+            .open(`offline${timestamp}`)
+            .then(async (cache) => {
+                try {
+                    const response = await fetch(event.request);
+                    cache.put(event.request, response.clone());
+                    return response;
+                } catch (err) {
+                    const response = await cache.match(event.request);
+                    if (response) return response;
 
-					throw err;
-				}
-			}),
-	);
+                    throw err;
+                }
+            }),
+    );
 });

+ 6 - 0
web/src/utils/session.ts

@@ -8,3 +8,9 @@ export interface PageData {
     params: Record<string, unknown>;
     query: Record<string, unknown>;
 }
+
+export interface PreloadContext {
+    fetch: (url: string, opts?: any) => any;
+    redirect: (statusCode: number, location: string) => unknown;
+    error: (status: number, error: Error | string) => unknown;
+}

+ 1 - 0
web/svelte.config.js

@@ -7,6 +7,7 @@ module.exports = {
             plugins: [
                 require("postcss-import"),
                 require("tailwindcss"),
+                require("postcss-nested"),
                 require("autoprefixer"),
             ],
         },