mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 23:06:41 +02:00
feat(local-first): migrate 9 apps to reactive useLiveQuery reads
Replace manual $state + fetchX() pattern with Dexie liveQuery hooks across 9 apps. All data reads now auto-update on IndexedDB changes (local writes, sync, other tabs). Stores reduced to mutation-only. Apps migrated: - Zitare: favorites, lists - Contacts: contacts - Calendar: calendars, events - Chat: conversations, templates - Clock: alarms, timers, worldClocks - ManaDeck: decks, cards - Presi: decks, slides - Context: spaces, documents - Storage: files, folders Pattern per app: 1. New queries.ts with useLiveQuery hooks + pure filter helpers 2. Stores slimmed to mutation-only (no $state arrays, no fetch methods) 3. Layout sets context via setContext() for child components 4. Components use getContext() for reactive reads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ced7dd7441
commit
30e124e609
87 changed files with 2528 additions and 3136 deletions
|
|
@ -1,8 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { FileText } from '@manacore/shared-icons';
|
||||
import { documentsStore } from '$lib/stores/documents.svelte';
|
||||
import { useAllDocuments } from '$lib/data/queries';
|
||||
import type { Document } from '$lib/types';
|
||||
|
||||
const allDocs = useAllDocuments();
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
|
|
@ -20,7 +22,7 @@
|
|||
|
||||
let filteredDocs = $derived(
|
||||
mentionQuery.trim()
|
||||
? documentsStore.documents
|
||||
? (allDocs.value ?? [])
|
||||
.filter((d) => d.title.toLowerCase().includes(mentionQuery.toLowerCase()))
|
||||
.slice(0, 6)
|
||||
: []
|
||||
|
|
|
|||
147
apps/context/apps/web/src/lib/data/queries.ts
Normal file
147
apps/context/apps/web/src/lib/data/queries.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* Reactive Queries & Pure Helpers for Context
|
||||
*
|
||||
* Uses Dexie liveQuery to automatically re-render when IndexedDB changes
|
||||
* (local writes, sync updates, other tabs). Components call these hooks
|
||||
* at init time; no manual fetch/refresh needed.
|
||||
*/
|
||||
|
||||
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
|
||||
import {
|
||||
spaceCollection,
|
||||
documentCollection,
|
||||
type LocalSpace,
|
||||
type LocalDocument,
|
||||
} from './local-store';
|
||||
import type { Space, Document, DocumentType } from '$lib/types';
|
||||
|
||||
// ─── Type Converters ──────────────────────────────────────
|
||||
|
||||
/** Convert LocalSpace (IndexedDB) to shared Space type. */
|
||||
export function toSpace(local: LocalSpace): Space {
|
||||
return {
|
||||
id: local.id,
|
||||
name: local.name,
|
||||
description: local.description ?? null,
|
||||
user_id: 'local',
|
||||
created_at: local.createdAt ?? new Date().toISOString(),
|
||||
settings: local.settings ?? null,
|
||||
pinned: local.pinned,
|
||||
prefix: local.prefix,
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert LocalDocument (IndexedDB) to shared Document type. */
|
||||
export function toDocument(local: LocalDocument): Document {
|
||||
return {
|
||||
id: local.id,
|
||||
title: local.title,
|
||||
content: local.content,
|
||||
type: local.type,
|
||||
space_id: local.spaceId ?? null,
|
||||
user_id: 'local',
|
||||
created_at: local.createdAt ?? new Date().toISOString(),
|
||||
updated_at: local.updatedAt ?? new Date().toISOString(),
|
||||
metadata: local.metadata ?? null,
|
||||
short_id: local.shortId ?? undefined,
|
||||
pinned: local.pinned,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Live Query Hooks (call during component init) ────────
|
||||
|
||||
/** All spaces, sorted by name. Auto-updates on any change. */
|
||||
export function useAllSpaces() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await spaceCollection.getAll();
|
||||
return locals.map(toSpace).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [] as Space[]);
|
||||
}
|
||||
|
||||
/** All documents. Auto-updates on any change. */
|
||||
export function useAllDocuments() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await documentCollection.getAll();
|
||||
return locals
|
||||
.map(toDocument)
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
|
||||
}, [] as Document[]);
|
||||
}
|
||||
|
||||
/** Documents for a specific space. Auto-updates on any change. */
|
||||
export function useSpaceDocuments(spaceId: string) {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await documentCollection.getAll({ spaceId });
|
||||
return locals
|
||||
.map(toDocument)
|
||||
.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
|
||||
}, [] as Document[]);
|
||||
}
|
||||
|
||||
// ─── Pure Helper Functions (for $derived) ─────────────────
|
||||
|
||||
/** Get pinned spaces from a list. */
|
||||
export function getPinnedSpaces(spaces: Space[]): Space[] {
|
||||
return spaces.filter((s) => s.pinned);
|
||||
}
|
||||
|
||||
/** Filter documents by type, search query, and tags. */
|
||||
export function filterDocuments(
|
||||
documents: Document[],
|
||||
options: {
|
||||
typeFilter?: DocumentType | 'all';
|
||||
searchQuery?: string;
|
||||
tagFilter?: string[];
|
||||
}
|
||||
): Document[] {
|
||||
let filtered = documents;
|
||||
|
||||
if (options.typeFilter && options.typeFilter !== 'all') {
|
||||
filtered = filtered.filter((d) => d.type === options.typeFilter);
|
||||
}
|
||||
|
||||
if (options.searchQuery?.trim()) {
|
||||
const q = options.searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(d) => d.title.toLowerCase().includes(q) || d.content?.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
if (options.tagFilter && options.tagFilter.length > 0) {
|
||||
filtered = filtered.filter((d) =>
|
||||
options.tagFilter!.some((tag) => d.metadata?.tags?.includes(tag))
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/** Compute document stats from a list. */
|
||||
export function getDocumentStats(documents: Document[]) {
|
||||
return {
|
||||
total: documents.length,
|
||||
text: documents.filter((d) => d.type === 'text').length,
|
||||
context: documents.filter((d) => d.type === 'context').length,
|
||||
prompt: documents.filter((d) => d.type === 'prompt').length,
|
||||
totalWords: documents.reduce((sum, d) => sum + (d.metadata?.word_count || 0), 0),
|
||||
};
|
||||
}
|
||||
|
||||
/** Get all unique tags from documents. */
|
||||
export function getAllDocumentTags(documents: Document[]): string[] {
|
||||
const tags = new Set<string>();
|
||||
documents.forEach((d) => {
|
||||
d.metadata?.tags?.forEach((t) => tags.add(t));
|
||||
});
|
||||
return Array.from(tags).sort();
|
||||
}
|
||||
|
||||
/** Find a space by ID. */
|
||||
export function findSpaceById(spaces: Space[], id: string): Space | undefined {
|
||||
return spaces.find((s) => s.id === id);
|
||||
}
|
||||
|
||||
/** Find a document by ID. */
|
||||
export function findDocumentById(documents: Document[], id: string): Document | undefined {
|
||||
return documents.find((d) => d.id === id);
|
||||
}
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
/**
|
||||
* Documents Store — Mutation-Only (Local-First)
|
||||
*
|
||||
* Reads are handled by useLiveQuery hooks in queries.ts.
|
||||
* This store only handles writes and local filter state.
|
||||
*/
|
||||
|
||||
import type { Document, DocumentType } from '$lib/types';
|
||||
import { ContextEvents } from '@manacore/shared-utils/analytics';
|
||||
import * as docsService from '$lib/services/documents';
|
||||
import { documentCollection, type LocalDocument } from '$lib/data/local-store';
|
||||
import { toDocument } from '$lib/data/queries';
|
||||
|
||||
let documents = $state<Document[]>([]);
|
||||
let currentDocument = $state<Document | null>(null);
|
||||
let loading = $state(false);
|
||||
let saving = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Filter state
|
||||
// Filter state (UI-only, not persisted)
|
||||
let searchQuery = $state('');
|
||||
let typeFilter = $state<DocumentType | 'all'>('all');
|
||||
let tagFilter = $state<string[]>([]);
|
||||
|
||||
export const documentsStore = {
|
||||
get documents() {
|
||||
return documents;
|
||||
},
|
||||
get currentDocument() {
|
||||
return currentDocument;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
|
|
@ -39,45 +39,6 @@ export const documentsStore = {
|
|||
return tagFilter;
|
||||
},
|
||||
|
||||
get filteredDocuments() {
|
||||
let filtered = documents;
|
||||
|
||||
if (typeFilter !== 'all') {
|
||||
filtered = filtered.filter((d) => d.type === typeFilter);
|
||||
}
|
||||
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(d) => d.title.toLowerCase().includes(q) || d.content?.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
if (tagFilter.length > 0) {
|
||||
filtered = filtered.filter((d) => tagFilter.some((tag) => d.metadata?.tags?.includes(tag)));
|
||||
}
|
||||
|
||||
return filtered;
|
||||
},
|
||||
|
||||
get allTags() {
|
||||
const tags = new Set<string>();
|
||||
documents.forEach((d) => {
|
||||
d.metadata?.tags?.forEach((t) => tags.add(t));
|
||||
});
|
||||
return Array.from(tags).sort();
|
||||
},
|
||||
|
||||
get stats() {
|
||||
return {
|
||||
total: documents.length,
|
||||
text: documents.filter((d) => d.type === 'text').length,
|
||||
context: documents.filter((d) => d.type === 'context').length,
|
||||
prompt: documents.filter((d) => d.type === 'prompt').length,
|
||||
totalWords: documents.reduce((sum, d) => sum + (d.metadata?.word_count || 0), 0),
|
||||
};
|
||||
},
|
||||
|
||||
setSearchQuery(query: string) {
|
||||
searchQuery = query;
|
||||
},
|
||||
|
|
@ -90,30 +51,6 @@ export const documentsStore = {
|
|||
tagFilter = tags;
|
||||
},
|
||||
|
||||
async load(spaceId?: string) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
documents = await docsService.getDocumentsWithPreview(spaceId);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden der Dokumente';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadDocument(id: string) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
currentDocument = await docsService.getDocumentById(id);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden des Dokuments';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async create(
|
||||
userId: string,
|
||||
content: string,
|
||||
|
|
@ -122,21 +59,24 @@ export const documentsStore = {
|
|||
title?: string
|
||||
) {
|
||||
saving = true;
|
||||
error = null;
|
||||
try {
|
||||
const result = await docsService.createDocument(
|
||||
userId,
|
||||
const newLocal: LocalDocument = {
|
||||
id: crypto.randomUUID(),
|
||||
title: title || 'Neues Dokument',
|
||||
content,
|
||||
type,
|
||||
spaceId,
|
||||
undefined,
|
||||
title
|
||||
);
|
||||
if (result.data) {
|
||||
documents = [result.data, ...documents];
|
||||
currentDocument = result.data;
|
||||
ContextEvents.documentCreated(type);
|
||||
}
|
||||
return result;
|
||||
spaceId: spaceId || null,
|
||||
pinned: false,
|
||||
metadata: null,
|
||||
};
|
||||
const inserted = await documentCollection.insert(newLocal);
|
||||
ContextEvents.documentCreated(type);
|
||||
return { data: toDocument(inserted), error: null };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Erstellen';
|
||||
error = msg;
|
||||
return { data: null, error: msg };
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
|
|
@ -144,64 +84,67 @@ export const documentsStore = {
|
|||
|
||||
async update(id: string, updates: Partial<Document>) {
|
||||
saving = true;
|
||||
error = null;
|
||||
try {
|
||||
const result = await docsService.updateDocument(id, updates);
|
||||
if (result.success) {
|
||||
documents = documents.map((d) => (d.id === id ? { ...d, ...updates } : d));
|
||||
if (currentDocument?.id === id) {
|
||||
currentDocument = { ...currentDocument, ...updates };
|
||||
}
|
||||
}
|
||||
return result;
|
||||
const localUpdates: Partial<LocalDocument> = {};
|
||||
if (updates.title !== undefined) localUpdates.title = updates.title;
|
||||
if (updates.content !== undefined) localUpdates.content = updates.content!;
|
||||
if (updates.type !== undefined) localUpdates.type = updates.type;
|
||||
if (updates.pinned !== undefined) localUpdates.pinned = updates.pinned!;
|
||||
if (updates.metadata !== undefined) localUpdates.metadata = updates.metadata;
|
||||
|
||||
await documentCollection.update(id, localUpdates);
|
||||
return { success: true, error: null };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Aktualisieren';
|
||||
error = msg;
|
||||
return { success: false, error: msg };
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
},
|
||||
|
||||
async delete(id: string) {
|
||||
const result = await docsService.deleteDocument(id);
|
||||
if (result.success) {
|
||||
error = null;
|
||||
try {
|
||||
await documentCollection.delete(id);
|
||||
ContextEvents.documentDeleted();
|
||||
documents = documents.filter((d) => d.id !== id);
|
||||
if (currentDocument?.id === id) {
|
||||
currentDocument = null;
|
||||
}
|
||||
return { success: true, error: null };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Löschen';
|
||||
error = msg;
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
async togglePinned(id: string) {
|
||||
const doc = documents.find((d) => d.id === id);
|
||||
if (!doc) return;
|
||||
const newPinned = !doc.pinned;
|
||||
const result = await docsService.toggleDocumentPinned(id, newPinned);
|
||||
if (result.success) {
|
||||
async togglePinned(id: string, currentPinned: boolean) {
|
||||
error = null;
|
||||
try {
|
||||
const newPinned = !currentPinned;
|
||||
await documentCollection.update(id, { pinned: newPinned });
|
||||
ContextEvents.documentPinned(newPinned);
|
||||
documents = documents.map((d) => (d.id === id ? { ...d, pinned: newPinned } : d));
|
||||
if (currentDocument?.id === id) {
|
||||
currentDocument = { ...currentDocument, pinned: newPinned };
|
||||
}
|
||||
return { success: true, error: null };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Pin-Toggle';
|
||||
error = msg;
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
async saveTags(id: string, tags: string[]) {
|
||||
const result = await docsService.saveDocumentTags(id, tags);
|
||||
if (result.success) {
|
||||
documents = documents.map((d) =>
|
||||
d.id === id ? { ...d, metadata: { ...d.metadata, tags } } : d
|
||||
);
|
||||
if (currentDocument?.id === id) {
|
||||
currentDocument = {
|
||||
...currentDocument,
|
||||
metadata: { ...currentDocument.metadata, tags },
|
||||
};
|
||||
error = null;
|
||||
try {
|
||||
const existing = await documentCollection.get(id);
|
||||
if (existing) {
|
||||
await documentCollection.update(id, {
|
||||
metadata: { ...existing.metadata, tags },
|
||||
});
|
||||
}
|
||||
return { success: true, error: null };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Speichern der Tags';
|
||||
error = msg;
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
clearCurrent() {
|
||||
currentDocument = null;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,75 +1,90 @@
|
|||
/**
|
||||
* Spaces Store — Mutation-Only (Local-First)
|
||||
*
|
||||
* Reads are handled by useLiveQuery hooks in queries.ts.
|
||||
* This store only handles writes (create, update, delete, toggle).
|
||||
*/
|
||||
|
||||
import type { Space } from '$lib/types';
|
||||
import { ContextEvents } from '@manacore/shared-utils/analytics';
|
||||
import * as spacesService from '$lib/services/spaces';
|
||||
import { spaceCollection, type LocalSpace } from '$lib/data/local-store';
|
||||
import { toSpace } from '$lib/data/queries';
|
||||
|
||||
let spaces = $state<Space[]>([]);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
export const spacesStore = {
|
||||
get spaces() {
|
||||
return spaces;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
get pinnedSpaces() {
|
||||
return spaces.filter((s) => s.pinned);
|
||||
},
|
||||
|
||||
async load() {
|
||||
async create(userId: string, name: string, description?: string) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
spaces = await spacesService.getSpaces();
|
||||
const newLocal: LocalSpace = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
description: description || null,
|
||||
settings: null,
|
||||
pinned: true,
|
||||
prefix: name.charAt(0).toUpperCase(),
|
||||
};
|
||||
const inserted = await spaceCollection.insert(newLocal);
|
||||
ContextEvents.spaceCreated();
|
||||
return { data: toSpace(inserted), error: null };
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Fehler beim Laden der Spaces';
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Erstellen';
|
||||
error = msg;
|
||||
return { data: null, error: msg };
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async getById(id: string): Promise<Space | null> {
|
||||
return spacesService.getSpaceById(id);
|
||||
},
|
||||
|
||||
async create(userId: string, name: string, description?: string) {
|
||||
const result = await spacesService.createSpace(userId, name, description);
|
||||
if (result.data) {
|
||||
spaces = [result.data, ...spaces];
|
||||
ContextEvents.spaceCreated();
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
async update(id: string, updates: Partial<Space>) {
|
||||
const result = await spacesService.updateSpace(id, updates);
|
||||
if (result.success) {
|
||||
spaces = spaces.map((s) => (s.id === id ? { ...s, ...updates } : s));
|
||||
error = null;
|
||||
try {
|
||||
const localUpdates: Partial<LocalSpace> = {};
|
||||
if (updates.name !== undefined) localUpdates.name = updates.name;
|
||||
if (updates.description !== undefined) localUpdates.description = updates.description;
|
||||
if (updates.pinned !== undefined) localUpdates.pinned = updates.pinned;
|
||||
if (updates.settings !== undefined) localUpdates.settings = updates.settings;
|
||||
|
||||
await spaceCollection.update(id, localUpdates);
|
||||
return { success: true, error: null };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Aktualisieren';
|
||||
error = msg;
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
async togglePinned(id: string) {
|
||||
const space = spaces.find((s) => s.id === id);
|
||||
if (!space) return;
|
||||
const newPinned = !space.pinned;
|
||||
const result = await spacesService.toggleSpacePinned(id, newPinned);
|
||||
if (result.success) {
|
||||
spaces = spaces.map((s) => (s.id === id ? { ...s, pinned: newPinned } : s));
|
||||
async togglePinned(id: string, currentPinned: boolean) {
|
||||
error = null;
|
||||
try {
|
||||
await spaceCollection.update(id, { pinned: !currentPinned });
|
||||
return { success: true, error: null };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Pin-Toggle';
|
||||
error = msg;
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
async delete(id: string) {
|
||||
const result = await spacesService.deleteSpace(id);
|
||||
if (result.success) {
|
||||
spaces = spaces.filter((s) => s.id !== id);
|
||||
error = null;
|
||||
try {
|
||||
await spaceCollection.delete(id);
|
||||
ContextEvents.spaceDeleted();
|
||||
return { success: true, error: null };
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Fehler beim Löschen';
|
||||
error = msg;
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { spacesStore } from '$lib/stores/spaces.svelte';
|
||||
import { documentsStore } from '$lib/stores/documents.svelte';
|
||||
import { useAllSpaces, useAllDocuments } from '$lib/data/queries';
|
||||
import {
|
||||
THEME_DEFINITIONS,
|
||||
DEFAULT_THEME_VARIANTS,
|
||||
|
|
@ -42,6 +43,10 @@
|
|||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
// Live queries: all spaces and documents (reactive, auto-updates on IndexedDB changes)
|
||||
const allSpaces = useAllSpaces();
|
||||
const allDocuments = useAllDocuments();
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let commandBarOpen = $state(false);
|
||||
|
|
@ -70,7 +75,7 @@
|
|||
const results: CommandBarItem[] = [];
|
||||
|
||||
// Search spaces
|
||||
spacesStore.spaces
|
||||
(allSpaces.value ?? [])
|
||||
.filter((s) => s.name.toLowerCase().includes(q) || s.description?.toLowerCase().includes(q))
|
||||
.slice(0, 5)
|
||||
.forEach((s) => {
|
||||
|
|
@ -82,7 +87,7 @@
|
|||
});
|
||||
|
||||
// Search documents
|
||||
documentsStore.documents
|
||||
(allDocuments.value ?? [])
|
||||
.filter((d) => d.title.toLowerCase().includes(q) || d.content?.toLowerCase().includes(q))
|
||||
.slice(0, 5)
|
||||
.forEach((d) => {
|
||||
|
|
@ -243,7 +248,6 @@
|
|||
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
await Promise.all([spacesStore.load(), documentsStore.load()]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Folder, FileText, Sparkle, Plus } from '@manacore/shared-icons';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { spacesStore } from '$lib/stores/spaces.svelte';
|
||||
import { documentsStore } from '$lib/stores/documents.svelte';
|
||||
import {
|
||||
useAllSpaces,
|
||||
useAllDocuments,
|
||||
getPinnedSpaces,
|
||||
getDocumentStats,
|
||||
} from '$lib/data/queries';
|
||||
import DocumentCard from '$lib/components/DocumentCard.svelte';
|
||||
import SpaceCard from '$lib/components/SpaceCard.svelte';
|
||||
import { AppLoadingSkeleton } from '$lib/components/skeletons';
|
||||
|
||||
let isLoading = $state(true);
|
||||
let recentDocs = $state<typeof documentsStore.documents>([]);
|
||||
|
||||
onMount(async () => {
|
||||
await spacesStore.load();
|
||||
await documentsStore.load();
|
||||
recentDocs = documentsStore.documents.slice(0, 6);
|
||||
isLoading = false;
|
||||
});
|
||||
const allSpaces = useAllSpaces();
|
||||
const allDocuments = useAllDocuments();
|
||||
let spaces = $derived(allSpaces.value ?? []);
|
||||
let documents = $derived(allDocuments.value ?? []);
|
||||
let pinnedSpaces = $derived(getPinnedSpaces(spaces));
|
||||
let stats = $derived(getDocumentStats(documents));
|
||||
let recentDocs = $derived(documents.slice(0, 6));
|
||||
|
||||
function handleDeleteDoc(id: string) {
|
||||
documentsStore.delete(id);
|
||||
}
|
||||
|
||||
function handleTogglePinDoc(id: string) {
|
||||
documentsStore.togglePinned(id);
|
||||
const doc = documents.find((d) => d.id === id);
|
||||
documentsStore.togglePinned(id, doc?.pinned ?? false);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -31,97 +35,93 @@
|
|||
<title>Context - Dashboard</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if isLoading}
|
||||
<AppLoadingSkeleton />
|
||||
{:else}
|
||||
<div class="dashboard">
|
||||
<header class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-foreground">Context</h1>
|
||||
<p class="text-muted-foreground text-sm mt-1">Dein Wissensmanagement Hub</p>
|
||||
</header>
|
||||
<div class="dashboard">
|
||||
<header class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-foreground">Context</h1>
|
||||
<p class="text-muted-foreground text-sm mt-1">Dein Wissensmanagement Hub</p>
|
||||
</header>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-foreground">{spacesStore.spaces.length}</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Spaces</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-foreground">{documentsStore.stats.total}</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Dokumente</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-foreground">
|
||||
{documentsStore.stats.totalWords.toLocaleString()}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Wörter</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-foreground">
|
||||
{documentsStore.stats.text}/{documentsStore.stats.context}/{documentsStore.stats.prompt}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Text/Kontext/Prompt</div>
|
||||
</div>
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-foreground">{spaces.length}</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Spaces</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex gap-3 mb-8">
|
||||
<a href="/spaces" class="btn btn-primary flex items-center gap-2">
|
||||
<Folder size={16} />
|
||||
Spaces
|
||||
</a>
|
||||
<a href="/documents" class="btn btn-secondary flex items-center gap-2">
|
||||
<FileText size={16} />
|
||||
Alle Dokumente
|
||||
</a>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-foreground">{stats.total}</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Dokumente</div>
|
||||
</div>
|
||||
|
||||
<!-- Pinned Spaces -->
|
||||
{#if spacesStore.pinnedSpaces.length > 0}
|
||||
<section class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-foreground mb-4">Angeheftete Spaces</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{#each spacesStore.pinnedSpaces as space}
|
||||
<SpaceCard {space} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Recent Documents -->
|
||||
{#if recentDocs.length > 0}
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-foreground">Zuletzt bearbeitet</h2>
|
||||
<a href="/documents" class="text-sm text-primary hover:underline">Alle anzeigen</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{#each recentDocs as doc}
|
||||
<DocumentCard
|
||||
document={doc}
|
||||
onTogglePin={handleTogglePinDoc}
|
||||
onDelete={handleDeleteDoc}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{:else}
|
||||
<div class="card p-8 text-center">
|
||||
<div class="p-4 rounded-full bg-muted inline-block mb-4">
|
||||
<FileText size={48} class="text-muted-foreground" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-foreground mb-2">Noch keine Dokumente</h3>
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
Erstelle deinen ersten Space und beginne mit dem Schreiben.
|
||||
</p>
|
||||
<a href="/spaces" class="btn btn-primary inline-flex items-center gap-2">
|
||||
<Plus size={16} />
|
||||
Ersten Space erstellen
|
||||
</a>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-foreground">
|
||||
{stats.totalWords.toLocaleString()}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-xs text-muted-foreground mt-1">Wörter</div>
|
||||
</div>
|
||||
<div class="card p-4 text-center">
|
||||
<div class="text-2xl font-bold text-foreground">
|
||||
{stats.text}/{stats.context}/{stats.prompt}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Text/Kontext/Prompt</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex gap-3 mb-8">
|
||||
<a href="/spaces" class="btn btn-primary flex items-center gap-2">
|
||||
<Folder size={16} />
|
||||
Spaces
|
||||
</a>
|
||||
<a href="/documents" class="btn btn-secondary flex items-center gap-2">
|
||||
<FileText size={16} />
|
||||
Alle Dokumente
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Pinned Spaces -->
|
||||
{#if pinnedSpaces.length > 0}
|
||||
<section class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-foreground mb-4">Angeheftete Spaces</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{#each pinnedSpaces as space}
|
||||
<SpaceCard {space} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Recent Documents -->
|
||||
{#if recentDocs.length > 0}
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-foreground">Zuletzt bearbeitet</h2>
|
||||
<a href="/documents" class="text-sm text-primary hover:underline">Alle anzeigen</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{#each recentDocs as doc}
|
||||
<DocumentCard
|
||||
document={doc}
|
||||
onTogglePin={handleTogglePinDoc}
|
||||
onDelete={handleDeleteDoc}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{:else}
|
||||
<div class="card p-8 text-center">
|
||||
<div class="p-4 rounded-full bg-muted inline-block mb-4">
|
||||
<FileText size={48} class="text-muted-foreground" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-foreground mb-2">Noch keine Dokumente</h3>
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
Erstelle deinen ersten Space und beginne mit dem Schreiben.
|
||||
</p>
|
||||
<a href="/spaces" class="btn btn-primary inline-flex items-center gap-2">
|
||||
<Plus size={16} />
|
||||
Ersten Space erstellen
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dashboard {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Plus, MagnifyingGlass, FileText } from '@manacore/shared-icons';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { documentsStore } from '$lib/stores/documents.svelte';
|
||||
import {
|
||||
useAllDocuments,
|
||||
filterDocuments,
|
||||
getDocumentStats,
|
||||
getAllDocumentTags,
|
||||
} from '$lib/data/queries';
|
||||
import DocumentCard from '$lib/components/DocumentCard.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import type { DocumentType } from '$lib/types';
|
||||
|
||||
let deleteTarget = $state<string | null>(null);
|
||||
|
||||
const allDocuments = useAllDocuments();
|
||||
let documents = $derived(allDocuments.value ?? []);
|
||||
let stats = $derived(getDocumentStats(documents));
|
||||
let allTags = $derived(getAllDocumentTags(documents));
|
||||
let filteredDocuments = $derived(
|
||||
filterDocuments(documents, {
|
||||
typeFilter: documentsStore.typeFilter,
|
||||
searchQuery: documentsStore.searchQuery,
|
||||
tagFilter: documentsStore.tagFilter,
|
||||
})
|
||||
);
|
||||
|
||||
const typeFilters: { value: DocumentType | 'all'; label: string }[] = [
|
||||
{ value: 'all', label: 'Alle' },
|
||||
{ value: 'text', label: 'Text' },
|
||||
|
|
@ -18,10 +35,6 @@
|
|||
{ value: 'prompt', label: 'Prompt' },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
await documentsStore.load();
|
||||
});
|
||||
|
||||
async function handleCreateDocument() {
|
||||
if (!authStore.user?.id) return;
|
||||
const result = await documentsStore.create(
|
||||
|
|
@ -48,7 +61,8 @@
|
|||
}
|
||||
|
||||
function handleTogglePin(id: string) {
|
||||
documentsStore.togglePinned(id);
|
||||
const doc = documents.find((d) => d.id === id);
|
||||
documentsStore.togglePinned(id, doc?.pinned ?? false);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -61,7 +75,7 @@
|
|||
<div>
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('documents.title')}</h1>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
{documentsStore.stats.total} Dokumente, {documentsStore.stats.totalWords.toLocaleString()} Wörter
|
||||
{stats.total} Dokumente, {stats.totalWords.toLocaleString()} Wörter
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-primary flex items-center gap-2" onclick={handleCreateDocument}>
|
||||
|
|
@ -84,13 +98,13 @@
|
|||
>
|
||||
{filter.label}
|
||||
{#if filter.value === 'all'}
|
||||
<span class="ml-1 opacity-60">{documentsStore.stats.total}</span>
|
||||
<span class="ml-1 opacity-60">{stats.total}</span>
|
||||
{:else if filter.value === 'text'}
|
||||
<span class="ml-1 opacity-60">{documentsStore.stats.text}</span>
|
||||
<span class="ml-1 opacity-60">{stats.text}</span>
|
||||
{:else if filter.value === 'context'}
|
||||
<span class="ml-1 opacity-60">{documentsStore.stats.context}</span>
|
||||
<span class="ml-1 opacity-60">{stats.context}</span>
|
||||
{:else if filter.value === 'prompt'}
|
||||
<span class="ml-1 opacity-60">{documentsStore.stats.prompt}</span>
|
||||
<span class="ml-1 opacity-60">{stats.prompt}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -112,9 +126,9 @@
|
|||
</div>
|
||||
|
||||
<!-- Tags filter -->
|
||||
{#if documentsStore.allTags.length > 0}
|
||||
{#if allTags.length > 0}
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
{#each documentsStore.allTags as tag}
|
||||
{#each allTags as tag}
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded-full transition-colors"
|
||||
class:bg-primary={documentsStore.tagFilter.includes(tag)}
|
||||
|
|
@ -139,9 +153,9 @@
|
|||
<!-- Document list -->
|
||||
{#if documentsStore.loading}
|
||||
<div class="text-center py-12 text-muted-foreground">Lade Dokumente...</div>
|
||||
{:else if documentsStore.filteredDocuments.length > 0}
|
||||
{:else if filteredDocuments.length > 0}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{#each documentsStore.filteredDocuments as doc}
|
||||
{#each filteredDocuments as doc}
|
||||
<DocumentCard document={doc} onTogglePin={handleTogglePin} onDelete={handleDeleteClick} />
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { ArrowLeft, Trash, Sparkle } from '@manacore/shared-icons';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { documentsStore } from '$lib/stores/documents.svelte';
|
||||
import { useAllDocuments, findDocumentById } from '$lib/data/queries';
|
||||
import { tokensStore } from '$lib/stores/tokens.svelte';
|
||||
import DocumentEditor from '$lib/components/DocumentEditor.svelte';
|
||||
import AIToolbar from '$lib/components/AIToolbar.svelte';
|
||||
|
|
@ -12,32 +13,19 @@
|
|||
import type { Document, DocumentType } from '$lib/types';
|
||||
import type { InsertionMode } from '$lib/services/ai';
|
||||
|
||||
let loading = $state(true);
|
||||
let showDeleteConfirm = $state(false);
|
||||
let showAI = $state(false);
|
||||
|
||||
let docId = $derived($page.params.id || '');
|
||||
let doc = $derived(documentsStore.currentDocument);
|
||||
|
||||
const allDocuments = useAllDocuments();
|
||||
let doc = $derived(findDocumentById(allDocuments.value ?? [], docId) ?? null);
|
||||
|
||||
onMount(() => {
|
||||
const init = async () => {
|
||||
await documentsStore.loadDocument(docId);
|
||||
if (!documentsStore.currentDocument) {
|
||||
goto('/documents');
|
||||
return;
|
||||
}
|
||||
loading = false;
|
||||
|
||||
// Load token balance
|
||||
if (authStore.user?.id) {
|
||||
tokensStore.loadBalance(authStore.user.id);
|
||||
}
|
||||
};
|
||||
init();
|
||||
|
||||
return () => {
|
||||
documentsStore.clearCurrent();
|
||||
};
|
||||
// Load token balance
|
||||
if (authStore.user?.id) {
|
||||
tokensStore.loadBalance(authStore.user.id);
|
||||
}
|
||||
});
|
||||
|
||||
function handleSave(updates: Partial<Document>) {
|
||||
|
|
@ -64,9 +52,7 @@
|
|||
} else if (mode === 'replace') {
|
||||
documentsStore.update(docId, { content: text });
|
||||
}
|
||||
|
||||
// Reload document to get updated content
|
||||
documentsStore.loadDocument(docId);
|
||||
// liveQuery will automatically update the doc
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
|
|
@ -84,7 +70,7 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-4xl pb-48">
|
||||
{#if loading}
|
||||
{#if !doc && !allDocuments.error}
|
||||
<div class="text-center py-12 text-muted-foreground">Lade Dokument...</div>
|
||||
{:else if doc}
|
||||
<!-- Breadcrumb -->
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Plus, MagnifyingGlass } from '@manacore/shared-icons';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { spacesStore } from '$lib/stores/spaces.svelte';
|
||||
import { useAllSpaces } from '$lib/data/queries';
|
||||
import SpaceCard from '$lib/components/SpaceCard.svelte';
|
||||
import CreateSpaceModal from '$lib/components/CreateSpaceModal.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
|
|
@ -15,20 +15,19 @@
|
|||
let deleteTarget = $state<string | null>(null);
|
||||
let editTarget = $state<Space | null>(null);
|
||||
|
||||
const allSpaces = useAllSpaces();
|
||||
let spaces = $derived(allSpaces.value ?? []);
|
||||
|
||||
let filteredSpaces = $derived(
|
||||
searchQuery.trim()
|
||||
? spacesStore.spaces.filter(
|
||||
? spaces.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
s.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: spacesStore.spaces
|
||||
: spaces
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
await spacesStore.load();
|
||||
});
|
||||
|
||||
async function handleCreate(name: string, description: string) {
|
||||
if (!authStore.user?.id) return;
|
||||
creating = true;
|
||||
|
|
@ -38,7 +37,8 @@
|
|||
}
|
||||
|
||||
function handleTogglePin(id: string) {
|
||||
spacesStore.togglePinned(id);
|
||||
const space = spaces.find((s) => s.id === id);
|
||||
spacesStore.togglePinned(id, space?.pinned ?? false);
|
||||
}
|
||||
|
||||
function handleDeleteClick(id: string) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
|
|
@ -14,13 +13,18 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { spacesStore } from '$lib/stores/spaces.svelte';
|
||||
import { documentsStore } from '$lib/stores/documents.svelte';
|
||||
import {
|
||||
useAllSpaces,
|
||||
useSpaceDocuments,
|
||||
filterDocuments,
|
||||
getDocumentStats,
|
||||
findSpaceById,
|
||||
} from '$lib/data/queries';
|
||||
import DocumentCard from '$lib/components/DocumentCard.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import BatchCreateModal from '$lib/components/BatchCreateModal.svelte';
|
||||
import type { Space, DocumentType } from '$lib/types';
|
||||
|
||||
let space = $state<Space | null>(null);
|
||||
let loading = $state(true);
|
||||
let editingName = $state(false);
|
||||
let editName = $state('');
|
||||
let editDescription = $state('');
|
||||
|
|
@ -30,16 +34,24 @@
|
|||
|
||||
let spaceId = $derived($page.params.id || '');
|
||||
|
||||
onMount(async () => {
|
||||
space = await spacesStore.getById(spaceId);
|
||||
if (!space) {
|
||||
goto('/spaces');
|
||||
return;
|
||||
const allSpaces = useAllSpaces();
|
||||
const spaceDocs = useSpaceDocuments(spaceId);
|
||||
let space = $derived(findSpaceById(allSpaces.value ?? [], spaceId) ?? null);
|
||||
let documents = $derived(spaceDocs.value ?? []);
|
||||
let stats = $derived(getDocumentStats(documents));
|
||||
let filteredDocuments = $derived(
|
||||
filterDocuments(documents, {
|
||||
typeFilter: documentsStore.typeFilter,
|
||||
searchQuery: documentsStore.searchQuery,
|
||||
})
|
||||
);
|
||||
|
||||
// Keep editName/editDescription in sync with space
|
||||
$effect(() => {
|
||||
if (space && !editingName) {
|
||||
editName = space.name;
|
||||
editDescription = space.description || '';
|
||||
}
|
||||
editName = space.name;
|
||||
editDescription = space.description || '';
|
||||
await documentsStore.load(spaceId);
|
||||
loading = false;
|
||||
});
|
||||
|
||||
async function handleCreateDocument() {
|
||||
|
|
@ -90,7 +102,8 @@
|
|||
}
|
||||
|
||||
function handleTogglePinDoc(id: string) {
|
||||
documentsStore.togglePinned(id);
|
||||
const doc = documents.find((d) => d.id === id);
|
||||
documentsStore.togglePinned(id, doc?.pinned ?? false);
|
||||
}
|
||||
|
||||
async function handleBatchCreate(items: { title: string; type: DocumentType }[]) {
|
||||
|
|
@ -107,7 +120,6 @@
|
|||
}
|
||||
batchCreating = false;
|
||||
showBatchCreate = false;
|
||||
await documentsStore.load(spaceId);
|
||||
}
|
||||
|
||||
const typeFilters: { value: DocumentType | 'all'; label: string }[] = [
|
||||
|
|
@ -133,7 +145,7 @@
|
|||
<span class="text-foreground font-medium">{space?.name || '...'}</span>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
{#if !space && !allSpaces.error}
|
||||
<div class="text-center py-12 text-muted-foreground">Lade...</div>
|
||||
{:else if space}
|
||||
<!-- Space Header -->
|
||||
|
|
@ -168,8 +180,8 @@
|
|||
<p class="text-sm text-muted-foreground mt-1">{space.description}</p>
|
||||
{/if}
|
||||
<div class="flex gap-4 mt-3 text-xs text-muted-foreground">
|
||||
<span>{documentsStore.stats.total} Dokumente</span>
|
||||
<span>{documentsStore.stats.totalWords.toLocaleString()} Wörter</span>
|
||||
<span>{stats.total} Dokumente</span>
|
||||
<span>{stats.totalWords.toLocaleString()} Wörter</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -233,9 +245,9 @@
|
|||
</div>
|
||||
|
||||
<!-- Documents -->
|
||||
{#if documentsStore.filteredDocuments.length > 0}
|
||||
{#if filteredDocuments.length > 0}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{#each documentsStore.filteredDocuments as doc}
|
||||
{#each filteredDocuments as doc}
|
||||
<DocumentCard
|
||||
document={doc}
|
||||
onTogglePin={handleTogglePinDoc}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue