diff --git a/package.json b/package.json index 18c8d45..8b96e23 100644 --- a/package.json +++ b/package.json @@ -28,16 +28,17 @@ "tailwindcss": "^3.3.2" }, "dependencies": { + "@codemirror/lang-markdown": "^6.2.2", + "@ddietr/codemirror-themes": "^1.4.2", "astro": "^3.1.1", "color": "^4.2.3", - "crypto-js": "^4.1.1", "flowbite": "^1.7.0", "flowbite-svelte": "^0.39.2", "flowbite-svelte-icons": "^0.2.5", - "gitea-js": "^1.20.1", "moment": "^2.29.4", "sharp": "^0.32.6", "svelte-awesome": "^3.2.0", + "svelte-codemirror-editor": "^1.1.0", "svelte-spa-router": "^3.3.0", "zod": "^3.21.4" } diff --git a/public/favicon.svg b/public/favicon.svg deleted file mode 100644 index f157bd1..0000000 --- a/public/favicon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/src/components/admin/App.svelte b/src/components/admin/App.svelte index e54ea7d..f1887f0 100644 --- a/src/components/admin/App.svelte +++ b/src/components/admin/App.svelte @@ -11,6 +11,7 @@ '/login': wrap({asyncComponent: () => import('./pages/Login.svelte'), conditions: detail => get(tokenStore) == ""}), '/event/:id': wrap({asyncComponent: () => import('./pages/Event.svelte'), conditions: detail => get(tokenStore) != ""}), '/event/:id/generate': wrap({asyncComponent: () => import('./pages/Generate.svelte'), conditions: detail => get(tokenStore) != ""}), + '/edit': wrap({asyncComponent: () => import('./pages/Edit.svelte'), conditions: detail => get(tokenStore) != ""}), '*': wrap({asyncComponent: () => import('./pages/NotFound.svelte')}) } diff --git a/src/components/admin/components/TypeAheadSearch.svelte b/src/components/admin/components/TypeAheadSearch.svelte index c08a615..31b5fea 100644 --- a/src/components/admin/components/TypeAheadSearch.svelte +++ b/src/components/admin/components/TypeAheadSearch.svelte @@ -1,14 +1,13 @@ +
+ + + + + Edit Pages + + + +
+
+ + {#await pagesFuture} + + {:then pages} +
+ +
+ + +
+
+ {@const pagesMap = mapToMap(pages)} + {#each pagesMap as [key, value]} +
+ {key} +
    + {#each value as page} + {@const match = nameRegex.exec(page.path) ? nameRegex.exec(page.path)[0] : ""} + {@const startIndex = page.path.indexOf(match)} + {@const endIndex = startIndex + match.length} +
  • selected = page.id}> + {page.path.substring(0, startIndex)}{match}{page.path.substring(endIndex, page.path.length)} +
  • + {/each} +
+
+ {/each} + {:catch error} +

{error.message}

+ {/await} +
+ + {#if selected} + + {/if} + +
+
+
\ No newline at end of file diff --git a/src/components/admin/pages/Home.svelte b/src/components/admin/pages/Home.svelte index fbe8ab8..27576f9 100644 --- a/src/components/admin/pages/Home.svelte +++ b/src/components/admin/pages/Home.svelte @@ -21,11 +21,12 @@ - Eventplanner + Admin-Tool + Edit Pages Permissions showLogoutModal = true} class="cursor-pointer select-none">Logout diff --git a/src/components/admin/pages/Login.svelte b/src/components/admin/pages/Login.svelte index 34db74b..3b3104e 100644 --- a/src/components/admin/pages/Login.svelte +++ b/src/components/admin/pages/Login.svelte @@ -3,7 +3,7 @@ import {fly} from "svelte/transition"; import {replace} from "svelte-spa-router"; import {EyeOutline, EyeSlashOutline, EyeSolid} from "flowbite-svelte-icons"; - import {tokenStore} from "../repo/repo.js"; + import {fetchWithToken, tokenStore} from "../repo/repo.js"; let show = false; let loading = false; @@ -12,7 +12,7 @@ async function handleSubmit() { loading = true; - let res = await fetch("https://steamwar.de/eventplanner-api/data", {headers: {"X-SW-Auth": value}}) + let res = await fetchWithToken(value, "/data") loading = false; if(res.ok) { $tokenStore = value; diff --git a/src/components/admin/pages/edit/Editor.svelte b/src/components/admin/pages/edit/Editor.svelte new file mode 100644 index 0000000..142ea35 --- /dev/null +++ b/src/components/admin/pages/edit/Editor.svelte @@ -0,0 +1,50 @@ + + +{#await pageFuture} + +{:then p} +
+
+ + + + Save + + + +
+ +
+{:catch error} +

{error.message}

+{/await} \ No newline at end of file diff --git a/src/components/admin/repo/page.ts b/src/components/admin/repo/page.ts new file mode 100644 index 0000000..a4c5580 --- /dev/null +++ b/src/components/admin/repo/page.ts @@ -0,0 +1,44 @@ +import type {Page, PageList} from "../types/page.ts"; +import {fetchWithToken} from "./repo.ts"; +import {PageListSchema, PageSchema} from "../types/page.ts"; +import {bytesToBase64} from "../util.ts"; +import {branches} from "../stores/stores.ts"; + +export class PageRepo { + constructor(private token: string) {} + + public async listPages(branch: string = "master"): Promise { + return await fetchWithToken(this.token, `/page?branch=${branch}`) + .then(value => value.json()) + .then(value => PageListSchema.parse(value)) + } + + public async getPage(id: number, branch: string = "master"): Promise { + return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`) + .then(value => value.json()) + .then(value => PageSchema.parse(value)) + } + + public async updatePage(id: number, content: string, sha: string, message: string, branch: string = "master"): Promise { + await fetchWithToken(this.token, `/page/${id}?branch=${branch}`, { + method: "PUT", + body: JSON.stringify({ + content: bytesToBase64(new TextEncoder().encode(content)), + sha, message + }) + }) + } + + public async getBranches(): Promise { + return await fetchWithToken(this.token, "/page/branch") + .then(value => value.json()) + } + + public async createBranch(branch: string): Promise { + await fetchWithToken(this.token, `/page/branch`, {method: "POST", body: JSON.stringify({branch})}) + } + + public async deleteBranch(branch: string): Promise { + await fetchWithToken(this.token, `/page/branch`, {method: "DELETE", body: JSON.stringify({branch})}) + } +} \ No newline at end of file diff --git a/src/components/admin/repo/repo.ts b/src/components/admin/repo/repo.ts index 2dbf045..f497f3c 100644 --- a/src/components/admin/repo/repo.ts +++ b/src/components/admin/repo/repo.ts @@ -2,14 +2,17 @@ import {derived, writable} from "svelte/store"; import {EventRepo} from "./event.js"; import {FightRepo} from "./fight.js"; import {PermsRepo} from "./perms.js"; +import {PageRepo} from "./page.ts"; export { EventRepo } from "./event.js" -export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => fetch(`https://steamwar.de/eventplanner-api${url}`, {...params, headers: {"X-SW-Auth": token, "Content-Type": "application/json", ...params.headers}}); +export const apiUrl = import.meta.env.PUBLIC_API_SERVER; +export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => fetch(`${apiUrl}${url}`, {...params, headers: {"Authorization": "Bearer " + (token), "Content-Type": "application/json", ...params.headers}}); export const tokenStore = writable(localStorage.getItem("sw-api-token") ?? "") tokenStore.subscribe((value) => localStorage.setItem("sw-api-token", value)) export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token)) export const fightRepo = derived(tokenStore, ($token) => new FightRepo($token)) export const permsRepo = derived(tokenStore, ($token) => new PermsRepo($token)) +export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token)) diff --git a/src/components/admin/stores/cached.ts b/src/components/admin/stores/cached.ts index ba88b26..4eca3c9 100644 --- a/src/components/admin/stores/cached.ts +++ b/src/components/admin/stores/cached.ts @@ -29,7 +29,7 @@ export function cached(normal: T, init: () => Promise): Cached { }; } -export function cachedFamily(normal: K, init: (T) => Promise): (T) => Cached { +export function cachedFamily(normal: K, init: (arg0: T) => Promise): (arg: T) => Cached { const stores: Map> = new Map(); return (arg: T) => { if(stores.has(arg)) { diff --git a/src/components/admin/stores/stores.ts b/src/components/admin/stores/stores.ts index 100a837..eb6a862 100644 --- a/src/components/admin/stores/stores.ts +++ b/src/components/admin/stores/stores.ts @@ -4,28 +4,27 @@ import {cached, cachedFamily} from "./cached.js"; import type {Team} from "../types/team.js"; import {TeamSchema} from "../types/team.js"; import {get, writable} from "svelte/store"; -import {permsRepo, tokenStore} from "../repo/repo.js"; +import {apiUrl, fetchWithToken, pageRepo, permsRepo, tokenStore} from "../repo/repo.js"; import {z} from "zod"; -export const schemTypes = cached([], () => { - return fetch("https://steamwar.de/eventplanner-api/data/schematicTypes", {headers: {"X-SW-Auth": get(tokenStore)}}) - .then(res => res.json()) -}) +export const schemTypes = cached([], () => + fetchWithToken(get(tokenStore), `/data/schematicTypes`) + .then(res => res.json())) export const players = cached([], async () => { - const res = await fetch("https://steamwar.de/eventplanner-api/data/users", {headers: {"X-SW-Auth": get(tokenStore)}}); + const res = await fetchWithToken(get(tokenStore), `/data/users`); return z.array(PlayerSchema).parse(await res.json()); }) export const gamemodes = cached([], async () => { - const res = await fetch("https://steamwar.de/eventplanner-api/data/gamemodes", {headers: {"X-SW-Auth": get(tokenStore)}}); + const res = await fetchWithToken(get(tokenStore), `/data/gamemodes`); return z.array(z.string()).parse(await res.json()); }) export const maps = cachedFamily([], async (gamemode) => { if (get(gamemodes).every(value => value !== gamemode)) return []; - const res = await fetch(`https://steamwar.de/eventplanner-api/data/gamemodes/${gamemode}/maps`, {headers: {"X-SW-Auth": get(tokenStore)}}); + const res = await fetchWithToken(get(tokenStore), `/data/gamemodes/${gamemode}/maps`); if (!res.ok) { return []; } else { @@ -34,14 +33,19 @@ export const maps = cachedFamily([], async (gamemode) => { }) export const groups = cached([], async () => { - const res = await fetch("https://steamwar.de/eventplanner-api/data/groups", {headers: {"X-SW-Auth": get(tokenStore)}}); + const res = await fetchWithToken(get(tokenStore), `/data/groups`); return z.array(z.string()).parse(await res.json()); }) export const teams = cached([], async () => { - const res = await fetch("https://steamwar.de/eventplanner-api/team", {headers: {"X-SW-Auth": get(tokenStore)}}); + const res = await fetchWithToken(get(tokenStore), `/team`); return z.array(TeamSchema).parse(await res.json()); }) +export const branches = cached([], async () => { + const res = await get(pageRepo).getBranches(); + return z.array(z.string()).parse(res); +}) + export const isWide = writable(window.innerWidth >= 640); window.addEventListener("resize", () => isWide.set(window.innerWidth >= 640)); diff --git a/src/components/admin/types/page.ts b/src/components/admin/types/page.ts new file mode 100644 index 0000000..4c7992a --- /dev/null +++ b/src/components/admin/types/page.ts @@ -0,0 +1,27 @@ +import {z} from "zod"; + +export const ListPageSchema = z.object({ + path: z.string(), + name: z.string(), + sha: z.string(), + downloadUrl: z.string().url(), + id: z.number().positive() +}); + +export type ListPage = z.infer; + +export const PageListSchema = z.array(ListPageSchema) + +export type PageList = z.infer; + +export const PageSchema = z.object({ + path: z.string(), + name: z.string(), + sha: z.string(), + downloadUrl: z.string().url(), + content: z.string(), + size: z.number().gte(0), + id: z.number().positive() +}) + +export type Page = z.infer; \ No newline at end of file diff --git a/src/components/admin/util.ts b/src/components/admin/util.ts index d5cfc1e..31056e1 100644 --- a/src/components/admin/util.ts +++ b/src/components/admin/util.ts @@ -1,8 +1,23 @@ import Color from "color"; import type {Team} from "./types/team.js"; +import type {ListPage, PageList} from "./types/page.ts"; export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); +export const nameRegex = new RegExp("(?!.*\/).+(?=\\.md)"); + +export function mapToMap(pages: PageList): Map { + const map = new Map(); + for (const page of pages) { + let folder = page.path.substring(0, page.path.indexOf(nameRegex.exec(page.path)[0])); + if (!map.has(folder)) { + map.set(folder, []); + } + map.get(folder).push(page); + } + return map; +} + export function colorFromTeam(team: Team): string { switch (team.color) { case "1": @@ -47,3 +62,14 @@ export function lighten(color: string) { export function brightness(color: string) { return Color(color).isLight() } + +export function base64ToBytes(base64: string) { + const binString = atob(base64); + // @ts-ignore + return Uint8Array.from(binString, (m) => m.codePointAt(0)); +} + +export function bytesToBase64(bytes: Uint8Array) { + const binString = String.fromCodePoint(...bytes); + return btoa(binString); +} diff --git a/src/layouts/Basic.astro b/src/layouts/Basic.astro index 5d4a7b6..6f77abe 100644 --- a/src/layouts/Basic.astro +++ b/src/layouts/Basic.astro @@ -1,7 +1,11 @@ --- import {astroI18n} from "astro-i18n"; +import icon from '../images/logo.png'; +import {getImage} from "astro:assets"; const { title, description } = Astro.props.frontmatter || Astro.props; + +const iconImage = await getImage({src: icon, height: 32, width: 32, format: 'png', quality: 100}); --- @@ -11,6 +15,7 @@ const { title, description } = Astro.props.frontmatter || Astro.props; content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> + {title} diff --git a/src/layouts/NavbarLayout.astro b/src/layouts/NavbarLayout.astro index 77f6c5b..1b0912c 100644 --- a/src/layouts/NavbarLayout.astro +++ b/src/layouts/NavbarLayout.astro @@ -4,6 +4,7 @@ import { Image } from 'astro:assets'; import '../styles/button.css'; import localLogo from "../images/logo.png" import {l, t} from "astro-i18n"; +import {YoutubeSolid} from "flowbite-svelte-icons" const { title } = Astro.props; --- @@ -13,7 +14,7 @@ const { title } = Astro.props;
+ before:bg-black before:absolute before:top-0 before:left-0 before:bottom-0 before:right-0 before:-z-10 before:scale-y-0 before:transition-transform before:origin-top">
- © SteamWar.de + © SteamWar.de - {new Date().getFullYear()}
diff --git a/src/pages/index.astro b/src/pages/index.astro index db163ce..5123da5 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,7 +2,7 @@ import NavbarLayout from "../layouts/NavbarLayout.astro"; import { Image } from "astro:assets"; -import localBau from "../images/2022-03-28_13.18.25.png"; +import localBau from "../images/bau.jpg"; import {l, t} from "astro-i18n"; import {CaretRight, Archive, Rocket, Bell} from "@astropub/icons" --- @@ -121,8 +121,9 @@ import {CaretRight, Archive, Rocket, Bell} from "@astropub/icons" } .card { - @apply w-72 border-2 bg-zinc-50 border-gray-100 flex flex-col items-center p-8 m-4 rounded-xl shadow-lg - dark:bg-zinc-900 dark:border-gray-800 dark:text-gray-100; + @apply w-72 border-2 bg-zinc-50 border-gray-100 flex flex-col items-center p-8 m-4 rounded-xl shadow-lg transition-transform duration-300 ease-in-out + dark:bg-zinc-900 dark:border-gray-800 dark:text-gray-100 + hover:scale-105; >h1 { @apply text-xl font-bold underline mt-4; }