feat: Add frontmatter editor and enhance page management with YAML support; update dependencies and improve UI interactions
Some checks failed
SteamWarCI Build failed
Some checks failed
SteamWarCI Build failed
This commit is contained in:
@ -23,6 +23,7 @@
|
|||||||
"@internationalized/date": "^3.8.1",
|
"@internationalized/date": "^3.8.1",
|
||||||
"@lucide/svelte": "^0.488.0",
|
"@lucide/svelte": "^0.488.0",
|
||||||
"@types/color": "^4.2.0",
|
"@types/color": "^4.2.0",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.15.23",
|
"@types/node": "^22.15.23",
|
||||||
"@types/three": "^0.170.0",
|
"@types/three": "^0.170.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||||
@ -76,6 +77,7 @@
|
|||||||
"flowbite": "^2.5.2",
|
"flowbite": "^2.5.2",
|
||||||
"flowbite-svelte": "^0.47.4",
|
"flowbite-svelte": "^0.47.4",
|
||||||
"flowbite-svelte-icons": "^2.2.0",
|
"flowbite-svelte-icons": "^2.2.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"svelte-awesome": "^3.3.5",
|
"svelte-awesome": "^3.3.5",
|
||||||
|
|||||||
@ -9,6 +9,9 @@
|
|||||||
import "easymde/dist/easymde.min.css";
|
import "easymde/dist/easymde.min.css";
|
||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { materialDark } from "@ddietr/codemirror-themes/theme/material-dark";
|
import { materialDark } from "@ddietr/codemirror-themes/theme/material-dark";
|
||||||
|
import FrontmatterEditor from "./FrontmatterEditor.svelte";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
|
||||||
let codemirrorParent: HTMLElement | undefined = $state();
|
let codemirrorParent: HTMLElement | undefined = $state();
|
||||||
let easyMdeParent: HTMLElement | undefined = $state();
|
let easyMdeParent: HTMLElement | undefined = $state();
|
||||||
@ -90,10 +93,24 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div>
|
<div class="flex-1 flex flex-col">
|
||||||
<div bind:this={codemirrorParent} class="hidden"></div>
|
{#if manager.selectedPage}
|
||||||
<div bind:this={easyMdeWrapper} class="hidden">
|
<div class="flex items-center justify-end p-2">
|
||||||
<textarea bind:this={easyMdeParent}></textarea>
|
<Button disabled={!(manager.selectedPage?.dirty ?? false)} onclick={() => manager.selectedPage?.save()}>Speichern</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
{#if manager.selectedPage.path.startsWith("src/content/announcements/")}
|
||||||
|
<div class="border-b flex-1" transition:slide>
|
||||||
|
<FrontmatterEditor />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1">
|
||||||
|
<div bind:this={codemirrorParent} class="hidden h-full"></div>
|
||||||
|
<div bind:this={easyMdeWrapper} class="hidden h-full">
|
||||||
|
<textarea bind:this={easyMdeParent}></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
122
src/components/moderator/pages/pages/FrontmatterEditor.svelte
Normal file
122
src/components/moderator/pages/pages/FrontmatterEditor.svelte
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { X } from "lucide-svelte";
|
||||||
|
import { manager } from "./page.svelte";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<details class="group">
|
||||||
|
<summary class="flex items-center justify-between p-3 cursor-pointer hover:bg-neutral-800">
|
||||||
|
<span class="font-medium">Frontmatter</span>
|
||||||
|
<svg class="w-4 h-4 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="p-3 border-t bg-neutral-900">
|
||||||
|
{#each Object.entries(manager.selectedPage?.frontmatter || {}) as [key, value]}
|
||||||
|
<div class="flex flex-col gap-2 mb-3 p-2 border rounded bg-neutral-800">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={key}
|
||||||
|
onchange={(e) => {
|
||||||
|
const newKey = (e.target as HTMLInputElement).value;
|
||||||
|
if (newKey !== key) {
|
||||||
|
manager.selectedPage!.frontmatter[newKey] = manager.selectedPage!.frontmatter[key];
|
||||||
|
delete manager.selectedPage?.frontmatter[key];
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="px-2 py-1 border rounded text-sm flex-shrink-0 w-32 bg-neutral-900"
|
||||||
|
placeholder="Key"
|
||||||
|
/>
|
||||||
|
<span>:</span>
|
||||||
|
{#if Array.isArray(value)}
|
||||||
|
<span class="text-xs text-muted-foreground">Array ({value.length} items)</span>
|
||||||
|
{:else if value instanceof Date || key === "created"}
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={value instanceof Date ? value.toISOString().split("T")[0] : typeof value === "string" ? value : ""}
|
||||||
|
onchange={(e) => {
|
||||||
|
const dateValue = (e.target as HTMLInputElement).value;
|
||||||
|
manager.selectedPage!.frontmatter[key] = dateValue ? new Date(dateValue) : "";
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={manager.selectedPage!.frontmatter[key]}
|
||||||
|
onchange={() => (manager.selectedPage!.dirty = true)}
|
||||||
|
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
||||||
|
placeholder="Value"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
delete manager.selectedPage!.frontmatter[key];
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-red-500 hover:text-red-700 p-1"
|
||||||
|
>
|
||||||
|
<X class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if Array.isArray(value)}
|
||||||
|
<div class="ml-4 space-y-1">
|
||||||
|
{#each value as item, index}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-muted-foreground w-6">[{index}]</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={manager.selectedPage!.frontmatter[key][index]}
|
||||||
|
onchange={() => (manager.selectedPage!.dirty = true)}
|
||||||
|
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
||||||
|
placeholder="Array item"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
manager.selectedPage!.frontmatter[key].splice(index, 1);
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-red-500 hover:text-red-700 p-1"
|
||||||
|
>
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
manager.selectedPage!.frontmatter[key].push("");
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-xs text-blue-500 hover:text-blue-700 ml-8"
|
||||||
|
>
|
||||||
|
+ Add item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
manager.selectedPage!.frontmatter[`new_key_${Object.keys(manager.selectedPage!.frontmatter).length}`] = "";
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-sm text-blue-500 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
+ Add field
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
manager.selectedPage!.frontmatter[`new_array_${Object.keys(manager.selectedPage!.frontmatter).length}`] = [];
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-sm text-green-500 hover:text-green-700"
|
||||||
|
>
|
||||||
|
+ Add array
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
@ -1,15 +1,143 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ResizablePane, ResizablePaneGroup } from "@components/ui/resizable";
|
import { ResizablePane, ResizablePaneGroup } from "@components/ui/resizable";
|
||||||
|
import { Separator } from "@components/ui/separator";
|
||||||
import { manager } from "./page.svelte";
|
import { manager } from "./page.svelte";
|
||||||
import ResizableHandle from "@components/ui/resizable/resizable-handle.svelte";
|
import ResizableHandle from "@components/ui/resizable/resizable-handle.svelte";
|
||||||
import PagesList from "./PagesList.svelte";
|
import PagesList from "./PagesList.svelte";
|
||||||
import EditorWithTabs from "./EditorWithTabs.svelte";
|
import EditorWithTabs from "./EditorWithTabs.svelte";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Check, ChevronsUpDown, RefreshCw, FileImage, Plus } from "lucide-svelte";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
|
import { cn } from "@components/utils";
|
||||||
|
import { pageRepo } from "@components/repo/page";
|
||||||
|
|
||||||
|
let branchSelectOpen = $state(false);
|
||||||
|
let imageSelectOpen = $state(false);
|
||||||
|
|
||||||
|
let fileInput: HTMLInputElement | undefined = $state();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-grow flex flex-col">
|
<div class="flex-grow flex flex-col">
|
||||||
<ResizablePaneGroup direction="horizontal" class="flex-grow">
|
<ResizablePaneGroup direction="horizontal" class="flex-grow">
|
||||||
<ResizablePane defaultSize={20}>
|
<ResizablePane defaultSize={20}>
|
||||||
<div class="overflow-y-scroll">
|
<div class="overflow-y-scroll">
|
||||||
|
<div class="flex p-2 gap-2">
|
||||||
|
<Popover bind:open={branchSelectOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" class="justify-between flex-1" {...props} role="combobox">
|
||||||
|
{manager.branch}
|
||||||
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search Branches..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No Branches Found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{#each manager.branches as branch}
|
||||||
|
<CommandItem
|
||||||
|
value={branch}
|
||||||
|
onSelect={() => {
|
||||||
|
if (manager.anyUnsavedChanges()) {
|
||||||
|
if (!confirm("You have unsaved changes. Are you sure you want to switch branches?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.branch = branch;
|
||||||
|
manager.pages = [];
|
||||||
|
branchSelectOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check class={cn("mr-2 size-4", branch !== manager.branch && "text-transparent")} />
|
||||||
|
{branch}
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Button size="icon" variant="outline" onclick={() => manager.reloadImages()}>
|
||||||
|
<RefreshCw />
|
||||||
|
</Button>
|
||||||
|
<Popover bind:open={imageSelectOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button size="icon" variant="outline" {...props}>
|
||||||
|
<FileImage />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side="right" class="w-[1000px] h-screen overflow-y-auto">
|
||||||
|
{#await manager.imagesLoad}
|
||||||
|
<p>Loading images...</p>
|
||||||
|
{:then images}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="p-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
bind:this={fileInput}
|
||||||
|
onchange={async (e) => {
|
||||||
|
const file = e.target?.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
const base64 = event.target?.result?.toString().split(",")[1];
|
||||||
|
if (base64) {
|
||||||
|
await $pageRepo.createImage(file.name, base64, manager.branch);
|
||||||
|
manager.reloadImages();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button onclick={() => fileInput?.click()} class="w-full">
|
||||||
|
<Plus class="mr-2 size-4" />
|
||||||
|
Upload Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 gap-2 p-2">
|
||||||
|
{#each images as image}
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
const backs = (manager.selectedPage?.path?.match(/\//g)?.length || 1) - 1;
|
||||||
|
|
||||||
|
const path = [...Array(backs).fill(".."), image.path.replace("src/", "")].join("/");
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(path);
|
||||||
|
imageSelectOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={image.downloadUrl} alt={image.name} class="w-full h-auto object-cover" />
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onclick={async () => {
|
||||||
|
const branchName = prompt("Enter branch name:");
|
||||||
|
if (branchName) {
|
||||||
|
await $pageRepo.createBranch(branchName);
|
||||||
|
manager.reloadBranches();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
{#await manager.pagesLoad}
|
{#await manager.pagesLoad}
|
||||||
<p>Loading pages...</p>
|
<p>Loading pages...</p>
|
||||||
{:then pages}
|
{:then pages}
|
||||||
|
|||||||
@ -2,10 +2,11 @@ import { base64ToBytes } from "@components/admin/util";
|
|||||||
import { pageRepo } from "@components/repo/page";
|
import { pageRepo } from "@components/repo/page";
|
||||||
import type { ListPage, PageList } from "@components/types/page";
|
import type { ListPage, PageList } from "@components/types/page";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
export class OpenEditPage {
|
export class OpenEditPage {
|
||||||
public content: string = "";
|
public content: string = "";
|
||||||
public frontmatter: { [key: string]: string } = $state({});
|
public frontmatter: { [key: string]: string | string[] | Date } = $state({});
|
||||||
public dirty: boolean = $state(false);
|
public dirty: boolean = $state(false);
|
||||||
|
|
||||||
public readonly fileType: string;
|
public readonly fileType: string;
|
||||||
@ -16,7 +17,7 @@ export class OpenEditPage {
|
|||||||
public readonly pageTitle: string,
|
public readonly pageTitle: string,
|
||||||
public readonly sha: string,
|
public readonly sha: string,
|
||||||
public readonly originalContent: string,
|
public readonly originalContent: string,
|
||||||
private readonly path: string
|
public readonly path: string
|
||||||
) {
|
) {
|
||||||
this.fileType = this.path.split(".").pop() || "md";
|
this.fileType = this.path.split(".").pop() || "md";
|
||||||
|
|
||||||
@ -24,6 +25,24 @@ export class OpenEditPage {
|
|||||||
this.frontmatter = this.parseFrontmatter(originalContent);
|
this.frontmatter = this.parseFrontmatter(originalContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async save(): Promise<void> {
|
||||||
|
if (!this.dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentToSave = "";
|
||||||
|
if (this.frontmatter) {
|
||||||
|
contentToSave += "---\n";
|
||||||
|
contentToSave += yaml.dump(this.frontmatter);
|
||||||
|
contentToSave += "---\n\n";
|
||||||
|
}
|
||||||
|
contentToSave += this.content;
|
||||||
|
const encodedContent = btoa(new TextEncoder().encode(contentToSave).reduce((data, byte) => data + String.fromCharCode(byte), ""));
|
||||||
|
await get(pageRepo).updatePage(this.pageId, this.sha, encodedContent, this.manager.branch);
|
||||||
|
this.dirty = false;
|
||||||
|
this.manager.reloadImages();
|
||||||
|
}
|
||||||
|
|
||||||
public focus(): boolean {
|
public focus(): boolean {
|
||||||
let index = this.manager.pages.indexOf(this);
|
let index = this.manager.pages.indexOf(this);
|
||||||
|
|
||||||
@ -35,18 +54,35 @@ export class OpenEditPage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseFrontmatter(content: string): { [key: string]: string } {
|
private parseFrontmatter(content: string): { [key: string]: string | string[] | Date } {
|
||||||
const frontmatter: { [key: string]: string } = {};
|
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
|
let inFrontmatter = false;
|
||||||
|
const frontmatterLines: string[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const match = line.match(/^\s*([a-zA-Z0-9_-]+):\s*(.*)$/);
|
if (line.trim() === "---") {
|
||||||
if (match) {
|
if (inFrontmatter) {
|
||||||
frontmatter[match[1]] = match[2].trim();
|
break; // End of frontmatter
|
||||||
|
}
|
||||||
|
inFrontmatter = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inFrontmatter) {
|
||||||
|
frontmatterLines.push(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return frontmatter;
|
if (frontmatterLines.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// You'll need to install js-yaml: npm install js-yaml @types/js-yaml
|
||||||
|
return (yaml.load(frontmatterLines.join("\n")) || {}) as { [key: string]: string | string[] | Date };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse YAML frontmatter:", error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeFrontmatter(content: string): string {
|
private removeFrontmatter(content: string): string {
|
||||||
@ -75,11 +111,34 @@ export interface DirTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PageManager {
|
export class PageManager {
|
||||||
|
public reloadImages() {
|
||||||
|
this.updater = this.updater + 1;
|
||||||
|
}
|
||||||
public branch: string = $state("master");
|
public branch: string = $state("master");
|
||||||
public pages: OpenEditPage[] = $state([]);
|
public pages: OpenEditPage[] = $state([]);
|
||||||
|
public branches: string[] = $state([]);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.reloadBranches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public reloadBranches() {
|
||||||
|
get(pageRepo)
|
||||||
|
.getBranches()
|
||||||
|
.then((branches) => {
|
||||||
|
this.branches = branches;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updater = $state(0);
|
||||||
|
|
||||||
public openPageIndex: number = $state(-1);
|
public openPageIndex: number = $state(-1);
|
||||||
public pagesLoad = $derived(get(pageRepo).listPages(this.branch).then(this.convertToTree));
|
public pagesLoad = $derived(get(pageRepo).listPages(this.branch).then(this.convertToTree).then(this._t(this.updater)));
|
||||||
|
public imagesLoad = $derived(get(pageRepo).listImages(this.branch).then(this._t(this.updater)));
|
||||||
|
|
||||||
|
private _t<T>(n: number): (v: T) => T {
|
||||||
|
return (v: T) => v;
|
||||||
|
}
|
||||||
|
|
||||||
public selectedPage = $derived(this.openPageIndex >= 0 ? this.pages[this.openPageIndex] : undefined);
|
public selectedPage = $derived(this.openPageIndex >= 0 ? this.pages[this.openPageIndex] : undefined);
|
||||||
|
|
||||||
@ -158,6 +217,10 @@ export class PageManager {
|
|||||||
await get(pageRepo).createFile(path, this.branch, newPageName, newPageName);
|
await get(pageRepo).createFile(path, this.branch, newPageName, newPageName);
|
||||||
this.branch = this.branch;
|
this.branch = this.branch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public anyUnsavedChanges() {
|
||||||
|
return this.pages.some((page) => page.dirty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const manager = $state(new PageManager());
|
export const manager = $state(new PageManager());
|
||||||
|
|||||||
@ -17,27 +17,26 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Page, PageList} from "@type/page.ts";
|
import type { Page, PageList } from "@type/page.ts";
|
||||||
import {fetchWithToken, tokenStore} from "./repo.ts";
|
import { fetchWithToken, tokenStore } from "./repo.ts";
|
||||||
import {PageListSchema, PageSchema} from "@type/page.ts";
|
import { PageListSchema, PageSchema } from "@type/page.ts";
|
||||||
import {bytesToBase64} from "../admin/util.ts";
|
import { bytesToBase64 } from "../admin/util.ts";
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
import {derived} from "svelte/store";
|
import { derived } from "svelte/store";
|
||||||
|
|
||||||
export class PageRepo {
|
export class PageRepo {
|
||||||
constructor(private token: string) {
|
constructor(private token: string) {}
|
||||||
}
|
|
||||||
|
|
||||||
public async listPages(branch: string = "master"): Promise<PageList> {
|
public async listPages(branch: string = "master"): Promise<PageList> {
|
||||||
return await fetchWithToken(this.token, `/page?branch=${branch}`)
|
return await fetchWithToken(this.token, `/page?branch=${branch}`)
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(PageListSchema.parse)
|
.then(PageListSchema.parse)
|
||||||
.then(value => value.map(value1 => ({...value1, path: value1.path.replace("src/content/", "")})));
|
.then((value) => value.map((value1) => ({ ...value1, path: value1.path.replace("src/content/", "") })));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPage(id: number, branch: string = "master"): Promise<Page> {
|
public async getPage(id: number, branch: string = "master"): Promise<Page> {
|
||||||
return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`)
|
return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`)
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(PageSchema.parse);
|
.then(PageSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,42 +45,57 @@ export class PageRepo {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: bytesToBase64(new TextEncoder().encode(content)),
|
content: bytesToBase64(new TextEncoder().encode(content)),
|
||||||
sha, message,
|
sha,
|
||||||
|
message,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBranches(): Promise<string[]> {
|
public async getBranches(): Promise<string[]> {
|
||||||
return await fetchWithToken(this.token, "/page/branch")
|
return await fetchWithToken(this.token, "/page/branch")
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(value => z.array(z.string()).parse(value));
|
.then((value) => z.array(z.string()).parse(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createBranch(branch: string): Promise<void> {
|
public async createBranch(branch: string): Promise<void> {
|
||||||
await fetchWithToken(this.token, "/page/branch", {method: "POST", body: JSON.stringify({branch})});
|
await fetchWithToken(this.token, "/page/branch", { method: "POST", body: JSON.stringify({ branch }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteBranch(branch: string): Promise<void> {
|
public async deleteBranch(branch: string): Promise<void> {
|
||||||
await fetchWithToken(this.token, "/page/branch", {method: "DELETE", body: JSON.stringify({branch})});
|
await fetchWithToken(this.token, "/page/branch", { method: "DELETE", body: JSON.stringify({ branch }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createFile(path: string, branch: string = "master", slug: string | null = null, title: string | null = null): Promise<void> {
|
public async createFile(path: string, branch: string = "master", slug: string | null = null, title: string | null = null): Promise<void> {
|
||||||
await fetchWithToken(this.token, `/page?branch=${branch}`, {method: "POST", body: JSON.stringify({path, slug, title})});
|
await fetchWithToken(this.token, `/page?branch=${branch}`, { method: "POST", body: JSON.stringify({ path, slug, title }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async merge(branch: string, message: string): Promise<void> {
|
public async merge(branch: string, message: string): Promise<void> {
|
||||||
await fetchWithToken(this.token, "/page/branch/merge", {
|
await fetchWithToken(this.token, "/page/branch/merge", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({branch, message}),
|
body: JSON.stringify({ branch, message }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deletePage(id: number, message: string, sha: string, branch: string = "master"): Promise<void> {
|
public async deletePage(id: number, message: string, sha: string, branch: string = "master"): Promise<void> {
|
||||||
await fetchWithToken(this.token, `/page/${id}?branch=${branch}`, {
|
await fetchWithToken(this.token, `/page/${id}?branch=${branch}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify({message, sha}),
|
body: JSON.stringify({ message, sha }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listImages(branch: string = "master"): Promise<PageList> {
|
||||||
|
return await fetchWithToken(this.token, `/page/images?branch=${branch}`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(PageListSchema.parse)
|
||||||
|
.then((value) => value.map((value1) => ({ ...value1, path: value1.path.replace("src/content/", "") })));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createImage(name: string, data: string, branch: string = "master"): Promise<void> {
|
||||||
|
await fetchWithToken(this.token, `/page/images?branch=${branch}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ name, data }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token));
|
export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token));
|
||||||
|
|||||||
Reference in New Issue
Block a user