diff --git a/package.json b/package.json index 198346e..efbe2e9 100644 --- a/package.json +++ b/package.json @@ -1,83 +1,93 @@ { - "name": "steamwar-website", - "type": "module", - "version": "0.0.1", - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro build", - "preview": "astro preview", - "astro": "astro", - "i18n:extract": "astro-i18n extract", - "i18n:generate:pages": "astro-i18n generate:pages --purge", - "i18n:generate:types": "astro-i18n generate:types", - "i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types", - "clean:dist": "rm -rf dist", - "clean:node_modules": "rm -rf node_modules", - "ci": "pnpm install && pnpm run i18n:sync && pnpm run build" - }, - "devDependencies": { - "@astrojs/svelte": "^7.0.4", - "@astrojs/tailwind": "^5.1.5", - "@astropub/icons": "^0.2.0", - "@internationalized/date": "^3.7.0", - "@lucide/svelte": "^0.488.0", - "@types/color": "^4.2.0", - "@types/node": "^22.9.3", - "@types/three": "^0.170.0", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "autoprefixer": "^10.4.20", - "bits-ui": "1.3.4", - "clsx": "^2.1.1", - "cmdk-sv": "^0.0.18", - "cssnano": "^7.0.6", - "embla-carousel-svelte": "^8.5.2", - "esbuild": "^0.24.0", - "eslint": "^9.15.0", - "eslint-plugin-astro": "^1.3.1", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-svelte": "^2.46.0", - "formsnap": "1.0.1", - "lucide-svelte": "^0.476.0", - "mode-watcher": "^0.5.1", - "paneforge": "^0.0.6", - "postcss-nesting": "^13.0.1", - "sass": "^1.81.0", - "svelte": "^5.16.0", - "svelte-sonner": "^0.3.28", - "tailwind-merge": "^2.5.5", - "tailwind-variants": "^0.3.1", - "tailwindcss": "^3.4.15", - "three": "^0.170.0", - "typescript": "^5.7.2", - "vaul-svelte": "^0.3.2", - "zod": "^3.23.8" - }, - "dependencies": { - "@astrojs/mdx": "^4.0.7", - "@astrojs/sitemap": "^3.2.1", - "@codemirror/commands": "^6.8.0", - "@codemirror/lang-json": "^6.0.1", - "@ddietr/codemirror-themes": "^1.4.4", - "@tanstack/table-core": "^8.21.2", - "astro": "^5.1.8", - "astro-i18n": "^2.2.4", - "astro-robots-txt": "^1.0.0", - "astro-seo": "^0.8.4", - "chart.js": "^4.4.6", - "chartjs-adapter-dayjs-4": "^1.0.4", - "chartjs-adapter-moment": "^1.0.1", - "color": "^4.2.3", - "dayjs": "^1.11.13", - "easymde": "^2.18.0", - "flowbite": "^2.5.2", - "flowbite-svelte": "^0.47.3", - "flowbite-svelte-icons": "^2.0.2", - "qs": "^6.13.1", - "sharp": "^0.33.5", - "svelte-awesome": "^3.3.5", - "svelte-codemirror-editor": "^1.4.1", - "svelte-spa-router": "^4.0.1" - } + "name": "steamwar-website", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro", + "i18n:extract": "astro-i18n extract", + "i18n:generate:pages": "astro-i18n generate:pages --purge", + "i18n:generate:types": "astro-i18n generate:types", + "i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types", + "clean:dist": "rm -rf dist", + "clean:node_modules": "rm -rf node_modules", + "ci": "pnpm install && pnpm run i18n:sync && pnpm run build" + }, + "devDependencies": { + "@astrojs/svelte": "^7.1.0", + "@astrojs/tailwind": "^5.1.5", + "@astropub/icons": "^0.2.0", + "@internationalized/date": "^3.8.1", + "@lucide/svelte": "^0.488.0", + "@types/color": "^4.2.0", + "@types/node": "^22.15.23", + "@types/three": "^0.170.0", + "@typescript-eslint/eslint-plugin": "^8.33.0", + "@typescript-eslint/parser": "^8.33.0", + "autoprefixer": "^10.4.21", + "bits-ui": "1.3.4", + "clsx": "^2.1.1", + "cmdk-sv": "^0.0.18", + "cssnano": "^7.0.7", + "embla-carousel-svelte": "^8.6.0", + "esbuild": "^0.24.2", + "eslint": "^9.27.0", + "eslint-plugin-astro": "^1.3.1", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-svelte": "^2.46.1", + "formsnap": "1.0.1", + "lucide-svelte": "^0.476.0", + "mode-watcher": "^0.5.1", + "paneforge": "^0.0.6", + "postcss-nesting": "^13.0.1", + "sass": "^1.89.0", + "svelte": "^5.33.4", + "svelte-sonner": "^0.3.28", + "tailwind-merge": "^2.6.0", + "tailwind-variants": "^0.3.1", + "tailwindcss": "^3.4.17", + "three": "^0.170.0", + "typescript": "^5.8.3", + "vaul-svelte": "^0.3.2", + "zod": "^3.25.31" + }, + "dependencies": { + "@astrojs/mdx": "^4.3.0", + "@astrojs/sitemap": "^3.4.0", + "@codemirror/commands": "^6.8.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/view": "^6.36.8", + "@ddietr/codemirror-themes": "^1.5.1", + "@tanstack/table-core": "^8.21.3", + "astro": "^5.8.0", + "astro-i18n": "^2.2.4", + "astro-robots-txt": "^1.0.0", + "astro-seo": "^0.8.4", + "chart.js": "^4.4.9", + "chartjs-adapter-dayjs-4": "^1.0.4", + "chartjs-adapter-moment": "^1.0.1", + "codemirror": "^6.0.1", + "color": "^4.2.3", + "dayjs": "^1.11.13", + "easymde": "^2.20.0", + "flowbite": "^2.5.2", + "flowbite-svelte": "^0.47.4", + "flowbite-svelte-icons": "^2.2.0", + "qs": "^6.14.0", + "sharp": "^0.33.5", + "svelte-awesome": "^3.3.5", + "svelte-spa-router": "^4.0.1" + }, + "pnpm": { + "ignoredBuiltDependencies": [ + "esbuild" + ], + "onlyBuiltDependencies": [ + "@parcel/watcher", + "sharp" + ] + } } diff --git a/src/components/FightTable.svelte b/src/components/FightTable.svelte index 8102329..fbbe51c 100644 --- a/src/components/FightTable.svelte +++ b/src/components/FightTable.svelte @@ -19,25 +19,27 @@ --> @@ -55,13 +57,15 @@ - {#each window(event.fights.filter(f => group === undefined ? true : f.group === group), rows) as fights} + {#each window( event.fights.filter((f) => (group === undefined ? true : f.group?.id === group)), rows ) as fights} {#each fights as fight (fight.id)} - {Intl.DateTimeFormat(astroI18n.locale, { - hour: "numeric", - minute: "numeric", - }).format(new Date(fight.start))} + {Intl.DateTimeFormat(astroI18n.locale, { + hour: "numeric", + minute: "numeric", + }).format(new Date(fight.start))} {fight.blueTeam.kuerzel} {fight.redTeam.kuerzel} {getWinner(fight)} @@ -70,4 +74,4 @@ {/each} - \ No newline at end of file + diff --git a/src/components/GroupTable.svelte b/src/components/GroupTable.svelte index 9b70167..9897329 100644 --- a/src/components/GroupTable.svelte +++ b/src/components/GroupTable.svelte @@ -19,33 +19,40 @@ -->
diff --git a/src/components/admin/pages/edit/Editor.svelte b/src/components/admin/pages/edit/Editor.svelte index ef3f5e0..a4fcda3 100644 --- a/src/components/admin/pages/edit/Editor.svelte +++ b/src/components/admin/pages/edit/Editor.svelte @@ -18,23 +18,22 @@ --> - { - if (dirty) { - return "You have unsaved changes. Are you sure you want to leave?"; - } -}}/> + + { + if (dirty) { + return "You have unsaved changes. Are you sure you want to leave?"; + } + }} +/> {#await pageFuture} - + {:then p}
{#snippet end()} - - - Delete - - - Save - - - {/snippet} + + Delete + Save + + {/snippet}
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")} - - {:else} - dirty = true}/> - {/if} + + {:else}{/if}
{:catch error}

{error.message}

-{/await} \ No newline at end of file +{/await} diff --git a/src/components/moderator/App.svelte b/src/components/moderator/App.svelte index 5da559e..4598e71 100644 --- a/src/components/moderator/App.svelte +++ b/src/components/moderator/App.svelte @@ -28,12 +28,14 @@ import Events from "@components/moderator/pages/events/Events.svelte"; import Dashboard from "@components/moderator/pages/dashboard/Dashboard.svelte"; import Event from "@components/moderator/pages/event/Event.svelte"; + import Pages from "@components/moderator/pages/pages/Pages.svelte"; const routes: RouteDefinition = { "/": Dashboard, "/events": Events, "/players": Players, "/event/:id": Event, + "/pages": Pages, }; @@ -48,7 +50,5 @@
-
- -
+ diff --git a/src/components/moderator/components/FightEdit.svelte b/src/components/moderator/components/FightEdit.svelte index 5a93d75..17a7330 100644 --- a/src/components/moderator/components/FightEdit.svelte +++ b/src/components/moderator/components/FightEdit.svelte @@ -37,7 +37,7 @@ let fightMap = $state(fight?.map); let fightBlueTeam = $state(fight?.blueTeam); let fightRedTeam = $state(fight?.redTeam); - let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : now("Europe/Berlin")); + let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : fromAbsolute(event.start, "Europe/Berlin")); let fightErgebnis = $state(fight?.ergebnis ?? 0); let fightSpectatePort = $state(fight?.spectatePort?.toString() ?? null); let fightGroup = $state(fight?.group?.id ?? null); @@ -139,7 +139,7 @@ - + No map found. diff --git a/src/components/moderator/pages/event/EventFightList.svelte b/src/components/moderator/pages/event/EventFightList.svelte index b21aa9b..ab99fd3 100644 --- a/src/components/moderator/pages/event/EventFightList.svelte +++ b/src/components/moderator/pages/event/EventFightList.svelte @@ -34,6 +34,8 @@ import GroupEditDialog from "./GroupEditDialog.svelte"; import GroupResultsDialog from "./GroupResultsDialog.svelte"; import type { ResponseGroups } from "@type/event"; + import { EditIcon, GroupIcon, LinkIcon } from "lucide-svelte"; + import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@components/ui/dropdown-menu"; let { data = $bindable() }: { data: ExtendedEvent } = $props(); @@ -209,7 +211,7 @@ {#each table.getRowModel().rows as groupRow (groupRow.id)} {#if groupRow.getIsGrouped()} - {@const group = groups.find((g) => g.id === groupRow.getValue("group"))} + {@const group = groups.find((g) => g.id == groupRow.getValue("group"))} {group?.name ?? "Keine Gruppe"} - + + + + + + + + + navigator.clipboard.writeText(` `)} + >Punkte Tabelle + navigator.clipboard.writeText(` `)} + >Kampf Tabelle + + + {#each groupRow.subRows as row (row.id)} diff --git a/src/components/moderator/pages/event/GroupResultsDialog.svelte b/src/components/moderator/pages/event/GroupResultsDialog.svelte index e4d3ee8..cf626f0 100644 --- a/src/components/moderator/pages/event/GroupResultsDialog.svelte +++ b/src/components/moderator/pages/event/GroupResultsDialog.svelte @@ -28,10 +28,10 @@ {#each Object.entries(group.points).toSorted((a, b) => b[1] - a[1]) as [teamIdString, points] (teamIdString)} {@const teamId = Number(teamIdString)} - {@const team = teams.find((t) => t.id === teamId) as ResponseTeam} - {@const playedGames = fights.filter((f) => f.start > Date.now() && f.group?.id === group.id && (f.blueTeam.id === teamId || f.redTeam.id === teamId)).length} + {@const team = teams.find((t) => t.id === teamId) as ResponseTeam | undefined} + {@const playedGames = fights.filter((f) => f.hasFinished && f.group?.id === group.id && (f.blueTeam.id === teamId || f.redTeam.id === teamId)).length} - {team.name} ({team.kuerzel}) + {team?.name ?? "?"} ({team?.kuerzel ?? "?"}) {playedGames} {points} diff --git a/src/components/moderator/pages/event/columns.ts b/src/components/moderator/pages/event/columns.ts index 48927c2..a3d0eb3 100644 --- a/src/components/moderator/pages/event/columns.ts +++ b/src/components/moderator/pages/event/columns.ts @@ -90,7 +90,7 @@ export const columns: ColumnDef = [ accessorKey: "ergebnis", cell: ({ row }) => { const fight = row.original; - if (fight.ergebnis === 0 && fight.start > Date.now()) { + if (!fight.hasFinished) { return "Noch nicht gespielt"; } else if (fight.ergebnis === 1) { return fight.blueTeam.name + " hat gewonnen"; diff --git a/src/components/moderator/pages/events/Events.svelte b/src/components/moderator/pages/events/Events.svelte index 2c24b27..17340ca 100644 --- a/src/components/moderator/pages/events/Events.svelte +++ b/src/components/moderator/pages/events/Events.svelte @@ -94,7 +94,7 @@ } -
+

Events

diff --git a/src/components/moderator/pages/pages/EditorWithTabs.svelte b/src/components/moderator/pages/pages/EditorWithTabs.svelte new file mode 100644 index 0000000..6495cbb --- /dev/null +++ b/src/components/moderator/pages/pages/EditorWithTabs.svelte @@ -0,0 +1,99 @@ + + +
+
+ {#each manager.pages as tab, index} + {@const isActive = manager.openPageIndex === index} + + {/each} +
+ +
+ + +
+
diff --git a/src/components/moderator/pages/pages/Pages.svelte b/src/components/moderator/pages/pages/Pages.svelte new file mode 100644 index 0000000..9f359ae --- /dev/null +++ b/src/components/moderator/pages/pages/Pages.svelte @@ -0,0 +1,27 @@ + + +
+ + +
+ {#await manager.pagesLoad} +

Loading pages...

+ {:then pages} + {#each Object.values(pages.dirs) as page} + + {/each} + {/await} +
+
+ + + + +
+
diff --git a/src/components/moderator/pages/pages/PagesList.svelte b/src/components/moderator/pages/pages/PagesList.svelte new file mode 100644 index 0000000..5a730fd --- /dev/null +++ b/src/components/moderator/pages/pages/PagesList.svelte @@ -0,0 +1,116 @@ + + + +
+ + +{#if open} +
+
+ {#if newPage} + + {/if} + {#each Object.values(page.dirs) as subPage (subPage.name)} + + {/each} + {#each Object.values(page.files) as file (file.id)} + + {/each} +
+
+{/if} diff --git a/src/components/moderator/pages/pages/page.svelte.ts b/src/components/moderator/pages/pages/page.svelte.ts new file mode 100644 index 0000000..dab6b51 --- /dev/null +++ b/src/components/moderator/pages/pages/page.svelte.ts @@ -0,0 +1,163 @@ +import { base64ToBytes } from "@components/admin/util"; +import { pageRepo } from "@components/repo/page"; +import type { ListPage, PageList } from "@components/types/page"; +import { get } from "svelte/store"; + +export class OpenEditPage { + public content: string = ""; + public frontmatter: { [key: string]: string } = $state({}); + public dirty: boolean = $state(false); + + public readonly fileType: string; + + public constructor( + private manager: PageManager, + public readonly pageId: number, + public readonly pageTitle: string, + public readonly sha: string, + public readonly originalContent: string, + private readonly path: string + ) { + this.fileType = this.path.split(".").pop() || "md"; + + this.content = this.removeFrontmatter(originalContent); + this.frontmatter = this.parseFrontmatter(originalContent); + } + + public focus(): boolean { + let index = this.manager.pages.indexOf(this); + + if (index === this.manager.openPageIndex) { + return true; + } + + this.manager.openPageIndex = this.manager.pages.indexOf(this); + return false; + } + + private parseFrontmatter(content: string): { [key: string]: string } { + const frontmatter: { [key: string]: string } = {}; + const lines = content.split("\n"); + + for (const line of lines) { + const match = line.match(/^\s*([a-zA-Z0-9_-]+):\s*(.*)$/); + if (match) { + frontmatter[match[1]] = match[2].trim(); + } + } + + return frontmatter; + } + + private removeFrontmatter(content: string): string { + const lines = content.split("\n"); + let inFrontmatter = false; + const result: string[] = []; + + for (const line of lines) { + if (line.trim() === "---") { + inFrontmatter = !inFrontmatter; + continue; + } + if (!inFrontmatter) { + result.push(line); + } + } + + return result.join("\n").trim(); + } +} + +export interface DirTree { + name: string; + dirs: { [key: string]: DirTree }; + files: { [key: string]: ListPage }; +} + +export class PageManager { + public branch: string = $state("master"); + public pages: OpenEditPage[] = $state([]); + + public openPageIndex: number = $state(-1); + public pagesLoad = $derived(get(pageRepo).listPages(this.branch).then(this.convertToTree)); + + public selectedPage = $derived(this.openPageIndex >= 0 ? this.pages[this.openPageIndex] : undefined); + + private convertToTree(pages: PageList): DirTree { + const tree: DirTree = { dirs: {}, files: {}, name: "/" }; + + pages.forEach((page) => { + const pathParts = page.path.split("/").filter((part) => part !== ""); + let current = tree; + + // Navigate/create directory structure + for (let i = 0; i < pathParts.length - 1; i++) { + const dir = pathParts[i]; + if (!current.dirs[dir]) { + current.dirs[dir] = { dirs: {}, files: {}, name: dir }; + } + current = current.dirs[dir]; + } + + // Add file to the final directory + const fileName = pathParts[pathParts.length - 1]; + current.files[fileName] = page; + }); + + return tree; + } + + public async openPage(pageId: number) { + const existingPage = this.existingPage(pageId); + if (existingPage) { + existingPage.focus(); + return; + } + + let r = await get(pageRepo).getPage(pageId, this.branch); + if (!r) { + return; + } + + const newPage = new OpenEditPage(this, pageId, r.name, r.sha, new TextDecoder().decode(base64ToBytes(r.content)), r.path); + this.pages.push(newPage); + newPage.focus(); + } + + public existingPage(pageId: number): OpenEditPage | undefined { + return this.pages.find((page) => page.pageId === pageId); + } + + public closePage(index: number) { + if (index < 0 || index >= this.pages.length) { + return; + } + + const page = this.pages[index]; + if (page.dirty) { + if (!confirm(`The page "${page.pageTitle}" has unsaved changes. Are you sure you want to close it?`)) { + return; + } + } + + this.pages.splice(index, 1); + if (this.openPageIndex >= index) { + this.openPageIndex = Math.max(0, this.openPageIndex - 1); + } + + if (this.openPageIndex < 0 && this.pages.length > 0) { + this.openPageIndex = 0; + } + + if (this.pages.length === 0) { + this.openPageIndex = -1; + } + } + + public async createPage(path: string, newPageName: string): Promise { + await get(pageRepo).createFile(path, this.branch, newPageName, newPageName); + this.branch = this.branch; + } +} + +export const manager = $state(new PageManager()); diff --git a/src/components/types/event.ts b/src/components/types/event.ts index 919c615..90db0cc 100644 --- a/src/components/types/event.ts +++ b/src/components/types/event.ts @@ -41,6 +41,7 @@ export const EventFightSchema = z.object({ ergebnis: z.number(), spectatePort: z.number().nullable(), group: ResponseGroupsSchema.nullable(), + hasFinished: z.boolean(), }); export type EventFight = z.infer; @@ -48,6 +49,7 @@ export type EventFight = z.infer; export const EventFightEditSchema = EventFightSchema.omit({ id: true, group: true, + hasFinished: true, }).extend({ group: z.number().nullable(), }); diff --git a/src/pages/ankuendigungen/[...slug].astro b/src/pages/ankuendigungen/[...slug].astro index 814a82b..eca0dcc 100644 --- a/src/pages/ankuendigungen/[...slug].astro +++ b/src/pages/ankuendigungen/[...slug].astro @@ -1,29 +1,29 @@ --- -import {astroI18n, createGetStaticPaths} from "astro-i18n"; -import {getCollection, CollectionEntry} from "astro:content"; +import { astroI18n, createGetStaticPaths } from "astro-i18n"; +import { getCollection, CollectionEntry } from "astro:content"; import PageLayout from "@layouts/PageLayout.astro"; -import {TagSolid, CalendarMonthSolid} from "flowbite-svelte-icons"; +import { TagSolid, CalendarMonthSolid } from "flowbite-svelte-icons"; import TagComponent from "@components/TagComponent.astro"; import LanguageWarning from "@components/LanguageWarning.astro"; -import {SEO} from "astro-seo"; +import { SEO } from "astro-seo"; import localBau from "@images/2022-03-28_13.18.25.png"; -import {getImage, Image} from "astro:assets"; +import { getImage, Image } from "astro:assets"; import "@styles/table.css"; export const getStaticPaths = createGetStaticPaths(async () => { - const posts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.locale); + const posts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.locale); - const germanPosts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.fallbackLocale); + const germanPosts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.fallbackLocale); - germanPosts.forEach(value => { - if (posts.find(post => post.data.key === value.data.key)) { + germanPosts.forEach((value) => { + if (posts.find((post) => post.data.key === value.data.key)) { return; } else { posts.push(value); } }); - return posts.map(value => ({ + return posts.map((value) => ({ params: { slug: value.slug.split("/").slice(1).join("/"), }, @@ -35,12 +35,12 @@ export const getStaticPaths = createGetStaticPaths(async () => { }); interface Props { - post: CollectionEntry<"announcements">, - german: boolean + post: CollectionEntry<"announcements">; + german: boolean; } -const {post, german} = Astro.props; -const {Content} = await post.render(); +const { post, german } = Astro.props; +const { Content } = await post.render(); const ogImage = await getImage({ src: post.data.image || localBau, @@ -52,64 +52,66 @@ const ogImage = await getImage({ -
- {post.data.image && ( -
- -
- )} + { + post.data.image && ( +
+ +
+ ) + }

{post.data.title}

- +
- {post.data.tags.map(tag => ( - - ))} + {post.data.tags.map((tag) => )}
- - {Intl.DateTimeFormat(astroI18n.locale, { - day: "numeric", - month: "short", - year: "numeric", - }).format(post.data.created)} - {post.data.author && ( - - {post.data.author} - {post.data.author} - - )} + + { + Intl.DateTimeFormat(astroI18n.locale, { + day: "numeric", + month: "short", + year: "numeric", + }).format(post.data.created) + } + { + post.data.author && ( + + {post.data.author} + {post.data.author} + + ) + }
- {german && ( - - )} - + {german && } +