First commit
This commit is contained in:
16
src/routes/[orgSlug]/documents/+page.server.ts
Normal file
16
src/routes/[orgSlug]/documents/+page.server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent, locals }) => {
|
||||
const { org } = await parent();
|
||||
const { supabase } = locals;
|
||||
|
||||
const { data: documents } = await supabase
|
||||
.from('documents')
|
||||
.select('*')
|
||||
.eq('org_id', org.id)
|
||||
.order('name');
|
||||
|
||||
return {
|
||||
documents: documents ?? []
|
||||
};
|
||||
};
|
||||
212
src/routes/[orgSlug]/documents/+page.svelte
Normal file
212
src/routes/[orgSlug]/documents/+page.svelte
Normal file
@@ -0,0 +1,212 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import { Button, Modal, Input } from "$lib/components/ui";
|
||||
import { FileTree, Editor } from "$lib/components/documents";
|
||||
import { buildDocumentTree } from "$lib/api/documents";
|
||||
import type { Document } from "$lib/supabase/types";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { Database } from "$lib/supabase/types";
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
documents: Document[];
|
||||
user: { id: string } | null;
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const supabase = getContext<SupabaseClient<Database>>("supabase");
|
||||
|
||||
let documents = $state(data.documents);
|
||||
let selectedDoc = $state<Document | null>(null);
|
||||
let showCreateModal = $state(false);
|
||||
let newDocName = $state("");
|
||||
let newDocType = $state<"folder" | "document">("document");
|
||||
let parentFolderId = $state<string | null>(null);
|
||||
|
||||
const documentTree = $derived(buildDocumentTree(documents));
|
||||
|
||||
function handleSelect(doc: Document) {
|
||||
if (doc.type === "document") {
|
||||
selectedDoc = doc;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd(folderId: string | null) {
|
||||
parentFolderId = folderId;
|
||||
showCreateModal = true;
|
||||
}
|
||||
|
||||
async function handleMove(docId: string, newParentId: string | null) {
|
||||
const { error } = await supabase
|
||||
.from("documents")
|
||||
.update({
|
||||
parent_id: newParentId,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", docId);
|
||||
|
||||
if (!error) {
|
||||
documents = documents.map((d) =>
|
||||
d.id === docId ? { ...d, parent_id: newParentId } : d,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (!newDocName.trim() || !data.user) return;
|
||||
|
||||
const { data: newDoc, error } = await supabase
|
||||
.from("documents")
|
||||
.insert({
|
||||
org_id: data.org.id,
|
||||
name: newDocName,
|
||||
type: newDocType,
|
||||
parent_id: parentFolderId,
|
||||
created_by: data.user.id,
|
||||
content:
|
||||
newDocType === "document"
|
||||
? { type: "doc", content: [] }
|
||||
: null,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (!error && newDoc) {
|
||||
documents = [...documents, newDoc];
|
||||
if (newDocType === "document") {
|
||||
selectedDoc = newDoc;
|
||||
}
|
||||
}
|
||||
|
||||
showCreateModal = false;
|
||||
newDocName = "";
|
||||
newDocType = "document";
|
||||
parentFolderId = null;
|
||||
}
|
||||
|
||||
async function handleSave(content: unknown) {
|
||||
if (!selectedDoc) return;
|
||||
|
||||
await supabase
|
||||
.from("documents")
|
||||
.update({ content, updated_at: new Date().toISOString() })
|
||||
.eq("id", selectedDoc.id);
|
||||
|
||||
documents = documents.map((d) =>
|
||||
d.id === selectedDoc!.id ? { ...d, content } : d,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full">
|
||||
<aside class="w-72 border-r border-light/10 flex flex-col">
|
||||
<div
|
||||
class="p-4 border-b border-light/10 flex items-center justify-between"
|
||||
>
|
||||
<h2 class="font-semibold text-light">Documents</h2>
|
||||
<Button size="sm" onclick={() => (showCreateModal = true)}>
|
||||
<svg
|
||||
class="w-4 h-4 mr-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
New
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-2">
|
||||
{#if documentTree.length === 0}
|
||||
<div class="text-center text-light/40 py-8 text-sm">
|
||||
<p>No documents yet</p>
|
||||
<p class="mt-1">Create your first document</p>
|
||||
</div>
|
||||
{:else}
|
||||
<FileTree
|
||||
items={documentTree}
|
||||
selectedId={selectedDoc?.id ?? null}
|
||||
onSelect={handleSelect}
|
||||
onAdd={handleAdd}
|
||||
onMove={handleMove}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1 overflow-hidden">
|
||||
{#if selectedDoc}
|
||||
<Editor document={selectedDoc} onSave={handleSave} />
|
||||
{:else}
|
||||
<div class="h-full flex items-center justify-center text-light/40">
|
||||
<div class="text-center">
|
||||
<svg
|
||||
class="w-16 h-16 mx-auto mb-4 opacity-50"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<polyline points="14,2 14,8 20,8" />
|
||||
</svg>
|
||||
<p>Select a document to edit</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => (showCreateModal = false)}
|
||||
title="Create New"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 py-2 px-4 rounded-lg border transition-colors {newDocType ===
|
||||
'document'
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-light/20'}"
|
||||
onclick={() => (newDocType = "document")}
|
||||
>
|
||||
Document
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-2 px-4 rounded-lg border transition-colors {newDocType ===
|
||||
'folder'
|
||||
? 'border-primary bg-primary/10'
|
||||
: 'border-light/20'}"
|
||||
onclick={() => (newDocType = "folder")}
|
||||
>
|
||||
Folder
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Name"
|
||||
bind:value={newDocName}
|
||||
placeholder={newDocType === "folder"
|
||||
? "Folder name"
|
||||
: "Document name"}
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<Button variant="ghost" onclick={() => (showCreateModal = false)}
|
||||
>Cancel</Button
|
||||
>
|
||||
<Button onclick={handleCreate} disabled={!newDocName.trim()}
|
||||
>Create</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
Reference in New Issue
Block a user