Updates
This commit is contained in:
@@ -22,11 +22,12 @@
|
||||
|
||||
export let selected: string | null = null
|
||||
export let items: {name: string, value: string}[] = []
|
||||
export let maxItems = 5;
|
||||
|
||||
export let searchValue = items.find(item => item.value === selected)?.name || ''
|
||||
let open = false
|
||||
|
||||
$: filteredItems = items.filter(item => item.name.toLowerCase().includes(searchValue.toLowerCase())).filter((value, index) => index < 5)
|
||||
$: filteredItems = items.filter(item => item.name.toLowerCase().includes(searchValue.toLowerCase())).filter((value, index) => index < maxItems)
|
||||
|
||||
function selectItem(item: {name: string, value: string}) {
|
||||
selected = item.value
|
||||
|
||||
@@ -18,83 +18,92 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {ArrowLeftSolid} from "flowbite-svelte-icons";
|
||||
import {Button, Card, Navbar, NavBrand, Spinner} from "flowbite-svelte";
|
||||
import {mapToMap, nameRegex} from "../util.ts";
|
||||
import TypeAheadSearch from "../components/TypeAheadSearch.svelte";
|
||||
import {branches} from "@stores/stores.ts";
|
||||
import Editor from "./edit/Editor.svelte";
|
||||
import {pageRepo} from "@repo/page.ts";
|
||||
import {ArrowLeftSolid} from "flowbite-svelte-icons";
|
||||
import {Button, Card, Navbar, NavBrand, Spinner} from "flowbite-svelte";
|
||||
import {mapToMap, nameRegex} from "../util.ts";
|
||||
import TypeAheadSearch from "../components/TypeAheadSearch.svelte";
|
||||
import {branches} from "@stores/stores.ts";
|
||||
import Editor from "./edit/Editor.svelte";
|
||||
import {pageRepo} from "@repo/page.ts";
|
||||
|
||||
$: pagesFuture = $pageRepo.listPages(selectedBranch);
|
||||
$: pagesFuture = $pageRepo.listPages(selectedBranch);
|
||||
|
||||
let selected: number | null = null;
|
||||
let selected: number | null = null;
|
||||
|
||||
let selectedBranch: string = "master";
|
||||
let searchValue: string = "";
|
||||
let selectedBranch: string = "master";
|
||||
let searchValue: string = "";
|
||||
let dirty = false;
|
||||
|
||||
$: availableBranches = $branches.map((branch) => ({
|
||||
name: branch,
|
||||
value: branch
|
||||
}))
|
||||
let selectedPath: string = "";
|
||||
let pathSearchValue: string = "";
|
||||
|
||||
async function createBranch() {
|
||||
const name = prompt("Branch name:")
|
||||
if (name) {
|
||||
selected = null
|
||||
await $pageRepo.createBranch(name)
|
||||
let inter = setInterval(() => {
|
||||
branches.reload()
|
||||
if ($branches.includes(name)) {
|
||||
selectedBranch = name
|
||||
searchValue = ""
|
||||
clearInterval(inter)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
$: availableBranches = $branches.map((branch) => ({
|
||||
name: branch,
|
||||
value: branch
|
||||
}))
|
||||
|
||||
async function deleteBranch(con: boolean) {
|
||||
if (selectedBranch !== "master") {
|
||||
let conf = con || confirm("Are you sure you want to delete this branch?")
|
||||
if(conf) {
|
||||
await $pageRepo.deleteBranch(selectedBranch)
|
||||
async function createBranch() {
|
||||
const name = prompt("Branch name:")
|
||||
if (name) {
|
||||
selected = null
|
||||
await $pageRepo.createBranch(name)
|
||||
let inter = setInterval(() => {
|
||||
branches.reload()
|
||||
if (!$branches.includes(selectedBranch)) {
|
||||
selectedBranch = "master"
|
||||
if ($branches.includes(name)) {
|
||||
selectedBranch = name
|
||||
searchValue = ""
|
||||
clearInterval(inter)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
} else {
|
||||
alert("You can't delete the master branch")
|
||||
}
|
||||
}
|
||||
|
||||
async function createFile() {
|
||||
let name = prompt("File name:", "pages/en/[Name]")
|
||||
if (name) {
|
||||
await $pageRepo.createFile(`${name}`, selectedBranch)
|
||||
reload()
|
||||
function changePage(id: number) {
|
||||
if (dirty) {
|
||||
if (confirm("You have unsaved changes. Are you sure you want to change the page?")) {
|
||||
selected = id
|
||||
dirty = false
|
||||
}
|
||||
} else {
|
||||
selected = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reload() {
|
||||
const w = selectedBranch
|
||||
selectedBranch = "###!"
|
||||
selectedBranch = w
|
||||
}
|
||||
async function deleteBranch(con: boolean) {
|
||||
if (selectedBranch !== "master") {
|
||||
let conf = con || confirm("Are you sure you want to delete this branch?")
|
||||
if(conf) {
|
||||
await $pageRepo.deleteBranch(selectedBranch)
|
||||
let inter = setInterval(() => {
|
||||
branches.reload()
|
||||
if (!$branches.includes(selectedBranch)) {
|
||||
selectedBranch = "master"
|
||||
searchValue = ""
|
||||
clearInterval(inter)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
} else {
|
||||
alert("You can't delete the master branch")
|
||||
}
|
||||
}
|
||||
|
||||
async function mergeBranch() {
|
||||
await $pageRepo.merge(selectedBranch, `Go live of ${selectedBranch}`)
|
||||
await deleteBranch(true)
|
||||
}
|
||||
async function createFile() {
|
||||
let name = prompt("File name:", "[Name].md")
|
||||
if (name) {
|
||||
await $pageRepo.createFile(`${selectedPath}${name}`, selectedBranch)
|
||||
reload()
|
||||
}
|
||||
}
|
||||
|
||||
function reload() {
|
||||
const w = selectedBranch
|
||||
selectedBranch = "###!"
|
||||
selectedBranch = w
|
||||
}
|
||||
</script>
|
||||
<div class="flex flex-col h-screen overflow-scroll">
|
||||
<Navbar let:hidden let:toggle>
|
||||
<Navbar>
|
||||
<NavBrand href="#">
|
||||
<ArrowLeftSolid></ArrowLeftSolid>
|
||||
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
|
||||
@@ -108,44 +117,42 @@ async function mergeBranch() {
|
||||
{#await pagesFuture}
|
||||
<Spinner />
|
||||
{:then pages}
|
||||
{@const pagesMap = mapToMap(pages)}
|
||||
<div class="border-b border-b-gray-600 pb-2 flex justify-between">
|
||||
<TypeAheadSearch items={availableBranches} bind:selected={selectedBranch} bind:searchValue />
|
||||
<div>
|
||||
<TypeAheadSearch items={availableBranches} bind:selected={selectedBranch} bind:searchValue />
|
||||
<TypeAheadSearch items={Array.from(pagesMap.keys()).map(value => ({value, name: value}))} bind:selected={selectedPath} bind:searchValue={pathSearchValue} maxItems={Number.MAX_VALUE} />
|
||||
</div>
|
||||
<div>
|
||||
{#if selectedBranch !== "master"}
|
||||
<Button on:click={mergeBranch}>Merge Branch</Button>
|
||||
<Button on:click={createFile} color="alternative" disabled={!selectedPath}>Create File</Button>
|
||||
<Button on:click={() => deleteBranch(false)} color="none">Delete Branch</Button>
|
||||
{:else}
|
||||
<Button on:click={createBranch}>Create Branch</Button>
|
||||
{/if}
|
||||
<Button on:click={createFile} color="alternative">Create File</Button>
|
||||
{#if selectedBranch !== "master"}
|
||||
<Button on:click={() => deleteBranch(false)} color="none">Delete Branch</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{@const pagesMap = mapToMap(pages)}
|
||||
{#each pagesMap as [key, value]}
|
||||
<details>
|
||||
<summary class="p-4 transition-colors hover:bg-gray-700 cursor-pointer">{key}</summary>
|
||||
<ul>
|
||||
{#each value as page}
|
||||
{@const nameRegexExec = nameRegex.exec(page.path)}
|
||||
{@const match = nameRegexExec ? nameRegexExec[0] : ""}
|
||||
{@const startIndex = page.path.indexOf(match)}
|
||||
{@const endIndex = startIndex + match.length}
|
||||
<li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer" on:click|preventDefault={() => selected = page.id}>
|
||||
<span class:text-orange-600={selected === page.id}>{page.path.substring(0, startIndex)}</span><span class="text-white" class:!text-orange-500={selected === page.id}>{match}</span><span class:text-orange-600={selected === page.id}>{page.path.substring(endIndex, page.path.length)}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
{/each}
|
||||
<ul>
|
||||
{#if (selectedPath)}
|
||||
{@const value = pagesMap.get(selectedPath) || []}
|
||||
{#each value as page}
|
||||
{@const nameRegexExec = nameRegex.exec(page.path)}
|
||||
{@const match = nameRegexExec ? nameRegexExec[0] : ""}
|
||||
{@const startIndex = page.path.indexOf(match)}
|
||||
{@const endIndex = startIndex + match.length}
|
||||
<li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer" on:click|preventDefault={() => changePage(page.id)}>
|
||||
<span class:text-orange-600={selected === page.id}>{page.path.substring(0, startIndex)}</span><span class="text-white" class:!text-orange-500={selected === page.id}>{match}</span><span class:text-orange-600={selected === page.id}>{page.path.substring(endIndex, page.path.length)}</span>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
{:catch error}
|
||||
<p>{error.message}</p>
|
||||
{/await}
|
||||
</Card>
|
||||
<Card class="!max-w-full" style="grid-column: 2/4">
|
||||
{#if selected}
|
||||
<Editor pageId={selected} branch={selectedBranch} on:reload={reload} />
|
||||
<Editor pageId={selected} branch={selectedBranch} on:reload={reload} bind:dirty />
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
export let pageId: number;
|
||||
export let branch: string;
|
||||
export let dirty: boolean = false;
|
||||
|
||||
let dispatcher = createEventDispatcher();
|
||||
|
||||
@@ -47,6 +48,7 @@
|
||||
let message = window.prompt("Commit message:", "Update " + page!.name)
|
||||
if (message) {
|
||||
$pageRepo.updatePage(pageId, pageContent, page!.sha, message, branch)
|
||||
dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +56,16 @@
|
||||
let message = window.prompt("Commit message:", "Delete " + page!.name)
|
||||
if (message) {
|
||||
await $pageRepo.deletePage(pageId, message, page!.sha, branch)
|
||||
dirty = false;
|
||||
dispatcher("reload")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:beforeunload={() => {
|
||||
if (dirty) {
|
||||
return "You have unsaved changes. Are you sure you want to leave?";
|
||||
}
|
||||
}} />
|
||||
{#await pageFuture}
|
||||
<Spinner />
|
||||
{:then p}
|
||||
@@ -76,9 +83,9 @@
|
||||
</Toolbar>
|
||||
</div>
|
||||
{#if page?.name.endsWith("md")}
|
||||
<MDEMarkdownEditor bind:value={pageContent} />
|
||||
<MDEMarkdownEditor bind:value={pageContent} bind:dirty />
|
||||
{:else}
|
||||
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} />
|
||||
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} on:change={() => dirty = true} />
|
||||
{/if}
|
||||
</div>
|
||||
{:catch error}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
import "easymde/dist/easymde.min.css"
|
||||
|
||||
export let value: string;
|
||||
export let dirty: boolean = false;
|
||||
|
||||
let editor: HTMLTextAreaElement;
|
||||
let mde: EasyMDE;
|
||||
|
||||
@@ -34,8 +36,10 @@
|
||||
})
|
||||
mde.codemirror.on("change", () => {
|
||||
value = mde.value();
|
||||
dirty = true;
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
mde.toTextArea();
|
||||
mde.cleanup();
|
||||
|
||||
@@ -19,10 +19,11 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import {Modal, Spinner} from "flowbite-svelte";
|
||||
import {Spinner} from "flowbite-svelte";
|
||||
import SchematicInfoModal from "./SchematicInfoModal.svelte";
|
||||
import type {Player} from "@type/data.ts";
|
||||
import {schemRepo} from "@repo/schem.ts";
|
||||
import SWModal from "@components/styled/SWModal.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -37,14 +38,14 @@
|
||||
</script>
|
||||
|
||||
{#await schemInfo}
|
||||
<Modal title="Loading" open on:close={() => dispatch("reset")}>
|
||||
<SWModal title="Loading" open on:close={() => dispatch("reset")}>
|
||||
<Spinner />
|
||||
</Modal>
|
||||
</SWModal>
|
||||
{:then info}
|
||||
<SchematicInfoModal {user} {info} on:reset />
|
||||
{:catch e}
|
||||
<Modal title="Error" open on:close={() => dispatch("reset")}>
|
||||
<SWModal title="Error" open on:close={() => dispatch("reset")}>
|
||||
<p>{e.message}</p>
|
||||
<button class="btn !ml-auto" slot="footer" on:click={() => dispatch("reset")}>Close</button>
|
||||
</Modal>
|
||||
</SWModal>
|
||||
{/await}
|
||||
@@ -20,12 +20,12 @@
|
||||
<script lang="ts">
|
||||
import {astroI18n, t} from "astro-i18n";
|
||||
import {CheckSolid, XCircleOutline} from "flowbite-svelte-icons";
|
||||
import {Modal} from "flowbite-svelte";
|
||||
import type {SchematicInfo} from "@type/schem.ts";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import type {Player} from "@type/data.ts";
|
||||
import dayjs from "dayjs";
|
||||
import {schemRepo} from "@repo/schem.ts";
|
||||
import SWModal from "@components/styled/SWModal.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title={info.schem.name} autoclose open on:close={() => dispatch("reset")}>
|
||||
<SWModal title={info.schem.name} open on:close={() => dispatch("reset")}>
|
||||
<p>{t("dashboard.schematic.info.path", {path: info.path})}</p>
|
||||
<p class="flex !mt-0">
|
||||
{t("dashboard.schematic.info.replaceColor")}
|
||||
@@ -76,4 +76,4 @@
|
||||
{/if}
|
||||
<button class="btn" class:!ml-auto={info.schem.owner !== user.id} on:click={() => dispatch("reset")}>{t("dashboard.schematic.info.btn.close")}</button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</SWModal>
|
||||
@@ -150,7 +150,7 @@
|
||||
<div class="w-full flex justify-center mt-4">
|
||||
<ul class="inline-flex">
|
||||
<li>
|
||||
<button on:click={() => page = 0} class="btn btn-gray h-8 px-3 text-sm flex items-center !m-0 !rounded-l-none">
|
||||
<button on:click={() => page = 0} class="btn btn-gray h-8 px-3 text-sm flex items-center !m-0 !rounded-r-none">
|
||||
<span class="sr-only">Next</span>
|
||||
<ChevronDoubleLeftOutline class="w-3 h-3" />
|
||||
</button>
|
||||
|
||||
@@ -34,14 +34,13 @@
|
||||
{#await request}
|
||||
<p>{t("status.loading")}</p>
|
||||
{:then data}
|
||||
<p>Playtime: {new Intl.NumberFormat(astroI18n.locale, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(data.playtime)}h</p>
|
||||
<p>Fights: {data.fights}</p>
|
||||
<p>{t("dashboard.stats.playtime", {playtime: new Intl.NumberFormat(astroI18n.locale, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(data.playtime)})}</p>
|
||||
<p>{t("dashboard.stats.fights", {fights: data.fights})}</p>
|
||||
{#if user.perms.includes("CHECK")}
|
||||
<p>Schematics Checked: {data.acceptedSchematics}</p>
|
||||
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
|
||||
{/if}
|
||||
{:catch error}
|
||||
<p>error: {error}</p>
|
||||
{/await}
|
||||
@@ -18,9 +18,10 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {Modal} from "flowbite-svelte";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import {schemRepo} from "@repo/schem.ts";
|
||||
import SWModal from "@components/styled/SWModal.svelte";
|
||||
import {t} from "astro-i18n"
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -55,12 +56,12 @@
|
||||
let uploadFile: FileList | null = null;
|
||||
</script>
|
||||
|
||||
<Modal title="Upload Schematic" bind:open autoclose outsideclose>
|
||||
<SWModal title={t("dashboard.schematic.title")} bind:open>
|
||||
<form>
|
||||
<input type="file" bind:files={uploadFile} />
|
||||
</form>
|
||||
<svelte:fragment slot="footer">
|
||||
<button class="btn !ml-auto" on:click={upload}>Upload</button>
|
||||
<button class="btn btn-gray" on:click={() => open = false}>Close</button>
|
||||
<button class="btn !ml-auto" on:click={upload}>{t("dashboard.schematic.upload")}</button>
|
||||
<button class="btn btn-gray" on:click={() => open = false}>{t("dashboard.schematic.cancel")}</button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
</SWModal>
|
||||
@@ -24,6 +24,8 @@
|
||||
import Statistics from "./Statistics.svelte";
|
||||
import {authRepo} from "@repo/auth.ts";
|
||||
import {tokenStore} from "@repo/repo.ts";
|
||||
import SWModal from "@components/styled/SWModal.svelte";
|
||||
import SWButton from "@components/styled/SWButton.svelte";
|
||||
|
||||
export let user: Player;
|
||||
|
||||
@@ -33,6 +35,7 @@
|
||||
window.location.href = l("/login")
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex mb-4 flex-col md:flex-row">
|
||||
<div>
|
||||
<div class="bg-zinc-50 border-gray-100 py-24 px-12 border-2 m-2 transition duration-300 ease-in-out rounded-xl shadow-lg hidden md:block
|
||||
|
||||
88
src/components/styled/SWModal.svelte
Normal file
88
src/components/styled/SWModal.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<!--
|
||||
- This file is a part of the SteamWar software.
|
||||
-
|
||||
- Copyright (C) 2024 SteamWar.de-Serverteam
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as published by
|
||||
- the Free Software Foundation, either version 3 of the License, or
|
||||
- (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {createEventDispatcher, onMount} from "svelte";
|
||||
|
||||
export let title: string;
|
||||
export let open: boolean;
|
||||
let internalOpen = open;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: if (open && !internalOpen) {
|
||||
dialog.showModal();
|
||||
internalOpen = true;
|
||||
} else if (!open && internalOpen) {
|
||||
dialog.close();
|
||||
internalOpen = false;
|
||||
}
|
||||
|
||||
let dialog: HTMLDialogElement;
|
||||
|
||||
onMount(() => {
|
||||
if (open) {
|
||||
dialog.showModal();
|
||||
internalOpen = true;
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
open = false;
|
||||
internalOpen = false;
|
||||
dispatch("close");
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog bind:this={dialog} on:close={close} on:cancel={close} on:click={() => dialog.close()} aria-hidden="true">
|
||||
<div on:click|stopPropagation aria-hidden="true">
|
||||
<div class="spaced bordered">
|
||||
<h1>{title}</h1>
|
||||
</div>
|
||||
<div class="spaced main bordered">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="footer spaced" on:click={() => dialog.close()} aria-hidden="true">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<style lang="scss">
|
||||
h1 {
|
||||
@apply text-4xl font-bold;
|
||||
}
|
||||
|
||||
dialog {
|
||||
@apply max-h-full max-w-md w-full rounded-lg shadow-lg
|
||||
dark:bg-neutral-800 dark:text-neutral-100;
|
||||
}
|
||||
|
||||
.spaced {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
.bordered {
|
||||
@apply border-b border-neutral-200 dark:border-neutral-700;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@apply flex mx-4 my-2;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user