mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(local-first): migrate tags + task stores to reactive liveQuery across all apps
- Todo: Replace manual fetch/state stores with useLiveQuery() for tasks,
projects, and tags. Components use Svelte context instead of store imports.
Stores reduced to mutation-only services. Removes ~200 lines of manual
state management. Enables multi-tab sync and auto-refresh on data changes.
- Tags (all 16 apps): Migrate from API-based createTagStore() to shared
local-first IndexedDB ('manacore-tags'). Tags now work offline and in
guest mode with default seed data. All apps share the same tag DB via
tagLocalStore + useAllTags() + setContext pattern.
- Cleanup: Delete unused Todo API files (projects.ts, labels.ts,
reminders.ts), remove dead labels store, clean up barrel exports.
Apps migrated: Todo, Zitare, Questions, Planta, Clock, Presi, Mukke,
Context, CityCorners, ManaDeck, Chat, Contacts, Calendar, Picture,
Storage, Photos
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
32939fbfb5
commit
5c33962439
83 changed files with 1896 additions and 3937 deletions
|
|
@ -1,11 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { DotsThree, Plus, X } from '@manacore/shared-icons';
|
||||
import TagStripModal from './TagStripModal.svelte';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
let showModal = $state(false);
|
||||
|
||||
function handleTagClick(tagId: string) {
|
||||
|
|
@ -27,16 +30,10 @@
|
|||
}
|
||||
|
||||
const sortedTags = $derived.by(() => {
|
||||
return [...eventTagsStore.tags].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
return [...tagsCtx.value].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
});
|
||||
|
||||
const hasTags = $derived(eventTagsStore.tags.length > 0);
|
||||
|
||||
onMount(async () => {
|
||||
if (eventTagsStore.tags.length === 0) {
|
||||
await eventTagsStore.fetchTags();
|
||||
}
|
||||
});
|
||||
const hasTags = $derived(tagsCtx.value.length > 0);
|
||||
</script>
|
||||
|
||||
<div class="tag-strip-wrapper">
|
||||
|
|
@ -59,9 +56,7 @@
|
|||
<span class="tag-name">Alle Tags</span>
|
||||
</button>
|
||||
|
||||
{#if eventTagsStore.loading}
|
||||
<div class="loading-state">Lädt...</div>
|
||||
{:else if !hasTags}
|
||||
{#if !hasTags}
|
||||
<button class="empty-state glass-tag" onclick={() => goto('/tags')}>
|
||||
<span>Keine Tags vorhanden</span>
|
||||
<span class="add-hint">+ Erstellen</span>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { Plus, X, Check, Pencil, Trash, MagnifyingGlass } from '@manacore/shared-icons';
|
||||
import { TagColorPicker, focusTrap } from '@manacore/shared-ui';
|
||||
import type { EventTag } from '@calendar/shared';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
|
@ -21,14 +25,14 @@
|
|||
let isCreatingTag = $state(false);
|
||||
|
||||
// Edit tag state
|
||||
let editingTag = $state<EventTag | null>(null);
|
||||
let editingTag = $state<Tag | null>(null);
|
||||
let editTagName = $state('');
|
||||
let editTagColor = $state('#3b82f6');
|
||||
let isSavingTag = $state(false);
|
||||
|
||||
// Filtered and sorted tags
|
||||
const sortedTags = $derived.by(() => {
|
||||
const tags = [...eventTagsStore.tags].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
const tags = [...tagsCtx.value].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
if (!searchQuery.trim()) return tags;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return tags.filter((t) => t.name.toLowerCase().includes(query));
|
||||
|
|
@ -51,17 +55,15 @@
|
|||
if (!newTagName.trim() || isCreatingTag) return;
|
||||
|
||||
isCreatingTag = true;
|
||||
const result = await eventTagsStore.createTag({
|
||||
name: newTagName.trim(),
|
||||
color: newTagColor,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error('Failed to create tag:', result.error);
|
||||
} else {
|
||||
try {
|
||||
await tagMutations.createTag({
|
||||
name: newTagName.trim(),
|
||||
color: newTagColor,
|
||||
});
|
||||
closeNewTagForm();
|
||||
} catch (e) {
|
||||
console.error('Failed to create tag:', e);
|
||||
}
|
||||
|
||||
isCreatingTag = false;
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +78,7 @@
|
|||
}
|
||||
|
||||
// ==================== EDIT TAG ====================
|
||||
function openEditTag(tag: EventTag) {
|
||||
function openEditTag(tag: Tag) {
|
||||
editingTag = tag;
|
||||
editTagName = tag.name;
|
||||
editTagColor = tag.color;
|
||||
|
|
@ -92,29 +94,26 @@
|
|||
if (!editingTag || !editTagName.trim() || isSavingTag) return;
|
||||
|
||||
isSavingTag = true;
|
||||
const result = await eventTagsStore.updateTag(editingTag.id, {
|
||||
name: editTagName.trim(),
|
||||
color: editTagColor,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error('Failed to update tag:', result.error);
|
||||
} else {
|
||||
try {
|
||||
await tagMutations.updateTag(editingTag.id, {
|
||||
name: editTagName.trim(),
|
||||
color: editTagColor,
|
||||
});
|
||||
closeEditTag();
|
||||
} catch (e) {
|
||||
console.error('Failed to update tag:', e);
|
||||
}
|
||||
|
||||
isSavingTag = false;
|
||||
}
|
||||
|
||||
async function handleDeleteTag() {
|
||||
if (!editingTag) return;
|
||||
|
||||
const result = await eventTagsStore.deleteTag(editingTag.id);
|
||||
|
||||
if (result.error) {
|
||||
console.error('Failed to delete tag:', result.error);
|
||||
} else {
|
||||
try {
|
||||
await tagMutations.deleteTag(editingTag.id);
|
||||
closeEditTag();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete tag:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,9 +168,7 @@
|
|||
|
||||
<!-- Content -->
|
||||
<div class="modal-content">
|
||||
{#if eventTagsStore.loading}
|
||||
<div class="loading-state">Lädt...</div>
|
||||
{:else if eventTagsStore.tags.length === 0 && !showNewTagForm}
|
||||
{#if tagsCtx.value.length === 0 && !showNewTagForm}
|
||||
<div class="empty-state">
|
||||
<p>Keine Tags vorhanden</p>
|
||||
<button class="create-btn" onclick={openNewTagForm}>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { getContext, type Snippet } from 'svelte';
|
||||
import { QuickInputBar, TagStrip } from '@manacore/shared-ui';
|
||||
import type { QuickInputItem, CreatePreview } from '@manacore/shared-ui';
|
||||
import { unifiedBarStore } from '$lib/stores/unified-bar.svelte';
|
||||
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import DateStrip from './DateStrip.svelte';
|
||||
import CalendarToolbarContent from './CalendarToolbarContent.svelte';
|
||||
|
|
@ -128,7 +131,7 @@
|
|||
{#if showCalendarLayers && unifiedBarStore.showTagStrip}
|
||||
<div class="unified-bar-layer tag-layer" transition:fly={flyConfig}>
|
||||
<TagStrip
|
||||
tags={eventTagsStore.tags.map((t) => ({
|
||||
tags={tagsCtx.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -137,7 +140,6 @@
|
|||
onToggle={(tagId) => settingsStore.toggleTagSelection(tagId)}
|
||||
onClear={() => settingsStore.clearTagSelection()}
|
||||
managementHref="/tags"
|
||||
loading={eventTagsStore.loading}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
|
||||
import type { Tag as SharedTag } from '@manacore/shared-tags';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: SharedTag[] } = getContext('tags');
|
||||
import {
|
||||
TagSelector,
|
||||
FilterDropdown,
|
||||
|
|
@ -91,7 +94,7 @@
|
|||
);
|
||||
|
||||
// Convert EventTag to Tag type for shared-ui components
|
||||
function eventTagToTag(tag: EventTag): Tag {
|
||||
function eventTagToTag(tag: SharedTag): Tag {
|
||||
return {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
|
|
@ -105,7 +108,7 @@
|
|||
}
|
||||
|
||||
// Derived available tags for TagSelector
|
||||
let availableTags = $derived(eventTagsStore.tags.map(eventTagToTag));
|
||||
let availableTags = $derived(tagsCtx.value.map(eventTagToTag));
|
||||
|
||||
// Calendar options for FilterDropdown
|
||||
let calendarOptions = $derived<FilterDropdownOption[]>(
|
||||
|
|
@ -172,13 +175,6 @@
|
|||
|
||||
let submitting = $state(false);
|
||||
|
||||
// Load tags on mount
|
||||
onMount(() => {
|
||||
if (eventTagsStore.tags.length === 0) {
|
||||
eventTagsStore.fetchTags();
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
@ -471,7 +467,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
{#if availableTags.length > 0 || eventTagsStore.loading}
|
||||
{#if availableTags.length > 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm font-medium text-foreground">Tags</span>
|
||||
<TagSelector
|
||||
|
|
|
|||
|
|
@ -1,93 +1,11 @@
|
|||
/**
|
||||
* Event Tags Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
*
|
||||
* Wraps createTagStore to provide a backward-compatible eventTagsStore interface
|
||||
* that existing Calendar components (TagStripModal, EventForm, /tags page) rely on.
|
||||
* Event Tags Store — Local-First via Shared Tag Store
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore, type TagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import type { EventTag } from '@calendar/shared';
|
||||
|
||||
// Re-export EventTag for backward compatibility
|
||||
export type { EventTag };
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Create the shared tag store
|
||||
const tagStore: TagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Backward-compatible wrapper that returns { data, error } results
|
||||
* to match the old Calendar API client pattern used by TagStripModal.
|
||||
*/
|
||||
async function wrapResult<T>(
|
||||
fn: () => Promise<T>
|
||||
): Promise<{ data: T | null; error: { message: string } | null }> {
|
||||
try {
|
||||
const data = await fn();
|
||||
return { data, error: null };
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||
return { data: null, error: { message } };
|
||||
}
|
||||
}
|
||||
|
||||
// Backward-compatible eventTagsStore wrapper
|
||||
export const eventTagsStore = {
|
||||
get tags() {
|
||||
return tagStore.tags as unknown as EventTag[];
|
||||
},
|
||||
get loading() {
|
||||
return tagStore.loading;
|
||||
},
|
||||
get error() {
|
||||
return tagStore.error;
|
||||
},
|
||||
|
||||
async fetchTags() {
|
||||
return tagStore.fetchTags();
|
||||
},
|
||||
|
||||
getById(id: string) {
|
||||
return tagStore.getById(id) as unknown as EventTag | undefined;
|
||||
},
|
||||
|
||||
getByIds(ids: string[]) {
|
||||
return tagStore.getByIds(ids) as unknown as EventTag[];
|
||||
},
|
||||
|
||||
async createTag(data: { name: string; color?: string; groupId?: string | null }) {
|
||||
return wrapResult(() => tagStore.createTag(data)) as Promise<{
|
||||
data: EventTag | null;
|
||||
error: { message: string } | null;
|
||||
}>;
|
||||
},
|
||||
|
||||
async updateTag(id: string, data: { name?: string; color?: string; groupId?: string | null }) {
|
||||
return wrapResult(() => tagStore.updateTag(id, data)) as Promise<{
|
||||
data: EventTag | null;
|
||||
error: { message: string } | null;
|
||||
}>;
|
||||
},
|
||||
|
||||
async deleteTag(id: string) {
|
||||
return wrapResult(() => tagStore.deleteTag(id));
|
||||
},
|
||||
|
||||
clear() {
|
||||
tagStore.clear();
|
||||
},
|
||||
};
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { setContext } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, InputBarHelpModal, ImmersiveModeToggle } from '@manacore/shared-ui';
|
||||
import {
|
||||
|
|
@ -22,7 +23,11 @@
|
|||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { birthdaysStore } from '$lib/stores/birthdays.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
|
@ -72,6 +77,10 @@
|
|||
splitPanel.openPanel(appId);
|
||||
}
|
||||
|
||||
// Live tag query + context
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// InputBar search - search events
|
||||
|
|
@ -129,7 +138,7 @@
|
|||
const resolved = resolveEventIds(
|
||||
parsed,
|
||||
calendarsStore.calendars.map((c) => ({ id: c.id, name: c.name })),
|
||||
eventTagsStore.tags.map((t) => ({ id: t.id, name: t.name })),
|
||||
allTags.value.map((t) => ({ id: t.id, name: t.name })),
|
||||
defaultCalendarId
|
||||
);
|
||||
|
||||
|
|
@ -285,12 +294,11 @@
|
|||
// Tag selector config for PillNavigation
|
||||
let tagSelectorConfig = $derived<PillTagSelectorConfig>({
|
||||
type: 'tag-selector',
|
||||
tags: eventTagsStore.tags.map((t) => ({ id: t.id, name: t.name, color: t.color || '#3b82f6' })),
|
||||
tags: allTags.value.map((t) => ({ id: t.id, name: t.name, color: t.color || '#3b82f6' })),
|
||||
selectedIds: settingsStore.selectedTagIds,
|
||||
onToggle: settingsStore.toggleTagSelection,
|
||||
onClear: settingsStore.clearTagSelection,
|
||||
onCreate: () => goto('/tags?new=true'),
|
||||
loading: eventTagsStore.loading,
|
||||
label: 'Tags',
|
||||
});
|
||||
|
||||
|
|
@ -407,6 +415,7 @@
|
|||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
tagMutations.stopSync();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
|
|
@ -433,8 +442,8 @@
|
|||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database (opens IndexedDB, seeds guest data)
|
||||
await calendarStore.initialize();
|
||||
// Initialize local-first databases (opens IndexedDB, seeds guest data)
|
||||
await Promise.all([calendarStore.initialize(), tagLocalStore.initialize()]);
|
||||
|
||||
// Initialize split-panel from URL/localStorage
|
||||
splitPanel.initialize();
|
||||
|
|
@ -447,10 +456,11 @@
|
|||
|
||||
// If authenticated, start syncing to server
|
||||
if (authStore.isAuthenticated) {
|
||||
calendarStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
calendarStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
|
||||
// Fetch tags and user settings (require auth)
|
||||
await eventTagsStore.fetchTags();
|
||||
// Load user settings (requires auth)
|
||||
await userSettings.load();
|
||||
} else if (shouldShowGuestWelcome('calendar')) {
|
||||
showGuestWelcome = true;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { Modal, Input, TagColorPicker, TagBadge } from '@manacore/shared-ui';
|
||||
import { MagnifyingGlass, Plus, CaretLeft } from '@manacore/shared-icons';
|
||||
import { eventTagsStore } from '$lib/stores/event-tags.svelte';
|
||||
import type { EventTag } from '@calendar/shared';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
let searchQuery = $state('');
|
||||
let showTagModal = $state(false);
|
||||
let editingTag = $state<EventTag | null>(null);
|
||||
let editingTag = $state<Tag | null>(null);
|
||||
|
||||
// Filtered tags based on search, sorted alphabetically
|
||||
const filteredTags = $derived.by(() => {
|
||||
const sorted = [...eventTagsStore.tags].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
const sorted = [...tagsCtx.value].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
if (!searchQuery.trim()) return sorted;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return sorted.filter((t) => t.name.toLowerCase().includes(query));
|
||||
|
|
@ -35,7 +38,7 @@
|
|||
showTagModal = true;
|
||||
}
|
||||
|
||||
function openEditTagModal(tag: EventTag) {
|
||||
function openEditTagModal(tag: Tag) {
|
||||
editingTag = tag;
|
||||
showTagModal = true;
|
||||
}
|
||||
|
|
@ -50,12 +53,12 @@
|
|||
|
||||
try {
|
||||
if (editingTag) {
|
||||
await eventTagsStore.updateTag(editingTag.id, {
|
||||
await tagMutations.updateTag(editingTag.id, {
|
||||
name: tagName.trim(),
|
||||
color: tagColor,
|
||||
});
|
||||
} else {
|
||||
await eventTagsStore.createTag({
|
||||
await tagMutations.createTag({
|
||||
name: tagName.trim(),
|
||||
color: tagColor,
|
||||
});
|
||||
|
|
@ -72,7 +75,7 @@
|
|||
if (!confirm(`Tag "${editingTag.name}" wirklich löschen?`)) return;
|
||||
|
||||
try {
|
||||
await eventTagsStore.deleteTag(editingTag.id);
|
||||
await tagMutations.deleteTag(editingTag.id);
|
||||
closeTagModal();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete tag:', e);
|
||||
|
|
@ -87,12 +90,6 @@
|
|||
}
|
||||
|
||||
const previewTag = $derived({ name: tagName || 'Tag Name', color: tagColor });
|
||||
|
||||
onMount(async () => {
|
||||
if (eventTagsStore.tags.length === 0) {
|
||||
await eventTagsStore.fetchTags();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -122,14 +119,14 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
{#if eventTagsStore.error}
|
||||
{#if false}
|
||||
<div class="error-banner" role="alert">
|
||||
<span>{eventTagsStore.error}</span>
|
||||
<span>{false}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Tag List -->
|
||||
{#if eventTagsStore.loading}
|
||||
{#if false}
|
||||
<div class="flex justify-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
|
|
@ -150,14 +147,14 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !eventTagsStore.loading && eventTagsStore.tags.length > 0}
|
||||
{#if !false && tagsCtx.value.length > 0}
|
||||
<p class="tags-count">
|
||||
{eventTagsStore.tags.length}
|
||||
{eventTagsStore.tags.length === 1 ? 'Tag' : 'Tags'}
|
||||
{tagsCtx.value.length}
|
||||
{tagsCtx.value.length === 1 ? 'Tag' : 'Tags'}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if !eventTagsStore.loading && eventTagsStore.tags.length === 0 && !searchQuery}
|
||||
{#if !false && tagsCtx.value.length === 0 && !searchQuery}
|
||||
<div class="empty-cta">
|
||||
<button onclick={openCreateTagModal} class="btn btn-primary">
|
||||
<Plus size={16} weight="bold" />
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -15,9 +15,14 @@
|
|||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { filterHiddenNavItems } from '@manacore/shared-theme';
|
||||
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
||||
import { setContext } from 'svelte';
|
||||
import { PillNavigation, TagStrip } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem } from '@manacore/shared-ui';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
|
|
@ -28,6 +33,10 @@
|
|||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { chatStore } from '$lib/data/local-store';
|
||||
|
||||
// Live tag query + context
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('chat');
|
||||
|
||||
|
|
@ -155,16 +164,19 @@
|
|||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
tagMutations.stopSync();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database
|
||||
await chatStore.initialize();
|
||||
// Initialize local-first databases
|
||||
await Promise.all([chatStore.initialize(), tagLocalStore.initialize()]);
|
||||
if (authStore.isAuthenticated) {
|
||||
chatStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
chatStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('chat')) {
|
||||
showGuestWelcome = true;
|
||||
|
|
@ -182,9 +194,8 @@
|
|||
|
||||
if (!authStore.isAuthenticated) return;
|
||||
|
||||
// Load user settings and tags
|
||||
// Load user settings
|
||||
await userSettings.load();
|
||||
await tagStore.fetchTags();
|
||||
|
||||
// Check for session conversations to migrate
|
||||
if (conversationsStore.hasSessionConversations) {
|
||||
|
|
@ -240,7 +251,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -249,7 +260,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +17,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { setContext } from 'svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { favoritesStore } from '$lib/stores/favorites.svelte';
|
||||
|
|
@ -21,6 +25,9 @@
|
|||
|
||||
const appItems = getPillAppItems('citycorners');
|
||||
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
|
@ -178,10 +185,11 @@
|
|||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await citycornersStore.initialize();
|
||||
await Promise.all([citycornersStore.initialize(), tagLocalStore.initialize()]);
|
||||
if (authStore.isAuthenticated) {
|
||||
citycornersStore.startSync(() => authStore.getValidToken());
|
||||
await tagStore.fetchTags();
|
||||
const getToken = () => authStore.getValidToken();
|
||||
citycornersStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('citycorners')) {
|
||||
showGuestWelcome = true;
|
||||
|
|
@ -227,7 +235,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -236,7 +244,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +16,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +17,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { tagsStore } from '$lib/stores/tags.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { contactsFilterStore } from '$lib/stores/filter.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { DotsThree, Plus, X } from '@manacore/shared-icons';
|
||||
import TagStripModal from './TagStripModal.svelte';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
let showModal = $state(false);
|
||||
|
||||
function handleTagClick(tagId: string) {
|
||||
|
|
@ -31,16 +34,10 @@
|
|||
}
|
||||
|
||||
const sortedTags = $derived.by(() => {
|
||||
return [...tagsStore.tags].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
return [...tagsCtx.value].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
});
|
||||
|
||||
const hasTags = $derived(tagsStore.tags.length > 0);
|
||||
|
||||
onMount(async () => {
|
||||
if (tagsStore.tags.length === 0) {
|
||||
await tagsStore.fetchTags();
|
||||
}
|
||||
});
|
||||
const hasTags = $derived(tagsCtx.value.length > 0);
|
||||
</script>
|
||||
|
||||
<div class="tag-strip-wrapper">
|
||||
|
|
@ -63,9 +60,7 @@
|
|||
<span class="tag-name">Alle Tags</span>
|
||||
</button>
|
||||
|
||||
{#if tagsStore.loading}
|
||||
<div class="loading-state">Lädt...</div>
|
||||
{:else if !hasTags}
|
||||
{#if !hasTags}
|
||||
<button class="empty-state glass-tag" onclick={() => goto('/tags')}>
|
||||
<span>Keine Tags vorhanden</span>
|
||||
<span class="add-hint">+ Erstellen</span>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { tagsStore } from '$lib/stores/tags.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { Plus, X, Check, Pencil, Trash, MagnifyingGlass } from '@manacore/shared-icons';
|
||||
import { TagColorPicker } from '@manacore/shared-ui';
|
||||
import type { ContactTag } from '$lib/api/contacts';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
|
@ -21,14 +25,14 @@
|
|||
let isCreatingTag = $state(false);
|
||||
|
||||
// Edit tag state
|
||||
let editingTag = $state<ContactTag | null>(null);
|
||||
let editingTag = $state<Tag | null>(null);
|
||||
let editTagName = $state('');
|
||||
let editTagColor = $state('#3b82f6');
|
||||
let isSavingTag = $state(false);
|
||||
|
||||
// Filtered and sorted tags
|
||||
const sortedTags = $derived.by(() => {
|
||||
const tags = [...tagsStore.tags].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
const tags = [...tagsCtx.value].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
if (!searchQuery.trim()) return tags;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return tags.filter((t) => t.name.toLowerCase().includes(query));
|
||||
|
|
@ -52,7 +56,7 @@
|
|||
|
||||
isCreatingTag = true;
|
||||
try {
|
||||
await tagsStore.createTag({
|
||||
await tagMutations.createTag({
|
||||
name: newTagName.trim(),
|
||||
color: newTagColor,
|
||||
});
|
||||
|
|
@ -74,7 +78,7 @@
|
|||
}
|
||||
|
||||
// ==================== EDIT TAG ====================
|
||||
function openEditTag(tag: ContactTag) {
|
||||
function openEditTag(tag: Tag) {
|
||||
editingTag = tag;
|
||||
editTagName = tag.name;
|
||||
editTagColor = tag.color;
|
||||
|
|
@ -91,7 +95,7 @@
|
|||
|
||||
isSavingTag = true;
|
||||
try {
|
||||
await tagsStore.updateTag(editingTag.id, {
|
||||
await tagMutations.updateTag(editingTag.id, {
|
||||
name: editTagName.trim(),
|
||||
color: editTagColor,
|
||||
});
|
||||
|
|
@ -106,7 +110,7 @@
|
|||
if (!editingTag) return;
|
||||
|
||||
try {
|
||||
await tagsStore.deleteTag(editingTag.id);
|
||||
await tagMutations.deleteTag(editingTag.id);
|
||||
closeEditTag();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete tag:', e);
|
||||
|
|
@ -164,9 +168,7 @@
|
|||
|
||||
<!-- Content -->
|
||||
<div class="modal-content">
|
||||
{#if tagsStore.loading}
|
||||
<div class="loading-state">Lädt...</div>
|
||||
{:else if tagsStore.tags.length === 0 && !showNewTagForm}
|
||||
{#if tagsCtx.value.length === 0 && !showNewTagForm}
|
||||
<div class="empty-state">
|
||||
<p>Keine Tags vorhanden</p>
|
||||
<button class="create-btn" onclick={openNewTagForm}>
|
||||
|
|
|
|||
|
|
@ -1,65 +1,11 @@
|
|||
/**
|
||||
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
*
|
||||
* Centralized store for tags, used by TagStrip, TagStripModal, and tags page.
|
||||
* Wraps createTagStore for backward compatibility with existing ContactTag interface.
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore, type TagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
// Re-export Tag as ContactTag for backward compatibility
|
||||
export type ContactTag = Tag;
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Create the shared tag store
|
||||
const tagStore: TagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
|
||||
// Backward-compatible tagsStore wrapper
|
||||
export const tagsStore = {
|
||||
get tags() {
|
||||
return tagStore.tags;
|
||||
},
|
||||
get loading() {
|
||||
return tagStore.loading;
|
||||
},
|
||||
get error() {
|
||||
return tagStore.error;
|
||||
},
|
||||
|
||||
async fetchTags() {
|
||||
return tagStore.fetchTags();
|
||||
},
|
||||
getById(id: string) {
|
||||
return tagStore.getById(id);
|
||||
},
|
||||
getColor(tagId: string) {
|
||||
return tagStore.getColor(tagId);
|
||||
},
|
||||
|
||||
async createTag(data: { name: string; color?: string }) {
|
||||
return tagStore.createTag(data);
|
||||
},
|
||||
async updateTag(id: string, data: { name?: string; color?: string }) {
|
||||
return tagStore.updateTag(id, data);
|
||||
},
|
||||
async deleteTag(id: string) {
|
||||
return tagStore.deleteTag(id);
|
||||
},
|
||||
clear() {
|
||||
tagStore.clear();
|
||||
},
|
||||
};
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { setContext } from 'svelte';
|
||||
import {
|
||||
PillNavigation,
|
||||
QuickInputBar,
|
||||
|
|
@ -46,7 +47,11 @@
|
|||
formatParsedContactPreview,
|
||||
} from '$lib/utils/contact-parser';
|
||||
import ContactsToolbar from '$lib/components/ContactsToolbar.svelte';
|
||||
import { tagsStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { contactsOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
|
|
@ -62,8 +67,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Tags state for Quick-Create
|
||||
let availableTags = $state<{ id: string; name: string }[]>([]);
|
||||
// Live tag query + context
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
// Check if we're on a contact detail route
|
||||
const contactDetailMatch = $derived($page.url.pathname.match(/^\/contacts\/([0-9a-f-]{36})$/i));
|
||||
|
|
@ -215,6 +221,7 @@
|
|||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
tagMutations.stopSync();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
|
|
@ -294,12 +301,14 @@
|
|||
});
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database (opens IndexedDB, seeds guest data)
|
||||
await contactsLocalStore.initialize();
|
||||
// Initialize local-first databases (opens IndexedDB, seeds guest data)
|
||||
await Promise.all([contactsLocalStore.initialize(), tagLocalStore.initialize()]);
|
||||
|
||||
// If authenticated, start syncing to server
|
||||
if (authStore.isAuthenticated) {
|
||||
contactsLocalStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
contactsLocalStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
|
||||
// Initialize split-panel from URL/localStorage
|
||||
|
|
@ -316,11 +325,9 @@
|
|||
// Load contacts from IndexedDB (guest seed or synced data)
|
||||
await contactsStore.loadContacts();
|
||||
|
||||
// Load user settings and tags only when authenticated
|
||||
// Load user settings only when authenticated
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
await tagsStore.fetchTags();
|
||||
availableTags = tagsStore.tags.map((t) => ({ id: t.id, name: t.name }));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -378,7 +385,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagsStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { ContactTag } from '$lib/api/contacts';
|
||||
import { tagsStore } from '$lib/stores/tags.svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag as SharedTag } from '@manacore/shared-tags';
|
||||
import { TagList, TagEditModal, type Tag } from '@manacore/shared-ui';
|
||||
import { MagnifyingGlass, Plus, CaretLeft } from '@manacore/shared-icons';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: SharedTag[] } = getContext('tags');
|
||||
|
||||
let searchQuery = $state('');
|
||||
|
||||
// Modal state
|
||||
let showModal = $state(false);
|
||||
let editingTag = $state<ContactTag | null>(null);
|
||||
let editingTag = $state<SharedTag | null>(null);
|
||||
|
||||
const filteredTags = $derived.by(() => {
|
||||
if (!searchQuery.trim()) return tagsStore.tags;
|
||||
if (!searchQuery.trim()) return tagsCtx.value;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return tagsStore.tags.filter((t) => t.name.toLowerCase().includes(query));
|
||||
return tagsCtx.value.filter((t) => t.name.toLowerCase().includes(query));
|
||||
});
|
||||
|
||||
function openCreateModal() {
|
||||
|
|
@ -23,7 +26,7 @@
|
|||
showModal = true;
|
||||
}
|
||||
|
||||
function openEditModal(tag: ContactTag) {
|
||||
function openEditModal(tag: SharedTag) {
|
||||
editingTag = tag;
|
||||
showModal = true;
|
||||
}
|
||||
|
|
@ -36,9 +39,9 @@
|
|||
async function handleSave(name: string, color: string) {
|
||||
try {
|
||||
if (editingTag) {
|
||||
await tagsStore.updateTag(editingTag.id, { name, color });
|
||||
await tagMutations.updateTag(editingTag.id, { name, color });
|
||||
} else {
|
||||
await tagsStore.createTag({ name, color });
|
||||
await tagMutations.createTag({ name, color });
|
||||
}
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
|
|
@ -50,7 +53,7 @@
|
|||
if (!editingTag) return;
|
||||
|
||||
try {
|
||||
await tagsStore.deleteTag(editingTag.id);
|
||||
await tagMutations.deleteTag(editingTag.id);
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete tag:', e);
|
||||
|
|
@ -61,17 +64,11 @@
|
|||
if (!confirm($_('tags.confirmDelete', { values: { name: tag.name } }))) return;
|
||||
|
||||
try {
|
||||
await tagsStore.deleteTag(tag.id);
|
||||
await tagMutations.deleteTag(tag.id);
|
||||
} catch (e) {
|
||||
console.error('Failed to delete tag:', e);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (tagsStore.tags.length === 0) {
|
||||
tagsStore.fetchTags();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -101,17 +98,10 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
{#if tagsStore.error}
|
||||
<div class="error-banner" role="alert">
|
||||
<span>{tagsStore.error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Tag List using shared component -->
|
||||
<TagList
|
||||
tags={filteredTags}
|
||||
loading={tagsStore.loading}
|
||||
onEdit={(tag) => openEditModal(tag as ContactTag)}
|
||||
onEdit={(tag) => openEditModal(tag as SharedTag)}
|
||||
onDelete={handleDeleteFromList}
|
||||
emptyMessage={searchQuery ? $_('tags.noResults') : $_('tags.noTags')}
|
||||
emptyDescription={searchQuery
|
||||
|
|
@ -119,14 +109,14 @@
|
|||
: $_('tags.createFirst')}
|
||||
/>
|
||||
|
||||
{#if !tagsStore.loading && tagsStore.tags.length > 0}
|
||||
{#if tagsCtx.value.length > 0}
|
||||
<p class="tags-count">
|
||||
{tagsStore.tags.length}
|
||||
{tagsStore.tags.length === 1 ? $_('tags.tagSingular') : $_('tags.tagPlural')}
|
||||
{tagsCtx.value.length}
|
||||
{tagsCtx.value.length === 1 ? $_('tags.tagSingular') : $_('tags.tagPlural')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if !tagsStore.loading && tagsStore.tags.length === 0 && !searchQuery}
|
||||
{#if tagsCtx.value.length === 0 && !searchQuery}
|
||||
<div class="empty-cta">
|
||||
<button onclick={openCreateModal} class="btn btn-primary">
|
||||
<Plus size={16} weight="bold" />
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, CommandBar, TagStrip } from '@manacore/shared-ui';
|
||||
import type {
|
||||
|
|
@ -31,10 +31,17 @@
|
|||
import { AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { contextStore } from '$lib/data/local-store';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
|
||||
const appItems = getPillAppItems('context');
|
||||
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let commandBarOpen = $state(false);
|
||||
|
|
@ -218,9 +225,11 @@
|
|||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await contextStore.initialize();
|
||||
await Promise.all([contextStore.initialize(), tagLocalStore.initialize()]);
|
||||
if (authStore.isAuthenticated) {
|
||||
contextStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
contextStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('context')) {
|
||||
showGuestWelcome = true;
|
||||
|
|
@ -234,7 +243,6 @@
|
|||
|
||||
if (authStore.isAuthenticated) {
|
||||
await userSettings.load();
|
||||
await tagStore.fetchTags();
|
||||
await Promise.all([spacesStore.load(), documentsStore.load()]);
|
||||
}
|
||||
}
|
||||
|
|
@ -280,7 +288,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -289,7 +297,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +16,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -2,13 +2,18 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { setContext } from 'svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
||||
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import {
|
||||
THEME_DEFINITIONS,
|
||||
DEFAULT_THEME_VARIANTS,
|
||||
|
|
@ -29,6 +34,9 @@
|
|||
// App switcher items
|
||||
const appItems = getPillAppItems('manadeck');
|
||||
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isCollapsed = $state(false);
|
||||
|
|
@ -177,12 +185,14 @@
|
|||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database (opens IndexedDB, seeds guest data)
|
||||
await manadeckStore.initialize();
|
||||
// Initialize local-first database and shared tag store
|
||||
await Promise.all([manadeckStore.initialize(), tagLocalStore.initialize()]);
|
||||
|
||||
// If authenticated, start syncing to server
|
||||
if (authStore.isAuthenticated) {
|
||||
manadeckStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
manadeckStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
|
||||
// Load decks from IndexedDB (guest seed or synced data)
|
||||
|
|
@ -194,9 +204,8 @@
|
|||
}
|
||||
|
||||
if (authStore.isAuthenticated) {
|
||||
// Load user settings and tags (require auth)
|
||||
// Load user settings (require auth)
|
||||
await userSettings.load();
|
||||
await tagStore.fetchTags();
|
||||
}
|
||||
|
||||
// Redirect to start page if on root and a custom start page is set
|
||||
|
|
@ -255,7 +264,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -264,7 +273,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +16,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { setContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { PillNavigation, QuickInputBar, DevBuildBadge, TagStrip } from '@manacore/shared-ui';
|
||||
|
|
@ -29,13 +30,21 @@
|
|||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { mukkeStore } from '$lib/data/local-store';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import MiniPlayer from '$lib/components/MiniPlayer.svelte';
|
||||
import FullPlayer from '$lib/components/FullPlayer.svelte';
|
||||
import QueuePanel from '$lib/components/QueuePanel.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// Shared tag store (local-first)
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('mukke' as any);
|
||||
|
||||
|
|
@ -185,17 +194,16 @@
|
|||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await mukkeStore.initialize();
|
||||
await Promise.all([mukkeStore.initialize(), tagLocalStore.initialize()]);
|
||||
if (authStore.isAuthenticated) {
|
||||
mukkeStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
mukkeStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('mukke')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
splitPanel.initialize();
|
||||
if (authStore.isAuthenticated) {
|
||||
await tagStore.fetchTags();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -242,7 +250,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -251,7 +259,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +17,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { format } from 'date-fns';
|
||||
import type { Photo } from '@photos/shared';
|
||||
import { photoStore } from '$lib/stores/photos.svelte';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
|
||||
interface Props {
|
||||
photo: Photo;
|
||||
|
|
|
|||
|
|
@ -1,91 +1,28 @@
|
|||
/**
|
||||
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
*
|
||||
* Wraps createTagStore for backward compatibility with existing tagStore interface.
|
||||
* Also preserves photo-specific tag operations (getPhotoTags, addTagToPhoto, etc.)
|
||||
* which go through the Photos backend, not mana-core-auth.
|
||||
* Core tag CRUD is handled by the shared local-first tag store.
|
||||
* Photo-specific tag operations (junction table) go through the Photos backend.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore, type TagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
||||
import { api } from '$lib/api/client';
|
||||
import type { Tag } from '@photos/shared';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Create the shared tag store
|
||||
const sharedTagStore: TagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
|
||||
// Backward-compatible tagStore wrapper
|
||||
export const tagStore = {
|
||||
// Getters (delegate to shared store)
|
||||
get tags() {
|
||||
return sharedTagStore.tags;
|
||||
},
|
||||
get loading() {
|
||||
return sharedTagStore.loading;
|
||||
},
|
||||
get error() {
|
||||
return sharedTagStore.error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all tags
|
||||
*/
|
||||
async loadTags() {
|
||||
return sharedTagStore.fetchTags();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create new tag
|
||||
*/
|
||||
async createTag(data: { name: string; color?: string }) {
|
||||
try {
|
||||
const tag = await sharedTagStore.createTag(data);
|
||||
return tag;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update tag
|
||||
*/
|
||||
async updateTag(id: string, data: { name?: string; color?: string }) {
|
||||
try {
|
||||
const tag = await sharedTagStore.updateTag(id, data);
|
||||
return tag;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete tag
|
||||
*/
|
||||
async deleteTag(id: string) {
|
||||
try {
|
||||
await sharedTagStore.deleteTag(id);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get tags for a photo (from Photos backend)
|
||||
*/
|
||||
/**
|
||||
* Photo-specific tag operations (junction table: photo <-> tag).
|
||||
* These go through the Photos backend, not the shared tag store.
|
||||
*/
|
||||
export const photoTagOps = {
|
||||
/** Get tags for a photo */
|
||||
async getPhotoTags(mediaId: string): Promise<Tag[]> {
|
||||
try {
|
||||
const result = await api.get<Tag[]>(`/photos/${mediaId}/tags`);
|
||||
|
|
@ -99,9 +36,7 @@ export const tagStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add tag to photo (from Photos backend)
|
||||
*/
|
||||
/** Add tag to photo */
|
||||
async addTagToPhoto(mediaId: string, tagId: string) {
|
||||
try {
|
||||
const result = await api.post(`/photos/${mediaId}/tags/${tagId}`);
|
||||
|
|
@ -112,9 +47,7 @@ export const tagStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove tag from photo (from Photos backend)
|
||||
*/
|
||||
/** Remove tag from photo */
|
||||
async removeTagFromPhoto(mediaId: string, tagId: string) {
|
||||
try {
|
||||
const result = await api.delete(`/photos/${mediaId}/tags/${tagId}`);
|
||||
|
|
@ -125,9 +58,7 @@ export const tagStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set all tags for a photo (from Photos backend)
|
||||
*/
|
||||
/** Set all tags for a photo */
|
||||
async setPhotoTags(mediaId: string, tagIds: string[]) {
|
||||
try {
|
||||
const result = await api.patch(`/photos/${mediaId}/tags`, { tagIds });
|
||||
|
|
@ -137,11 +68,4 @@ export const tagStore = {
|
|||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset store
|
||||
*/
|
||||
reset() {
|
||||
sharedTagStore.clear();
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,13 +8,22 @@
|
|||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { photoStore } from '$lib/stores/photos.svelte';
|
||||
import { albumStore } from '$lib/stores/albums.svelte';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { setContext } from 'svelte';
|
||||
import { THEME_DEFINITIONS, DEFAULT_THEME_VARIANTS } from '@manacore/shared-theme';
|
||||
import type { ThemeVariant } from '@manacore/shared-theme';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { photosStore } from '$lib/data/local-store';
|
||||
|
||||
// Live query for shared tags (local-first)
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isDark = $derived(theme.isDark);
|
||||
|
|
@ -81,7 +90,6 @@
|
|||
await authStore.signOut();
|
||||
photoStore.reset();
|
||||
albumStore.reset();
|
||||
tagStore.reset();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +97,7 @@
|
|||
async function handleInputSearch(query: string): Promise<QuickInputItem[]> {
|
||||
const q = query.toLowerCase();
|
||||
const albums = albumStore.albums.filter((a) => a.name?.toLowerCase().includes(q));
|
||||
const tags = tagStore.tags.filter((t) => t.name?.toLowerCase().includes(q));
|
||||
const tags = allTags.value.filter((t) => t.name?.toLowerCase().includes(q));
|
||||
const results: QuickInputItem[] = [];
|
||||
for (const album of albums.slice(0, 5)) {
|
||||
results.push({ id: `album-${album.id}`, title: album.name, subtitle: 'Album' });
|
||||
|
|
@ -111,10 +119,11 @@
|
|||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await photosStore.initialize();
|
||||
await Promise.all([photosStore.initialize(), tagLocalStore.initialize()]);
|
||||
if (authStore.isAuthenticated) {
|
||||
photosStore.startSync(() => authStore.getValidToken());
|
||||
await Promise.all([photoStore.loadStats(), albumStore.loadAlbums(), tagStore.loadTags()]);
|
||||
tagMutations.startSync(() => authStore.getValidToken());
|
||||
await Promise.all([photoStore.loadStats(), albumStore.loadAlbums()]);
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('photos')) {
|
||||
showGuestWelcome = true;
|
||||
|
|
@ -152,7 +161,7 @@
|
|||
<!-- TagStrip (toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#6b7280',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
import { images, selectedImage } from '$lib/stores/images';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import { getImageTags, getAllTags, addTagToImage, removeTagFromImage } from '$lib/api/tags';
|
||||
import { getImageTags, addTagToImage, removeTagFromImage } from '$lib/api/tags';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag as SharedTag } from '@manacore/shared-tags';
|
||||
import {
|
||||
X,
|
||||
Info,
|
||||
|
|
@ -32,16 +34,28 @@
|
|||
|
||||
let { image, onClose }: Props = $props();
|
||||
|
||||
const sharedTags: { value: SharedTag[] } = getContext('tags');
|
||||
|
||||
let isArchiving = $state(false);
|
||||
let isDeleting = $state(false);
|
||||
let imageTags = $state<Tag[]>([]);
|
||||
let showInfo = $state(false);
|
||||
let showTagModal = $state(false);
|
||||
let showPublishModal = $state(false);
|
||||
let allTags = $state<Tag[]>([]);
|
||||
let isLoadingTags = $state(false);
|
||||
let isPublishing = $state(false);
|
||||
|
||||
// Derive allTags from the shared context
|
||||
let allTags = $derived(
|
||||
sharedTags.value.map(
|
||||
(t): Tag => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color,
|
||||
createdAt: t.createdAt,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Get current image index
|
||||
const currentIndex = $derived(image ? $images.findIndex((img) => img.id === image?.id) : -1);
|
||||
|
||||
|
|
@ -157,17 +171,8 @@
|
|||
}).format(date);
|
||||
}
|
||||
|
||||
async function openTagModal() {
|
||||
function openTagModal() {
|
||||
showTagModal = true;
|
||||
isLoadingTags = true;
|
||||
try {
|
||||
allTags = await getAllTags();
|
||||
} catch (error) {
|
||||
console.error('Error loading tags:', error);
|
||||
toastStore.show('Fehler beim Laden der Tags', 'error');
|
||||
} finally {
|
||||
isLoadingTags = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeTagModal() {
|
||||
|
|
@ -481,13 +486,7 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{#if isLoadingTags}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent"
|
||||
></div>
|
||||
</div>
|
||||
{:else if allTags.length === 0}
|
||||
{#if allTags.length === 0}
|
||||
<p class="py-8 text-center text-gray-500 dark:text-gray-400">Keine Tags verfügbar</p>
|
||||
{:else}
|
||||
<div class="max-h-96 space-y-2 overflow-y-auto">
|
||||
|
|
|
|||
|
|
@ -14,8 +14,12 @@
|
|||
hasMoreExplore,
|
||||
showExploreFavoritesOnly,
|
||||
} from '$lib/stores/explore';
|
||||
import { tags, selectedTags } from '$lib/stores/tags';
|
||||
import { selectedTags } from '$lib/stores/tags';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { showFavoritesOnly } from '$lib/stores/images';
|
||||
|
||||
const allTags: { value: Tag[] } = getContext('tags');
|
||||
import { searchPublicImages, getPublicImages } from '$lib/api/explore';
|
||||
import { showKeyboardShortcuts } from '$lib/stores/ui';
|
||||
import TagPills from '$lib/components/tags/TagPills.svelte';
|
||||
|
|
@ -373,7 +377,7 @@
|
|||
<span>Favoriten</span>
|
||||
</button>
|
||||
|
||||
{#if $tags.length > 0}
|
||||
{#if allTags.value.length > 0}
|
||||
<TagPills />
|
||||
{:else}
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Keine Tags vorhanden</p>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { tags, selectedTags } from '$lib/stores/tags';
|
||||
import type { Database } from '@picture/shared/types';
|
||||
import { selectedTags } from '$lib/stores/tags';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { Check } from '@manacore/shared-icons';
|
||||
|
||||
type Tag = Database['public']['Tables']['tags']['Row'];
|
||||
const allTags: { value: Tag[] } = getContext('tags');
|
||||
|
||||
function toggleTag(tagId: string) {
|
||||
selectedTags.update((current) => {
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
</script>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each $tags as tag (tag.id)}
|
||||
{#each allTags.value as tag (tag.id)}
|
||||
{@const selected = isSelected(tag.id)}
|
||||
<button
|
||||
onclick={() => toggleTag(tag.id)}
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
</button>
|
||||
{/each}
|
||||
|
||||
{#if $tags.length === 0}
|
||||
{#if allTags.value.length === 0}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Keine Tags vorhanden. Erstelle Tags in der Tag-Verwaltung.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@
|
|||
showTagSubmenu,
|
||||
hideTagSubmenu,
|
||||
} from '$lib/stores/contextMenu';
|
||||
import { tags } from '$lib/stores/tags';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
const allTags: { value: Tag[] } = getContext('tags');
|
||||
import { addTagToImage, removeTagFromImage, getImageTags } from '$lib/api/tags';
|
||||
import { archiveImage, unarchiveImage, deleteImage, toggleFavorite } from '$lib/api/images';
|
||||
import { images } from '$lib/stores/images';
|
||||
import { archivedImages } from '$lib/stores/archive';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
import type { Tag } from '$lib/api/tags';
|
||||
import {
|
||||
DownloadSimple,
|
||||
Link,
|
||||
|
|
@ -322,7 +324,7 @@
|
|||
onmouseleave={hideTagSubmenu}
|
||||
role="menu"
|
||||
>
|
||||
{#if $tags.length === 0}
|
||||
{#if allTags.value.length === 0}
|
||||
<div class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">Keine Tags vorhanden</div>
|
||||
{:else}
|
||||
<div class="px-3 pb-2 pt-1">
|
||||
|
|
@ -330,7 +332,7 @@
|
|||
Tags hinzufügen/entfernen
|
||||
</p>
|
||||
</div>
|
||||
{#each $tags as tag}
|
||||
{#each allTags.value as tag}
|
||||
{@const hasTag = imageTags.some((t) => t.id === tag.id)}
|
||||
<button
|
||||
onclick={() => {
|
||||
|
|
|
|||
|
|
@ -1,57 +1,16 @@
|
|||
/**
|
||||
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
*
|
||||
* Replaces old Svelte 4 writable stores with createTagStore wrapper.
|
||||
* Exports writable-compatible stores for backward compatibility with existing consumers.
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
*/
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import { createTagStore, type TagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
// Re-export Tag for backward compatibility with '$lib/api/tags' Tag type
|
||||
export type { Tag };
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Create the shared tag store (Svelte 5 runes-based)
|
||||
const sharedTagStore: TagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
|
||||
// Backward-compatible writable stores for existing consumers
|
||||
// These are kept as writables so $tags and $selectedTags syntax still works
|
||||
export const tags = writable<Tag[]>([]);
|
||||
/** UI-only: currently selected tag IDs for filtering (not persisted) */
|
||||
export const selectedTags = writable<string[]>([]);
|
||||
export const isLoadingTags = writable<boolean>(false);
|
||||
|
||||
/**
|
||||
* Fetch tags from the shared store and sync to the writable store.
|
||||
* Call this on mount instead of using the old getAllTags() API function.
|
||||
*/
|
||||
export async function fetchAndSyncTags(): Promise<void> {
|
||||
isLoadingTags.set(true);
|
||||
try {
|
||||
await sharedTagStore.fetchTags();
|
||||
tags.set(sharedTagStore.tags);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch tags:', e);
|
||||
} finally {
|
||||
isLoadingTags.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct access to the shared tag store for components that want the runes-based API.
|
||||
*/
|
||||
export const tagStore = sharedTagStore;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,12 @@
|
|||
import KeyboardShortcutsModal from '$lib/components/ui/KeyboardShortcutsModal.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { isUIVisible, toggleUI, showKeyboardShortcuts } from '$lib/stores/ui';
|
||||
import { tagStore } from '$lib/stores/tags';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { setContext } from 'svelte';
|
||||
import { pictureOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
|
|
@ -32,6 +37,10 @@
|
|||
// App switcher items
|
||||
const appItems = getPillAppItems('picture');
|
||||
|
||||
// Live query for shared tags (local-first)
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// PillNav state
|
||||
|
|
@ -87,12 +96,13 @@
|
|||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database (opens IndexedDB, seeds guest data)
|
||||
await pictureStore.initialize();
|
||||
// Initialize local-first databases (opens IndexedDB, seeds guest data)
|
||||
await Promise.all([pictureStore.initialize(), tagLocalStore.initialize()]);
|
||||
|
||||
// If authenticated, start syncing to server
|
||||
if (authStore.isAuthenticated) {
|
||||
pictureStore.startSync(() => authStore.getValidToken());
|
||||
tagMutations.startSync(() => authStore.getValidToken());
|
||||
}
|
||||
|
||||
// Show guest welcome modal on first visit
|
||||
|
|
@ -101,7 +111,7 @@
|
|||
}
|
||||
|
||||
if (authStore.isAuthenticated) {
|
||||
await Promise.all([userSettings.load(), tagStore.fetchTags()]);
|
||||
await userSettings.load();
|
||||
}
|
||||
|
||||
// Redirect to start page if on /app and a custom start page is set
|
||||
|
|
@ -313,7 +323,7 @@
|
|||
<!-- TagStrip (toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#6b7280',
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@
|
|||
showFavoritesOnly,
|
||||
} from '$lib/stores/images';
|
||||
import { isUIVisible } from '$lib/stores/ui';
|
||||
import { tags, selectedTags } from '$lib/stores/tags';
|
||||
import { imageCollection, imageTagCollection, tagCollection } from '$lib/data/local-store';
|
||||
import { selectedTags } from '$lib/stores/tags';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { imageCollection, imageTagCollection } from '$lib/data/local-store';
|
||||
import type { Image } from '$lib/api/images';
|
||||
import type { LocalImage } from '$lib/data/local-store';
|
||||
import GalleryGrid from '$lib/components/gallery/GalleryGrid.svelte';
|
||||
|
|
@ -23,6 +25,8 @@
|
|||
import { Heart } from '@manacore/shared-icons';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const allTags: { value: Tag[] } = getContext('tags');
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
let loadingMore = $state(false);
|
||||
|
|
@ -59,9 +63,7 @@
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
loadTags().then(() => {
|
||||
loadInitialImages();
|
||||
});
|
||||
loadInitialImages();
|
||||
|
||||
// Setup Intersection Observer for infinite scroll
|
||||
observer = new IntersectionObserver(
|
||||
|
|
@ -85,22 +87,6 @@
|
|||
};
|
||||
});
|
||||
|
||||
async function loadTags() {
|
||||
try {
|
||||
const localTags = await tagCollection.getAll();
|
||||
tags.set(
|
||||
localTags.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color ?? undefined,
|
||||
createdAt: t.createdAt ?? new Date().toISOString(),
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error loading tags:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// React to tag and favorites filter changes
|
||||
$effect(() => {
|
||||
if ($selectedTags || $showFavoritesOnly !== undefined) {
|
||||
|
|
@ -210,7 +196,7 @@
|
|||
</button>
|
||||
|
||||
<!-- Tags -->
|
||||
{#if $tags.length > 0}
|
||||
{#if allTags.value.length > 0}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||
>Tags:</span
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { tags, isLoadingTags } from '$lib/stores/tags';
|
||||
import { getAllTags, createTag, updateTag, deleteTag } from '$lib/api/tags';
|
||||
import type { Tag } from '$lib/api/tags';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { toastStore } from '@manacore/shared-ui';
|
||||
import { PageHeader } from '@manacore/shared-ui';
|
||||
import { Plus, Tag as TagIcon, PencilSimple, Trash } from '@manacore/shared-icons';
|
||||
|
||||
const allTags: { value: Tag[] } = getContext('tags');
|
||||
|
||||
let showCreateModal = $state(false);
|
||||
let showEditModal = $state(false);
|
||||
let editingTag = $state<Tag | null>(null);
|
||||
|
|
@ -26,32 +27,14 @@
|
|||
'#14B8A6', // teal
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
await loadTags();
|
||||
});
|
||||
|
||||
async function loadTags() {
|
||||
isLoadingTags.set(true);
|
||||
try {
|
||||
const data = await getAllTags();
|
||||
tags.set(data);
|
||||
} catch (error) {
|
||||
console.error('Error loading tags:', error);
|
||||
toastStore.show('Fehler beim Laden der Tags', 'error');
|
||||
} finally {
|
||||
isLoadingTags.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateTag() {
|
||||
if (!newTagName.trim()) return;
|
||||
|
||||
try {
|
||||
await createTag({
|
||||
await tagMutations.createTag({
|
||||
name: newTagName.trim(),
|
||||
color: newTagColor,
|
||||
});
|
||||
await loadTags();
|
||||
toastStore.show('Tag erfolgreich erstellt', 'success');
|
||||
newTagName = '';
|
||||
newTagColor = '#3B82F6';
|
||||
|
|
@ -73,11 +56,10 @@
|
|||
if (!editingTag || !editTagName.trim()) return;
|
||||
|
||||
try {
|
||||
await updateTag(editingTag.id, {
|
||||
await tagMutations.updateTag(editingTag.id, {
|
||||
name: editTagName.trim(),
|
||||
color: editTagColor,
|
||||
});
|
||||
await loadTags();
|
||||
toastStore.show('Tag erfolgreich aktualisiert', 'success');
|
||||
showEditModal = false;
|
||||
editingTag = null;
|
||||
|
|
@ -91,8 +73,7 @@
|
|||
if (!confirm('Möchten Sie diesen Tag wirklich löschen?')) return;
|
||||
|
||||
try {
|
||||
await deleteTag(tagId);
|
||||
await loadTags();
|
||||
await tagMutations.deleteTag(tagId);
|
||||
toastStore.show('Tag erfolgreich gelöscht', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error deleting tag:', error);
|
||||
|
|
@ -124,13 +105,7 @@
|
|||
</PageHeader>
|
||||
|
||||
<!-- Tags Grid -->
|
||||
{#if $isLoadingTags}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent dark:border-blue-400"
|
||||
></div>
|
||||
</div>
|
||||
{:else if $tags.length === 0}
|
||||
{#if allTags.value.length === 0}
|
||||
<div
|
||||
class="rounded-3xl border border-gray-200/50 bg-white/80 p-12 text-center backdrop-blur-xl dark:border-gray-700/50 dark:bg-gray-900/80"
|
||||
>
|
||||
|
|
@ -144,7 +119,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each $tags as tag (tag.id)}
|
||||
{#each allTags.value as tag (tag.id)}
|
||||
<div
|
||||
class="group relative rounded-2xl border border-gray-200/50 bg-white/80 p-6 backdrop-blur-xl transition-all hover:shadow-lg dark:border-gray-700/50 dark:bg-gray-900/80"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
import { page } from '$app/stores';
|
||||
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, QuickInputItem, CreatePreview } from '@manacore/shared-ui';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { plantsApi } from '$lib/api/plants';
|
||||
|
|
@ -14,10 +18,14 @@
|
|||
} from '$lib/utils/plant-parser';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { setContext } from 'svelte';
|
||||
import { plantaStore } from '$lib/data/local-store';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
// TagStrip visibility
|
||||
|
|
@ -101,10 +109,11 @@
|
|||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
await plantaStore.initialize();
|
||||
await Promise.all([plantaStore.initialize(), tagLocalStore.initialize()]);
|
||||
if (authStore.isAuthenticated) {
|
||||
plantaStore.startSync(() => authStore.getValidToken());
|
||||
await tagStore.fetchTags();
|
||||
const getToken = () => authStore.getValidToken();
|
||||
plantaStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('planta')) {
|
||||
showGuestWelcome = true;
|
||||
|
|
@ -134,7 +143,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -143,7 +152,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +16,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => auth.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { setContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, QuickInputBar, TagStrip } from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
|
|
@ -23,6 +28,10 @@
|
|||
// App switcher items
|
||||
const appItems = getPillAppItems('presi');
|
||||
|
||||
// Shared tag store (local-first)
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isCollapsed = $state(false);
|
||||
|
|
@ -143,12 +152,14 @@
|
|||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database (opens IndexedDB, seeds guest data)
|
||||
await presiStore.initialize();
|
||||
// Initialize local-first databases (opens IndexedDB, seeds guest data)
|
||||
await Promise.all([presiStore.initialize(), tagLocalStore.initialize()]);
|
||||
|
||||
// If authenticated, start syncing to server
|
||||
if (auth.isAuthenticated) {
|
||||
presiStore.startSync(() => auth.getValidToken());
|
||||
const getToken = () => auth.getValidToken();
|
||||
presiStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
|
||||
// Load decks from IndexedDB (guest seed or synced data)
|
||||
|
|
@ -160,9 +171,8 @@
|
|||
}
|
||||
|
||||
if (auth.isAuthenticated) {
|
||||
// Load user settings and tags (require auth)
|
||||
// Load user settings (requires auth)
|
||||
await userSettings.load();
|
||||
await tagStore.fetchTags();
|
||||
}
|
||||
|
||||
// Redirect to start page if on root and a custom start page is set
|
||||
|
|
@ -228,7 +238,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -237,7 +247,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +17,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, setContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
|
|
@ -19,10 +19,17 @@
|
|||
CreatePreview,
|
||||
} from '@manacore/shared-ui';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
// App switcher items
|
||||
const appItems = getPillAppItems('questions');
|
||||
|
||||
|
|
@ -47,14 +54,15 @@
|
|||
let showGuestWelcome = $state(false);
|
||||
|
||||
async function handleAuthReady() {
|
||||
await questionsAppStore.initialize();
|
||||
await Promise.all([questionsAppStore.initialize(), tagLocalStore.initialize()]);
|
||||
if (authStore.isAuthenticated) {
|
||||
questionsAppStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
questionsAppStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
const token = await authStore.getValidToken();
|
||||
apiClient.setAccessToken(token);
|
||||
await collectionsStore.load();
|
||||
await questionsStore.load();
|
||||
await tagStore.fetchTags();
|
||||
}
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('questions')) {
|
||||
showGuestWelcome = true;
|
||||
|
|
@ -202,7 +210,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -211,7 +219,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +16,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -1,62 +1,11 @@
|
|||
/**
|
||||
* Tags Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore, type TagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
// Re-export Tag for convenience
|
||||
export type { Tag };
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Create the shared tag store
|
||||
const tagStore: TagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
|
||||
// Export the store with a backward-compatible wrapper
|
||||
export const tagsStore = {
|
||||
get tags() {
|
||||
return tagStore.tags;
|
||||
},
|
||||
get loading() {
|
||||
return tagStore.loading;
|
||||
},
|
||||
get error() {
|
||||
return tagStore.error;
|
||||
},
|
||||
|
||||
async fetchTags() {
|
||||
return tagStore.fetchTags();
|
||||
},
|
||||
getById(id: string) {
|
||||
return tagStore.getById(id);
|
||||
},
|
||||
getColor(tagId: string) {
|
||||
return tagStore.getColor(tagId);
|
||||
},
|
||||
|
||||
async createTag(data: { name: string; color?: string }) {
|
||||
return tagStore.createTag(data);
|
||||
},
|
||||
async updateTag(id: string, data: { name?: string; color?: string }) {
|
||||
return tagStore.updateTag(id, data);
|
||||
},
|
||||
async deleteTag(id: string) {
|
||||
return tagStore.deleteTag(id);
|
||||
},
|
||||
clear() {
|
||||
tagStore.clear();
|
||||
},
|
||||
};
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@
|
|||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { tagsStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { setContext } from 'svelte';
|
||||
import { THEME_DEFINITIONS } from '@manacore/shared-theme';
|
||||
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
|
|
@ -27,6 +32,10 @@
|
|||
// App switcher items
|
||||
const appItems = getPillAppItems('storage');
|
||||
|
||||
// Live query for shared tags (local-first)
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let isCollapsed = $state(false);
|
||||
|
|
@ -165,12 +174,13 @@
|
|||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database
|
||||
await storageStore.initialize();
|
||||
// Initialize local-first databases
|
||||
await Promise.all([storageStore.initialize(), tagLocalStore.initialize()]);
|
||||
|
||||
// If authenticated, start syncing
|
||||
if (authStore.isAuthenticated) {
|
||||
storageStore.startSync(() => authStore.getValidToken());
|
||||
tagMutations.startSync(() => authStore.getValidToken());
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
|
|
@ -182,8 +192,8 @@
|
|||
}
|
||||
|
||||
if (authStore.isAuthenticated) {
|
||||
// Load user settings and tags (require auth)
|
||||
await Promise.all([userSettings.load(), tagsStore.fetchTags()]);
|
||||
// Load user settings (require auth)
|
||||
await userSettings.load();
|
||||
}
|
||||
|
||||
// Initialize collapsed state from localStorage
|
||||
|
|
@ -248,7 +258,7 @@
|
|||
<!-- TagStrip (toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagsStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#6b7280',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { tagsStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { toastStore, PageHeader } from '@manacore/shared-ui';
|
||||
import { Plus, Tag as TagIcon, PencilSimple, Trash } from '@manacore/shared-icons';
|
||||
|
||||
const allTags: { value: Tag[] } = getContext('tags');
|
||||
|
||||
let showCreateModal = $state(false);
|
||||
let showEditModal = $state(false);
|
||||
let editingTag = $state<{ id: string; name: string; color: string } | null>(null);
|
||||
|
|
@ -23,17 +26,11 @@
|
|||
'#14B8A6',
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
if (tagsStore.tags.length === 0) {
|
||||
await tagsStore.fetchTags();
|
||||
}
|
||||
});
|
||||
|
||||
async function handleCreateTag() {
|
||||
if (!newTagName.trim()) return;
|
||||
|
||||
try {
|
||||
await tagsStore.createTag({
|
||||
await tagMutations.createTag({
|
||||
name: newTagName.trim(),
|
||||
color: newTagColor,
|
||||
});
|
||||
|
|
@ -58,7 +55,7 @@
|
|||
if (!editingTag || !editTagName.trim()) return;
|
||||
|
||||
try {
|
||||
await tagsStore.updateTag(editingTag.id, {
|
||||
await tagMutations.updateTag(editingTag.id, {
|
||||
name: editTagName.trim(),
|
||||
color: editTagColor,
|
||||
});
|
||||
|
|
@ -75,7 +72,7 @@
|
|||
if (!confirm('Möchten Sie diesen Tag wirklich löschen?')) return;
|
||||
|
||||
try {
|
||||
await tagsStore.deleteTag(tagId);
|
||||
await tagMutations.deleteTag(tagId);
|
||||
toastStore.show('Tag erfolgreich gelöscht', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error deleting tag:', error);
|
||||
|
|
@ -107,13 +104,7 @@
|
|||
</PageHeader>
|
||||
|
||||
<!-- Tags Grid -->
|
||||
{#if tagsStore.loading}
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div
|
||||
class="h-8 w-8 animate-spin rounded-full border-4 border-solid border-blue-600 border-r-transparent dark:border-blue-400"
|
||||
></div>
|
||||
</div>
|
||||
{:else if tagsStore.tags.length === 0}
|
||||
{#if allTags.value.length === 0}
|
||||
<div
|
||||
class="rounded-3xl border border-gray-200/50 bg-white/80 p-12 text-center backdrop-blur-xl dark:border-gray-700/50 dark:bg-gray-900/80"
|
||||
>
|
||||
|
|
@ -127,7 +118,7 @@
|
|||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each tagsStore.tags as tag (tag.id)}
|
||||
{#each allTags.value as tag (tag.id)}
|
||||
<div
|
||||
class="group relative rounded-2xl border border-gray-200/50 bg-white/80 p-6 backdrop-blur-xl transition-all hover:shadow-lg dark:border-gray-700/50 dark:bg-gray-900/80"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
export { apiClient } from './client';
|
||||
export * from './projects';
|
||||
export * from './tasks';
|
||||
export * from './labels';
|
||||
export * from './reminders';
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
/**
|
||||
* Labels API - Uses central Tags API from mana-core-auth
|
||||
*
|
||||
* This module wraps the central Tags API to provide backward-compatible
|
||||
* "labels" interface for the Todo app. Tags and Labels are now unified
|
||||
* across all Manacore apps.
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import {
|
||||
createTagsClient,
|
||||
type Tag,
|
||||
type CreateTagInput,
|
||||
type UpdateTagInput,
|
||||
} from '@manacore/shared-tags';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
// Re-export Tag as Label for backward compatibility
|
||||
export type Label = Tag;
|
||||
|
||||
// Get auth URL dynamically at runtime
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Lazy-initialized client
|
||||
let _tagsClient: ReturnType<typeof createTagsClient> | null = null;
|
||||
|
||||
function getTagsClient() {
|
||||
if (!browser) return null;
|
||||
if (!_tagsClient) {
|
||||
_tagsClient = createTagsClient({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: async () => {
|
||||
const token = await authStore.getAccessToken();
|
||||
return token || '';
|
||||
},
|
||||
});
|
||||
}
|
||||
return _tagsClient;
|
||||
}
|
||||
|
||||
export async function getLabels(): Promise<Label[]> {
|
||||
const client = getTagsClient();
|
||||
if (!client) return [];
|
||||
return client.getAll();
|
||||
}
|
||||
|
||||
export async function createLabel(data: CreateTagInput): Promise<Label> {
|
||||
const client = getTagsClient();
|
||||
if (!client) throw new Error('Tags client not available');
|
||||
return client.create(data);
|
||||
}
|
||||
|
||||
export async function updateLabel(id: string, data: UpdateTagInput): Promise<Label> {
|
||||
const client = getTagsClient();
|
||||
if (!client) throw new Error('Tags client not available');
|
||||
return client.update(id, data);
|
||||
}
|
||||
|
||||
export async function deleteLabel(id: string): Promise<void> {
|
||||
const client = getTagsClient();
|
||||
if (!client) throw new Error('Tags client not available');
|
||||
await client.delete(id);
|
||||
}
|
||||
|
||||
export async function createDefaultLabels(): Promise<Label[]> {
|
||||
const client = getTagsClient();
|
||||
if (!client) return [];
|
||||
return client.createDefaults();
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { apiClient } from './client';
|
||||
import type { Project } from '@todo/shared';
|
||||
|
||||
interface CreateProjectDto {
|
||||
name: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface UpdateProjectDto {
|
||||
name?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
isArchived?: boolean;
|
||||
}
|
||||
|
||||
interface ReorderProjectsDto {
|
||||
projectIds: string[];
|
||||
}
|
||||
|
||||
interface ProjectsResponse {
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
interface ProjectResponse {
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export async function getProjects(): Promise<Project[]> {
|
||||
const response = await apiClient.get<ProjectsResponse>('/api/v1/projects');
|
||||
return response.projects;
|
||||
}
|
||||
|
||||
export async function getProject(id: string): Promise<Project> {
|
||||
const response = await apiClient.get<ProjectResponse>(`/api/v1/projects/${id}`);
|
||||
return response.project;
|
||||
}
|
||||
|
||||
export async function createProject(data: CreateProjectDto): Promise<Project> {
|
||||
const response = await apiClient.post<ProjectResponse>('/api/v1/projects', data);
|
||||
return response.project;
|
||||
}
|
||||
|
||||
export async function updateProject(id: string, data: UpdateProjectDto): Promise<Project> {
|
||||
const response = await apiClient.put<ProjectResponse>(`/api/v1/projects/${id}`, data);
|
||||
return response.project;
|
||||
}
|
||||
|
||||
export async function deleteProject(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/v1/projects/${id}`);
|
||||
}
|
||||
|
||||
export async function archiveProject(id: string): Promise<Project> {
|
||||
const response = await apiClient.post<ProjectResponse>(`/api/v1/projects/${id}/archive`);
|
||||
return response.project;
|
||||
}
|
||||
|
||||
export async function reorderProjects(projectIds: string[]): Promise<void> {
|
||||
await apiClient.put('/api/v1/projects/reorder', { projectIds } as ReorderProjectsDto);
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { apiClient } from './client';
|
||||
import type { Reminder, ReminderType } from '@todo/shared';
|
||||
|
||||
interface CreateReminderDto {
|
||||
minutesBefore: number;
|
||||
type?: ReminderType;
|
||||
}
|
||||
|
||||
interface RemindersResponse {
|
||||
reminders: Reminder[];
|
||||
}
|
||||
|
||||
interface ReminderResponse {
|
||||
reminder: Reminder;
|
||||
}
|
||||
|
||||
export async function getReminders(taskId: string): Promise<Reminder[]> {
|
||||
const response = await apiClient.get<RemindersResponse>(`/api/v1/tasks/${taskId}/reminders`);
|
||||
return response.reminders;
|
||||
}
|
||||
|
||||
export async function createReminder(taskId: string, data: CreateReminderDto): Promise<Reminder> {
|
||||
const response = await apiClient.post<ReminderResponse>(
|
||||
`/api/v1/tasks/${taskId}/reminders`,
|
||||
data
|
||||
);
|
||||
return response.reminder;
|
||||
}
|
||||
|
||||
export async function deleteReminder(id: string): Promise<void> {
|
||||
await apiClient.delete(`/api/v1/reminders/${id}`);
|
||||
}
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects, getProjectById } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { PRIORITY_OPTIONS } from '@todo/shared';
|
||||
import { format, addDays } from 'date-fns';
|
||||
|
|
@ -33,7 +37,7 @@
|
|||
// Derived values
|
||||
let currentPriority = $derived(PRIORITY_OPTIONS.find((p) => p.value === selectedPriority)!);
|
||||
let selectedProject = $derived(
|
||||
selectedProjectId ? projectsStore.getById(selectedProjectId) : undefined
|
||||
selectedProjectId ? getProjectById(projectsCtx.value, selectedProjectId) : undefined
|
||||
);
|
||||
let dateLabel = $derived(() => {
|
||||
const today = new Date();
|
||||
|
|
@ -282,7 +286,7 @@
|
|||
<span class="project-dot" style="background-color: #6b7280"></span>
|
||||
Kein Projekt
|
||||
</button>
|
||||
{#each projectsStore.activeProjects as project}
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { labelsStore } from '$lib/stores/labels.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { DotsThree, Plus, X } from '@manacore/shared-icons';
|
||||
import TagStripModal from './TagStripModal.svelte';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
interface Props {
|
||||
/** Whether the filter strip below is visible (affects vertical position) */
|
||||
filterStripVisible?: boolean;
|
||||
|
|
@ -39,16 +41,10 @@
|
|||
}
|
||||
|
||||
const sortedTags = $derived.by(() => {
|
||||
return [...labelsStore.labels].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
return [...tagsCtx.value].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
});
|
||||
|
||||
const hasTags = $derived(labelsStore.labels.length > 0);
|
||||
|
||||
onMount(async () => {
|
||||
if (labelsStore.labels.length === 0) {
|
||||
await labelsStore.fetchLabels();
|
||||
}
|
||||
});
|
||||
const hasTags = $derived(tagsCtx.value.length > 0);
|
||||
</script>
|
||||
|
||||
<div class="tag-strip-wrapper" class:above-filter-strip={filterStripVisible}>
|
||||
|
|
@ -71,9 +67,7 @@
|
|||
<span class="tag-name">Alle Tags</span>
|
||||
</button>
|
||||
|
||||
{#if labelsStore.loading}
|
||||
<div class="loading-state">Lädt...</div>
|
||||
{:else if !hasTags}
|
||||
{#if !hasTags}
|
||||
<button class="empty-state glass-tag" onclick={() => goto('/tags')}>
|
||||
<span>Keine Tags vorhanden</span>
|
||||
<span class="add-hint">+ Erstellen</span>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { labelsStore } from '$lib/stores/labels.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import { Plus, X, Check, Pencil, Trash, MagnifyingGlass } from '@manacore/shared-icons';
|
||||
import { TagColorPicker, focusTrap } from '@manacore/shared-ui';
|
||||
import type { Label } from '$lib/api/labels';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
|
|
@ -21,14 +24,14 @@
|
|||
let isCreatingTag = $state(false);
|
||||
|
||||
// Edit tag state
|
||||
let editingTag = $state<Label | null>(null);
|
||||
let editingTag = $state<Tag | null>(null);
|
||||
let editTagName = $state('');
|
||||
let editTagColor = $state('#8b5cf6');
|
||||
let isSavingTag = $state(false);
|
||||
|
||||
// Filtered and sorted tags
|
||||
const sortedTags = $derived.by(() => {
|
||||
const tags = [...labelsStore.labels].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
const tags = [...tagsCtx.value].sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
||||
if (!searchQuery.trim()) return tags;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return tags.filter((t) => t.name.toLowerCase().includes(query));
|
||||
|
|
@ -52,7 +55,7 @@
|
|||
|
||||
isCreatingTag = true;
|
||||
try {
|
||||
await labelsStore.createLabel({
|
||||
await tagMutations.createTag({
|
||||
name: newTagName.trim(),
|
||||
color: newTagColor,
|
||||
});
|
||||
|
|
@ -74,7 +77,7 @@
|
|||
}
|
||||
|
||||
// ==================== EDIT TAG ====================
|
||||
function openEditTag(tag: Label) {
|
||||
function openEditTag(tag: Tag) {
|
||||
editingTag = tag;
|
||||
editTagName = tag.name;
|
||||
editTagColor = tag.color;
|
||||
|
|
@ -91,7 +94,7 @@
|
|||
|
||||
isSavingTag = true;
|
||||
try {
|
||||
await labelsStore.updateLabel(editingTag.id, {
|
||||
await tagMutations.updateTag(editingTag.id, {
|
||||
name: editTagName.trim(),
|
||||
color: editTagColor,
|
||||
});
|
||||
|
|
@ -106,7 +109,7 @@
|
|||
if (!editingTag) return;
|
||||
|
||||
try {
|
||||
await labelsStore.deleteLabel(editingTag.id);
|
||||
await tagMutations.deleteTag(editingTag.id);
|
||||
closeEditTag();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete tag:', e);
|
||||
|
|
@ -164,9 +167,7 @@
|
|||
|
||||
<!-- Content -->
|
||||
<div class="modal-content">
|
||||
{#if labelsStore.loading}
|
||||
<div class="loading-state">Lädt...</div>
|
||||
{:else if labelsStore.labels.length === 0 && !showNewTagForm}
|
||||
{#if tagsCtx.value.length === 0 && !showNewTagForm}
|
||||
<div class="empty-state">
|
||||
<p>Keine Tags vorhanden</p>
|
||||
<button class="create-btn" onclick={openNewTagForm}>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@
|
|||
} from '@todo/shared';
|
||||
import type { ContactReference, ContactOrManual } from '@manacore/shared-types';
|
||||
import { STATUS_OPTIONS, RECURRENCE_OPTIONS } from '@todo/shared';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import { format } from 'date-fns';
|
||||
import SubtaskList from './SubtaskList.svelte';
|
||||
|
|
@ -288,7 +292,7 @@
|
|||
<label class="form-label" for="task-project">Projekt</label>
|
||||
<select id="task-project" class="form-select" bind:value={projectId}>
|
||||
<option value={null}>Kein Projekt</option>
|
||||
{#each projectsStore.activeProjects as project}
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import { labelsStore } from '$lib/stores/labels.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
import type { SortBy, SortOrder } from '$lib/stores/view.svelte';
|
||||
import { X, DotsThree } from '@manacore/shared-icons';
|
||||
|
||||
|
|
@ -215,7 +221,7 @@
|
|||
onchange={(e) => onProjectChange(e.currentTarget.value || null)}
|
||||
>
|
||||
<option value="">Alle Projekte</option>
|
||||
{#each projectsStore.activeProjects as project}
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
|
@ -325,7 +331,7 @@
|
|||
onchange={(e) => onProjectChange(e.currentTarget.value || null)}
|
||||
>
|
||||
<option value="">Alle Projekte</option>
|
||||
{#each projectsStore.activeProjects as project}
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
|
@ -346,7 +352,7 @@
|
|||
{#if selectedLabelIds.length > 0}
|
||||
<div class="flex items-center gap-1">
|
||||
{#each selectedLabelIds.slice(0, 3) as labelId}
|
||||
{@const label = labelsStore.labels.find((l) => l.id === labelId)}
|
||||
{@const label = tagsCtx.value.find((l) => l.id === labelId)}
|
||||
{#if label}
|
||||
<div
|
||||
class="w-3 h-3 rounded-full ring-2 ring-background"
|
||||
|
|
@ -385,11 +391,11 @@
|
|||
<div
|
||||
class="absolute top-full left-0 mt-2 z-50 min-w-[220px] bg-popover border border-border rounded-xl shadow-lg p-2 animate-in fade-in slide-in-from-top-2 duration-150"
|
||||
>
|
||||
{#if labelsStore.labels.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p class="text-sm text-muted-foreground p-3 text-center">Keine Tags vorhanden</p>
|
||||
{:else}
|
||||
<div class="max-h-[200px] overflow-y-auto">
|
||||
{#each labelsStore.labels as label}
|
||||
{#each tagsCtx.value as label}
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-muted/50 transition-colors"
|
||||
onclick={() => toggleLabel(label.id)}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@
|
|||
import { STATUS_OPTIONS, RECURRENCE_OPTIONS } from '@todo/shared';
|
||||
import { format, isToday, isPast, isTomorrow } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects, getProjectColor } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||
import { ContactAvatar, ContactSelector } from '@manacore/shared-ui';
|
||||
import SubtaskList from './SubtaskList.svelte';
|
||||
|
|
@ -298,7 +302,7 @@
|
|||
// Get project color
|
||||
let projectColor = $derived(() => {
|
||||
if (!task.projectId) return null;
|
||||
return projectsStore.getColor(task.projectId);
|
||||
return getProjectColor(projectsCtx.value, task.projectId);
|
||||
});
|
||||
|
||||
// Subtasks progress
|
||||
|
|
@ -566,7 +570,7 @@
|
|||
<label class="form-label" for="task-project-{task.id}">Projekt</label>
|
||||
<select id="task-project-{task.id}" class="form-select" bind:value={projectId}>
|
||||
<option value={null}>Kein Projekt</option>
|
||||
{#each projectsStore.activeProjects as project}
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@
|
|||
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
import type { Task, UpdateTaskInput } from '@todo/shared';
|
||||
import TaskItem from './TaskItem.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
|
||||
|
||||
// Context menu state
|
||||
|
|
@ -64,7 +68,7 @@
|
|||
];
|
||||
|
||||
// Add project move options if there are projects
|
||||
const projects = projectsStore.activeProjects;
|
||||
const projects = getActiveProjects(projectsCtx.value);
|
||||
if (projects.length > 0) {
|
||||
items.push({ id: 'divider-2', label: '', type: 'divider' });
|
||||
items.push({
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { PRIORITY_OPTIONS } from '@todo/shared';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import { labelsStore } from '$lib/stores/labels.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
import { getActiveProjects } from '$lib/data/task-queries';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import { viewStore, type SortBy } from '$lib/stores/view.svelte';
|
||||
import { PillToolbarButton, PillToolbarDivider, PillViewSwitcher } from '@manacore/shared-ui';
|
||||
|
||||
|
|
@ -144,7 +147,7 @@
|
|||
onchange={(e) => (selectedProjectFilter = e.currentTarget.value || null)}
|
||||
>
|
||||
<option value="">Alle Projekte</option>
|
||||
{#each projectsStore.activeProjects as project}
|
||||
{#each getActiveProjects(projectsCtx.value) as project}
|
||||
<option value={project.id}>{project.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { labelsStore } from '$lib/stores/labels.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
interface Props {
|
||||
selectedIds: string[];
|
||||
|
|
@ -37,7 +40,7 @@
|
|||
{:else}
|
||||
<div class="selected-tags">
|
||||
{#each selectedIds.slice(0, 3) as tagId}
|
||||
{@const tag = labelsStore.getById(tagId)}
|
||||
{@const tag = tagsCtx.value.find((t) => t.id === tagId)}
|
||||
{#if tag}
|
||||
<span class="tag-chip" style="--tag-color: {tag.color}">
|
||||
{tag.name}
|
||||
|
|
@ -56,7 +59,7 @@
|
|||
|
||||
{#if showDropdown}
|
||||
<div class="tag-dropdown" onclick={(e) => e.stopPropagation()} role="listbox">
|
||||
{#each labelsStore.labels as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<button
|
||||
type="button"
|
||||
class="tag-option"
|
||||
|
|
@ -79,7 +82,7 @@
|
|||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{#if labelsStore.labels.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<div class="no-tags">Keine Tags vorhanden</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
154
apps/todo/apps/web/src/lib/data/task-queries.ts
Normal file
154
apps/todo/apps/web/src/lib/data/task-queries.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Reactive Queries & Pure Filter Helpers for Todo
|
||||
*
|
||||
* 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 {
|
||||
taskCollection,
|
||||
projectCollection,
|
||||
type LocalTask,
|
||||
type LocalProject,
|
||||
} from './local-store';
|
||||
import type { Task, Project } from '@todo/shared';
|
||||
import { isToday, isPast, isFuture, startOfDay, addDays } from 'date-fns';
|
||||
|
||||
// ─── Type Converters ───────────────────────────────────────
|
||||
|
||||
export function toTask(local: LocalTask): Task {
|
||||
return {
|
||||
id: local.id,
|
||||
projectId: local.projectId,
|
||||
userId: local.userId ?? 'guest',
|
||||
title: local.title,
|
||||
description: local.description,
|
||||
dueDate: local.dueDate,
|
||||
scheduledDate: local.scheduledDate,
|
||||
scheduledStartTime: local.scheduledStartTime,
|
||||
estimatedDuration: local.estimatedDuration,
|
||||
priority: local.priority,
|
||||
status: local.isCompleted ? 'completed' : 'pending',
|
||||
isCompleted: local.isCompleted,
|
||||
completedAt: local.completedAt,
|
||||
order: local.order,
|
||||
recurrenceRule: local.recurrenceRule,
|
||||
subtasks: local.subtasks ?? null,
|
||||
metadata: local.metadata as Task['metadata'],
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function toProject(local: LocalProject): Project {
|
||||
return {
|
||||
id: local.id,
|
||||
userId: local.userId ?? 'guest',
|
||||
name: local.name,
|
||||
color: local.color,
|
||||
icon: local.icon,
|
||||
order: local.order,
|
||||
isArchived: local.isArchived,
|
||||
isDefault: local.isDefault,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Live Query Hooks (call during component init) ─────────
|
||||
|
||||
/** All tasks, sorted by order. Auto-updates on any change. */
|
||||
export function useAllTasks() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await taskCollection.getAll(undefined, {
|
||||
sortBy: 'order',
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
return locals.map(toTask);
|
||||
}, [] as Task[]);
|
||||
}
|
||||
|
||||
/** All projects, sorted by order. Auto-updates on any change. */
|
||||
export function useAllProjects() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await projectCollection.getAll(undefined, {
|
||||
sortBy: 'order',
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
return locals.map(toProject);
|
||||
}, [] as Project[]);
|
||||
}
|
||||
|
||||
// ─── Pure Filter Functions (for $derived) ──────────────────
|
||||
|
||||
export function filterIncomplete(tasks: Task[]): Task[] {
|
||||
return tasks.filter((t) => !t.isCompleted);
|
||||
}
|
||||
|
||||
export function filterCompleted(tasks: Task[]): Task[] {
|
||||
return tasks.filter((t) => t.isCompleted);
|
||||
}
|
||||
|
||||
export function filterOverdue(tasks: Task[]): Task[] {
|
||||
return tasks.filter((t) => {
|
||||
if (!t.dueDate || t.isCompleted) return false;
|
||||
const dueDate = new Date(t.dueDate);
|
||||
return isPast(startOfDay(dueDate)) && !isToday(dueDate);
|
||||
});
|
||||
}
|
||||
|
||||
export function filterToday(tasks: Task[]): Task[] {
|
||||
const today = startOfDay(new Date());
|
||||
return tasks.filter((t) => {
|
||||
if (t.isCompleted) return false;
|
||||
if (!t.dueDate) return true;
|
||||
const taskDate = startOfDay(new Date(t.dueDate));
|
||||
return taskDate.getTime() === today.getTime();
|
||||
});
|
||||
}
|
||||
|
||||
export function filterUpcoming(tasks: Task[]): Task[] {
|
||||
const today = startOfDay(new Date());
|
||||
const weekFromNow = addDays(today, 7);
|
||||
return tasks.filter((t) => {
|
||||
if (!t.dueDate || t.isCompleted) return false;
|
||||
const dueDate = new Date(t.dueDate);
|
||||
return isFuture(dueDate) && dueDate <= weekFromNow;
|
||||
});
|
||||
}
|
||||
|
||||
export function filterByProject(tasks: Task[], projectId: string | null): Task[] {
|
||||
if (projectId === null) {
|
||||
return tasks.filter((t) => !t.projectId);
|
||||
}
|
||||
return tasks.filter((t) => t.projectId === projectId);
|
||||
}
|
||||
|
||||
export function filterByLabel(tasks: Task[], labelId: string): Task[] {
|
||||
return tasks.filter((t) => t.labels?.some((l) => l.id === labelId));
|
||||
}
|
||||
|
||||
// ─── Pure Project Helpers ──────────────────────────────────
|
||||
|
||||
export function getActiveProjects(projects: Project[]): Project[] {
|
||||
return projects.filter((p) => !p.isArchived).sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
export function getArchivedProjects(projects: Project[]): Project[] {
|
||||
return projects.filter((p) => p.isArchived);
|
||||
}
|
||||
|
||||
export function getInboxProject(projects: Project[]): Project | undefined {
|
||||
return projects.find((p) => p.isDefault);
|
||||
}
|
||||
|
||||
export function getProjectById(projects: Project[], id: string): Project | undefined {
|
||||
return projects.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
export function getProjectColor(projects: Project[], projectId: string): string {
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
return project?.color || '#6b7280';
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
export { authStore } from './auth.svelte';
|
||||
export { projectsStore } from './projects.svelte';
|
||||
export { tasksStore } from './tasks.svelte';
|
||||
export { labelsStore } from './labels.svelte';
|
||||
export { viewStore } from './view.svelte';
|
||||
export type { ViewType, SortBy, SortOrder } from './view.svelte';
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
/**
|
||||
* Labels Store - Uses shared Tag Store backed by central mana-core-auth
|
||||
*/
|
||||
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore, type TagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
// Re-export Tag as Label for backward compatibility
|
||||
export type Label = Tag;
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
// Create the shared tag store
|
||||
const tagStore: TagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
|
||||
// Backward-compatible labelsStore wrapper
|
||||
export const labelsStore = {
|
||||
get labels() {
|
||||
return tagStore.tags;
|
||||
},
|
||||
get loading() {
|
||||
return tagStore.loading;
|
||||
},
|
||||
get error() {
|
||||
return tagStore.error;
|
||||
},
|
||||
|
||||
async fetchLabels() {
|
||||
return tagStore.fetchTags();
|
||||
},
|
||||
getById(id: string) {
|
||||
return tagStore.getById(id);
|
||||
},
|
||||
getColor(labelId: string) {
|
||||
return tagStore.getColor(labelId);
|
||||
},
|
||||
|
||||
async createLabel(data: { name: string; color?: string }) {
|
||||
return tagStore.createTag(data);
|
||||
},
|
||||
async updateLabel(id: string, data: { name?: string; color?: string }) {
|
||||
return tagStore.updateTag(id, data);
|
||||
},
|
||||
async deleteLabel(id: string) {
|
||||
return tagStore.deleteTag(id);
|
||||
},
|
||||
clear() {
|
||||
tagStore.clear();
|
||||
},
|
||||
};
|
||||
|
|
@ -1,112 +1,44 @@
|
|||
/**
|
||||
* Projects Store — Local-First with Dexie.js
|
||||
* Projects Store — Mutation-Only Service
|
||||
*
|
||||
* All reads and writes go to IndexedDB first.
|
||||
* Same public API as before so components don't need changes.
|
||||
* All reads are handled by useLiveQuery() hooks in task-queries.ts.
|
||||
* This store only provides write operations (create, update, delete, etc.).
|
||||
* IndexedDB writes automatically trigger UI updates via Dexie liveQuery.
|
||||
*/
|
||||
|
||||
import type { Project } from '@todo/shared';
|
||||
import { projectCollection, type LocalProject } from '$lib/data/local-store';
|
||||
import { toProject } from '$lib/data/task-queries';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
// State
|
||||
let projects = $state<Project[]>([]);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
/** Convert a LocalProject (IndexedDB) to the shared Project type. */
|
||||
function toProject(local: LocalProject): Project {
|
||||
return {
|
||||
id: local.id,
|
||||
userId: local.userId ?? 'guest',
|
||||
name: local.name,
|
||||
color: local.color,
|
||||
icon: local.icon,
|
||||
order: local.order,
|
||||
isArchived: local.isArchived,
|
||||
isDefault: local.isDefault,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export const projectsStore = {
|
||||
get projects() {
|
||||
return projects;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
get inboxProject() {
|
||||
return projects.find((p) => p.isDefault);
|
||||
},
|
||||
|
||||
get activeProjects() {
|
||||
return projects.filter((p) => !p.isArchived).sort((a, b) => a.order - b.order);
|
||||
},
|
||||
|
||||
get archivedProjects() {
|
||||
return projects.filter((p) => p.isArchived);
|
||||
},
|
||||
|
||||
/**
|
||||
* Load projects from IndexedDB.
|
||||
*/
|
||||
async fetchProjects() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const localProjects = await projectCollection.getAll(undefined, {
|
||||
sortBy: 'order',
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
projects = localProjects.map(toProject);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch projects';
|
||||
console.error('Failed to fetch projects:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
getById(id: string): Project | undefined {
|
||||
return projects.find((p) => p.id === id);
|
||||
},
|
||||
|
||||
getColor(projectId: string): string {
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
return project?.color || '#6b7280';
|
||||
},
|
||||
|
||||
async createProject(data: { name: string; description?: string; color?: string; icon?: string }) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const count = await projectCollection.count();
|
||||
const newLocal: LocalProject = {
|
||||
id: crypto.randomUUID(),
|
||||
name: data.name,
|
||||
color: data.color ?? '#6b7280',
|
||||
icon: data.icon ?? null,
|
||||
order: projects.length,
|
||||
order: count,
|
||||
isArchived: false,
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
const inserted = await projectCollection.insert(newLocal);
|
||||
const newProject = toProject(inserted);
|
||||
projects = [...projects, newProject];
|
||||
TodoEvents.projectCreated();
|
||||
return newProject;
|
||||
return toProject(inserted);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create project';
|
||||
console.error('Failed to create project:', e);
|
||||
throw e;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -118,9 +50,7 @@ export const projectsStore = {
|
|||
try {
|
||||
const updated = await projectCollection.update(id, data as Partial<LocalProject>);
|
||||
if (updated) {
|
||||
const updatedProject = toProject(updated);
|
||||
projects = projects.map((p) => (p.id === id ? updatedProject : p));
|
||||
return updatedProject;
|
||||
return toProject(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update project';
|
||||
|
|
@ -133,7 +63,6 @@ export const projectsStore = {
|
|||
error = null;
|
||||
try {
|
||||
await projectCollection.delete(id);
|
||||
projects = projects.filter((p) => p.id !== id);
|
||||
TodoEvents.projectDeleted();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete project';
|
||||
|
|
@ -149,9 +78,7 @@ export const projectsStore = {
|
|||
isArchived: true,
|
||||
} as Partial<LocalProject>);
|
||||
if (updated) {
|
||||
const archivedProject = toProject(updated);
|
||||
projects = projects.map((p) => (p.id === id ? archivedProject : p));
|
||||
return archivedProject;
|
||||
return toProject(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to archive project';
|
||||
|
|
@ -163,11 +90,6 @@ export const projectsStore = {
|
|||
async reorderProjects(projectIds: string[]) {
|
||||
error = null;
|
||||
try {
|
||||
projects = projects.map((p) => {
|
||||
const newOrder = projectIds.indexOf(p.id);
|
||||
return newOrder !== -1 ? { ...p, order: newOrder } : p;
|
||||
});
|
||||
|
||||
for (let i = 0; i < projectIds.length; i++) {
|
||||
await projectCollection.update(projectIds[i], { order: i } as Partial<LocalProject>);
|
||||
}
|
||||
|
|
@ -178,16 +100,6 @@ export const projectsStore = {
|
|||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
projects = [];
|
||||
loading = false;
|
||||
error = null;
|
||||
},
|
||||
|
||||
isGuestInbox(_id: string) {
|
||||
return false;
|
||||
},
|
||||
|
||||
get guestInboxId() {
|
||||
return 'personal-project';
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,236 +1,23 @@
|
|||
/**
|
||||
* Tasks Store — Local-First with Dexie.js
|
||||
* Tasks Store — Mutation-Only Service
|
||||
*
|
||||
* All reads and writes go to IndexedDB first.
|
||||
* When authenticated, changes sync to the server in the background.
|
||||
* Same public API as before so components don't need changes.
|
||||
* All reads are handled by useLiveQuery() hooks in task-queries.ts.
|
||||
* This store only provides write operations (create, update, delete, etc.).
|
||||
* IndexedDB writes automatically trigger UI updates via Dexie liveQuery.
|
||||
*/
|
||||
|
||||
import type { Task, TaskPriority, TaskStatus, Subtask } from '@todo/shared';
|
||||
import { taskCollection, type LocalTask } from '$lib/data/local-store';
|
||||
import { isToday, isPast, isFuture, startOfDay, addDays } from 'date-fns';
|
||||
import { toTask } from '$lib/data/task-queries';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
// State — populated from IndexedDB
|
||||
let tasks = $state<Task[]>([]);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
/** Convert a LocalTask (IndexedDB record) to the shared Task type. */
|
||||
function toTask(local: LocalTask): Task {
|
||||
return {
|
||||
id: local.id,
|
||||
projectId: local.projectId,
|
||||
userId: local.userId ?? 'guest',
|
||||
title: local.title,
|
||||
description: local.description,
|
||||
dueDate: local.dueDate,
|
||||
scheduledDate: local.scheduledDate,
|
||||
scheduledStartTime: local.scheduledStartTime,
|
||||
estimatedDuration: local.estimatedDuration,
|
||||
priority: local.priority,
|
||||
status: local.isCompleted ? 'completed' : 'pending',
|
||||
isCompleted: local.isCompleted,
|
||||
completedAt: local.completedAt,
|
||||
order: local.order,
|
||||
recurrenceRule: local.recurrenceRule,
|
||||
subtasks: local.subtasks ?? null,
|
||||
metadata: local.metadata as Task['metadata'],
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Load tasks from IndexedDB into the reactive state. */
|
||||
async function refreshTasks(filter?: Partial<LocalTask>) {
|
||||
const localTasks = await taskCollection.getAll(filter, { sortBy: 'order', sortDirection: 'asc' });
|
||||
tasks = localTasks.map(toTask);
|
||||
}
|
||||
|
||||
export const tasksStore = {
|
||||
// Getters
|
||||
get tasks() {
|
||||
return tasks;
|
||||
},
|
||||
get loading() {
|
||||
return loading;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
get incompleteTasks() {
|
||||
return tasks.filter((t) => !t.isCompleted);
|
||||
},
|
||||
|
||||
get completedTasks() {
|
||||
return tasks.filter((t) => t.isCompleted);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch tasks with optional filters — reads from IndexedDB.
|
||||
*/
|
||||
async fetchTasks(
|
||||
query: {
|
||||
projectId?: string;
|
||||
labelId?: string;
|
||||
priority?: TaskPriority;
|
||||
status?: TaskStatus;
|
||||
dueBefore?: string;
|
||||
dueAfter?: string;
|
||||
isCompleted?: boolean;
|
||||
search?: string;
|
||||
} = {}
|
||||
) {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const filter: Partial<LocalTask> = {};
|
||||
if (query.projectId) filter.projectId = query.projectId;
|
||||
if (query.priority) filter.priority = query.priority;
|
||||
if (query.isCompleted !== undefined) filter.isCompleted = query.isCompleted;
|
||||
|
||||
let localTasks = await taskCollection.getAll(
|
||||
Object.keys(filter).length > 0 ? filter : undefined,
|
||||
{ sortBy: 'order', sortDirection: 'asc' }
|
||||
);
|
||||
|
||||
// Client-side search filter
|
||||
if (query.search) {
|
||||
const search = query.search.toLowerCase();
|
||||
localTasks = localTasks.filter(
|
||||
(t) =>
|
||||
t.title.toLowerCase().includes(search) || t.description?.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
tasks = localTasks.map(toTask);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch tasks';
|
||||
console.error('Failed to fetch tasks:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchInboxTasks() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const localTasks = await taskCollection.getAll(undefined, {
|
||||
sortBy: 'order',
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
// Inbox = tasks without projectId or with null projectId
|
||||
tasks = localTasks.filter((t) => !t.projectId).map(toTask);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch inbox tasks';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchTodayTasks() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const localTasks = await taskCollection.getAll(
|
||||
{ isCompleted: false },
|
||||
{ sortBy: 'order', sortDirection: 'asc' }
|
||||
);
|
||||
const today = startOfDay(new Date());
|
||||
tasks = localTasks
|
||||
.filter((t) => {
|
||||
if (!t.dueDate) return false;
|
||||
const d = new Date(t.dueDate);
|
||||
return isToday(d) || (isPast(startOfDay(d)) && !isToday(d));
|
||||
})
|
||||
.map(toTask);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch today tasks';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchUpcomingTasks() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const localTasks = await taskCollection.getAll(
|
||||
{ isCompleted: false },
|
||||
{ sortBy: 'dueDate', sortDirection: 'asc' }
|
||||
);
|
||||
const today = startOfDay(new Date());
|
||||
const weekFromNow = addDays(today, 7);
|
||||
tasks = localTasks
|
||||
.filter((t) => {
|
||||
if (!t.dueDate) return false;
|
||||
const d = new Date(t.dueDate);
|
||||
return isFuture(d) && d <= weekFromNow;
|
||||
})
|
||||
.map(toTask);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch upcoming tasks';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchAllTasks() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
await refreshTasks();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to fetch all tasks';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
getTasksByProject(projectId: string | null): Task[] {
|
||||
if (projectId === null) {
|
||||
return tasks.filter((t) => !t.projectId);
|
||||
}
|
||||
return tasks.filter((t) => t.projectId === projectId);
|
||||
},
|
||||
|
||||
getTasksByLabel(labelId: string): Task[] {
|
||||
return tasks.filter((t) => t.labels?.some((l) => l.id === labelId));
|
||||
},
|
||||
|
||||
get overdueTasks(): Task[] {
|
||||
return tasks.filter((t) => {
|
||||
if (!t.dueDate || t.isCompleted) return false;
|
||||
const dueDate = new Date(t.dueDate);
|
||||
return isPast(startOfDay(dueDate)) && !isToday(dueDate);
|
||||
});
|
||||
},
|
||||
|
||||
get todayTasks(): Task[] {
|
||||
const today = startOfDay(new Date());
|
||||
return tasks.filter((t) => {
|
||||
if (t.isCompleted) return false;
|
||||
if (!t.dueDate) return true;
|
||||
const taskDate = startOfDay(new Date(t.dueDate));
|
||||
return taskDate.getTime() === today.getTime();
|
||||
});
|
||||
},
|
||||
|
||||
get upcomingTasks(): Task[] {
|
||||
const today = startOfDay(new Date());
|
||||
const weekFromNow = addDays(today, 7);
|
||||
return tasks.filter((t) => {
|
||||
if (!t.dueDate || t.isCompleted) return false;
|
||||
const dueDate = new Date(t.dueDate);
|
||||
return isFuture(dueDate) && dueDate <= weekFromNow;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new task — writes to IndexedDB instantly.
|
||||
*/
|
||||
async createTask(data: {
|
||||
title: string;
|
||||
description?: string;
|
||||
|
|
@ -243,6 +30,7 @@ export const tasksStore = {
|
|||
}) {
|
||||
error = null;
|
||||
try {
|
||||
const count = await taskCollection.count();
|
||||
const newLocal: LocalTask = {
|
||||
id: crypto.randomUUID(),
|
||||
title: data.title,
|
||||
|
|
@ -251,16 +39,14 @@ export const tasksStore = {
|
|||
priority: data.priority ?? 'medium',
|
||||
isCompleted: false,
|
||||
dueDate: data.dueDate ?? null,
|
||||
order: tasks.length,
|
||||
order: count,
|
||||
recurrenceRule: data.recurrenceRule ?? null,
|
||||
subtasks: data.subtasks,
|
||||
};
|
||||
|
||||
const inserted = await taskCollection.insert(newLocal);
|
||||
const newTask = toTask(inserted);
|
||||
tasks = [...tasks, newTask];
|
||||
TodoEvents.taskCreated(!!data.dueDate);
|
||||
return newTask;
|
||||
return toTask(inserted);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create task';
|
||||
console.error('Failed to create task:', e);
|
||||
|
|
@ -268,9 +54,6 @@ export const tasksStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a task — writes to IndexedDB instantly.
|
||||
*/
|
||||
async updateTask(
|
||||
id: string,
|
||||
data: {
|
||||
|
|
@ -296,9 +79,7 @@ export const tasksStore = {
|
|||
try {
|
||||
const updated = await taskCollection.update(id, data as Partial<LocalTask>);
|
||||
if (updated) {
|
||||
const updatedTask = toTask(updated);
|
||||
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
|
||||
return updatedTask;
|
||||
return toTask(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update task';
|
||||
|
|
@ -307,9 +88,6 @@ export const tasksStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Optimistic update — for drag-and-drop. Instant local write.
|
||||
*/
|
||||
async updateTaskOptimistic(
|
||||
id: string,
|
||||
data: {
|
||||
|
|
@ -317,10 +95,6 @@ export const tasksStore = {
|
|||
isCompleted?: boolean;
|
||||
}
|
||||
) {
|
||||
// Immediate local state update
|
||||
tasks = tasks.map((t) => (t.id === id ? { ...t, ...data } : t));
|
||||
|
||||
// Persist to IndexedDB
|
||||
const updateData: Partial<LocalTask> = {};
|
||||
if (data.dueDate !== undefined) updateData.dueDate = data.dueDate;
|
||||
if (data.isCompleted !== undefined) {
|
||||
|
|
@ -335,7 +109,6 @@ export const tasksStore = {
|
|||
error = null;
|
||||
try {
|
||||
await taskCollection.delete(id);
|
||||
tasks = tasks.filter((t) => t.id !== id);
|
||||
TodoEvents.taskDeleted();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete task';
|
||||
|
|
@ -352,10 +125,8 @@ export const tasksStore = {
|
|||
completedAt: new Date().toISOString(),
|
||||
} as Partial<LocalTask>);
|
||||
if (updated) {
|
||||
const completedTask = toTask(updated);
|
||||
tasks = tasks.map((t) => (t.id === id ? completedTask : t));
|
||||
TodoEvents.taskCompleted();
|
||||
return completedTask;
|
||||
return toTask(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to complete task';
|
||||
|
|
@ -372,10 +143,8 @@ export const tasksStore = {
|
|||
completedAt: null,
|
||||
} as Partial<LocalTask>);
|
||||
if (updated) {
|
||||
const uncompletedTask = toTask(updated);
|
||||
tasks = tasks.map((t) => (t.id === id ? uncompletedTask : t));
|
||||
TodoEvents.taskUncompleted();
|
||||
return uncompletedTask;
|
||||
return toTask(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to uncomplete task';
|
||||
|
|
@ -389,9 +158,7 @@ export const tasksStore = {
|
|||
try {
|
||||
const updated = await taskCollection.update(id, { projectId } as Partial<LocalTask>);
|
||||
if (updated) {
|
||||
const movedTask = toTask(updated);
|
||||
tasks = tasks.map((t) => (t.id === id ? movedTask : t));
|
||||
return movedTask;
|
||||
return toTask(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to move task';
|
||||
|
|
@ -401,17 +168,13 @@ export const tasksStore = {
|
|||
},
|
||||
|
||||
async updateLabels(id: string, labelIds: string[]) {
|
||||
// Labels are stored via the central tag system, not locally.
|
||||
// For now, update the task metadata to track label associations.
|
||||
error = null;
|
||||
try {
|
||||
const updated = await taskCollection.update(id, {
|
||||
metadata: { labelIds },
|
||||
} as Partial<LocalTask>);
|
||||
if (updated) {
|
||||
const updatedTask = toTask(updated);
|
||||
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
|
||||
return updatedTask;
|
||||
return toTask(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update labels';
|
||||
|
|
@ -425,9 +188,7 @@ export const tasksStore = {
|
|||
try {
|
||||
const updated = await taskCollection.update(id, { subtasks } as Partial<LocalTask>);
|
||||
if (updated) {
|
||||
const updatedTask = toTask(updated);
|
||||
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
|
||||
return updatedTask;
|
||||
return toTask(updated);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update subtasks';
|
||||
|
|
@ -439,13 +200,6 @@ export const tasksStore = {
|
|||
async reorderTasks(taskIds: string[]) {
|
||||
error = null;
|
||||
try {
|
||||
// Update order in local state immediately
|
||||
tasks = tasks.map((t) => {
|
||||
const newOrder = taskIds.indexOf(t.id);
|
||||
return newOrder !== -1 ? { ...t, order: newOrder } : t;
|
||||
});
|
||||
|
||||
// Persist each order change to IndexedDB
|
||||
for (let i = 0; i < taskIds.length; i++) {
|
||||
await taskCollection.update(taskIds[i], { order: i } as Partial<LocalTask>);
|
||||
}
|
||||
|
|
@ -455,15 +209,6 @@ export const tasksStore = {
|
|||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
tasks = [];
|
||||
loading = false;
|
||||
error = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* No longer relevant — all tasks are local and editable.
|
||||
*/
|
||||
isDemoTask(_taskId: string) {
|
||||
return false;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { setContext } from 'svelte';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import {
|
||||
PillNavigation,
|
||||
|
|
@ -24,8 +25,12 @@
|
|||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import { labelsStore } from '$lib/stores/labels.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import TaskFilters from '$lib/components/TaskFilters.svelte';
|
||||
import { viewStore, type SortBy } from '$lib/stores/view.svelte';
|
||||
|
|
@ -39,16 +44,29 @@
|
|||
import { filterHiddenNavItems } from '@manacore/shared-theme';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { getTasks } from '$lib/api/tasks';
|
||||
import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from '$lib/utils/task-parser';
|
||||
import { todoOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
import { todoStore } from '$lib/data/local-store';
|
||||
import { todoStore, taskCollection } from '$lib/data/local-store';
|
||||
import { useAllTasks, useAllProjects, getActiveProjects } from '$lib/data/task-queries';
|
||||
import SyncIndicator from '$lib/components/SyncIndicator.svelte';
|
||||
|
||||
// Live queries — auto-update when IndexedDB changes (local writes, sync, other tabs)
|
||||
const allTasks = useAllTasks();
|
||||
const allProjects = useAllProjects();
|
||||
const allTags = useAllSharedTags();
|
||||
|
||||
// Provide data to child components via Svelte context
|
||||
setContext('projects', allProjects);
|
||||
setContext('tasks', allTasks);
|
||||
setContext('tags', allTags);
|
||||
|
||||
// Derived active projects for UI
|
||||
let activeProjects = $derived(getActiveProjects(allProjects.value));
|
||||
|
||||
// Guest welcome modal state
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
|
|
@ -71,13 +89,18 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// QuickInputBar search - search tasks
|
||||
// QuickInputBar search - search tasks locally
|
||||
async function handleSearch(query: string): Promise<QuickInputItem[]> {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
try {
|
||||
const tasks = await getTasks({ search: query });
|
||||
return tasks.slice(0, 10).map((task) => ({
|
||||
const q = query.toLowerCase();
|
||||
return allTasks.value
|
||||
.filter(
|
||||
(task) =>
|
||||
task.title.toLowerCase().includes(q) || task.description?.toLowerCase().includes(q)
|
||||
)
|
||||
.slice(0, 10)
|
||||
.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
subtitle: task.isCompleted
|
||||
|
|
@ -86,9 +109,6 @@
|
|||
? new Date(task.dueDate).toLocaleDateString('de-DE')
|
||||
: 'Kein Datum',
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(item: QuickInputItem) {
|
||||
|
|
@ -114,7 +134,7 @@
|
|||
|
||||
try {
|
||||
const parsed = parseTaskInput(query);
|
||||
const resolved = resolveTaskIds(parsed, projectsStore.projects, labelsStore.labels);
|
||||
const resolved = resolveTaskIds(parsed, allProjects.value, allTags.value);
|
||||
|
||||
await tasksStore.createTask({
|
||||
title: resolved.title,
|
||||
|
|
@ -296,18 +316,19 @@
|
|||
|
||||
async function handleLogout() {
|
||||
await authStore.signOut();
|
||||
projectsStore.clear();
|
||||
labelsStore.clear();
|
||||
tagMutations.stopSync();
|
||||
goto('/login');
|
||||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database (opens IndexedDB, seeds guest data)
|
||||
await todoStore.initialize();
|
||||
// Initialize local-first databases (opens IndexedDB, seeds guest data)
|
||||
await Promise.all([todoStore.initialize(), tagLocalStore.initialize()]);
|
||||
|
||||
// If authenticated, start syncing to server
|
||||
if (authStore.isAuthenticated) {
|
||||
todoStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
todoStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
}
|
||||
|
||||
// Initialize split-panel from URL/localStorage
|
||||
|
|
@ -319,12 +340,11 @@
|
|||
// Show guest welcome modal on first visit
|
||||
initGuestWelcome();
|
||||
|
||||
// Load projects from IndexedDB (guest seed or synced data)
|
||||
await projectsStore.fetchProjects();
|
||||
// Tags and projects are now loaded reactively via useLiveQuery — no fetch needed
|
||||
|
||||
// Labels and user settings need auth (central mana-core-auth service)
|
||||
// User settings need auth
|
||||
if (authStore.isAuthenticated) {
|
||||
await Promise.all([labelsStore.fetchLabels(), userSettings.load()]);
|
||||
await userSettings.load();
|
||||
}
|
||||
|
||||
// Redirect to start page if on root and a custom start page is set
|
||||
|
|
@ -399,10 +419,10 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={labelsStore.labels.map((l) => ({
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
color: l.color || '#6b7280',
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#6b7280',
|
||||
}))}
|
||||
selectedIds={viewStore.filterLabelIds}
|
||||
onToggle={(tagId) => {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { format, addDays, subDays, startOfDay } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { Sparkle, ArrowDown } from '@manacore/shared-icons';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import { applyTaskFilters } from '$lib/utils/task-filters';
|
||||
import { filterOverdue, filterToday, filterCompleted } from '$lib/data/task-queries';
|
||||
import TaskList from '$lib/components/TaskList.svelte';
|
||||
import CollapsibleSection from '$lib/components/CollapsibleSection.svelte';
|
||||
import { TaskListSkeleton } from '$lib/components/skeletons';
|
||||
import type { Task } from '@todo/shared';
|
||||
|
||||
let isLoading = $state(true);
|
||||
// Live tasks from layout context — auto-updates on IndexedDB changes
|
||||
const allTasks: { readonly value: Task[] } = getContext('tasks');
|
||||
|
||||
let tipDismissed = $state(false);
|
||||
|
||||
// Build filter criteria from viewStore (reactive)
|
||||
|
|
@ -26,30 +29,20 @@
|
|||
return applyTaskFilters(tasks, filterCriteria);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
onMount(() => {
|
||||
viewStore.setToday();
|
||||
|
||||
try {
|
||||
// Fetch tasks (works in both guest and authenticated mode)
|
||||
await tasksStore.fetchAllTasks();
|
||||
} catch (error) {
|
||||
console.error('Failed to load tasks:', error);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
// Derived task lists (with filters applied)
|
||||
let overdueTasks = $derived(applyFilters(tasksStore.overdueTasks));
|
||||
let todayTasks = $derived(applyFilters(tasksStore.todayTasks));
|
||||
let completedTasks = $derived(applyFilters(tasksStore.completedTasks));
|
||||
// Derived task lists (with filters applied) — automatically reactive via liveQuery
|
||||
let overdueTasks = $derived(applyFilters(filterOverdue(allTasks.value)));
|
||||
let todayTasks = $derived(applyFilters(filterToday(allTasks.value)));
|
||||
let completedTasks = $derived(applyFilters(filterCompleted(allTasks.value)));
|
||||
|
||||
// Tomorrow's tasks
|
||||
let tomorrowDate = $derived(addDays(startOfDay(new Date()), 1));
|
||||
let dayAfterTomorrowDate = $derived(addDays(startOfDay(new Date()), 2));
|
||||
let tomorrowTasks = $derived(
|
||||
applyFilters(
|
||||
tasksStore.tasks.filter((task) => {
|
||||
allTasks.value.filter((task) => {
|
||||
if (!task.dueDate || task.isCompleted) return false;
|
||||
const taskDate = startOfDay(new Date(task.dueDate));
|
||||
return taskDate.getTime() === tomorrowDate.getTime();
|
||||
|
|
@ -66,7 +59,7 @@
|
|||
for (let i = 2; i <= 7; i++) {
|
||||
const date = addDays(today, i);
|
||||
const dayTasks = applyFilters(
|
||||
tasksStore.tasks.filter((task) => {
|
||||
allTasks.value.filter((task) => {
|
||||
if (!task.dueDate || task.isCompleted) return false;
|
||||
const taskDate = startOfDay(new Date(task.dueDate));
|
||||
return taskDate.getTime() === date.getTime();
|
||||
|
|
@ -122,23 +115,20 @@
|
|||
|
||||
// Drag and drop handler - uses optimistic updates for smooth UX
|
||||
async function handleTaskDrop(taskId: string, targetDate: Date | 'completed' | 'overdue') {
|
||||
const task = tasksStore.tasks.find((t) => t.id === taskId);
|
||||
const task = allTasks.value.find((t) => t.id === taskId);
|
||||
if (!task) return;
|
||||
|
||||
if (targetDate === 'completed') {
|
||||
// Mark task as completed (optimistic)
|
||||
if (!task.isCompleted) {
|
||||
await tasksStore.updateTaskOptimistic(taskId, { isCompleted: true });
|
||||
}
|
||||
} else if (targetDate === 'overdue') {
|
||||
// Set to yesterday (optimistic)
|
||||
const yesterday = subDays(startOfDay(new Date()), 1);
|
||||
await tasksStore.updateTaskOptimistic(taskId, {
|
||||
dueDate: yesterday.toISOString(),
|
||||
isCompleted: task.isCompleted ? false : undefined,
|
||||
});
|
||||
} else {
|
||||
// Set to specific date (optimistic)
|
||||
await tasksStore.updateTaskOptimistic(taskId, {
|
||||
dueDate: targetDate.toISOString(),
|
||||
isCompleted: task.isCompleted ? false : undefined,
|
||||
|
|
@ -152,8 +142,10 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="unified-view">
|
||||
{#if isLoading || tasksStore.loading}
|
||||
<TaskListSkeleton sections={3} tasksPerSection={3} />
|
||||
{#if allTasks.error}
|
||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
|
||||
{allTasks.error}
|
||||
</div>
|
||||
{:else if tasksStore.error}
|
||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
|
||||
{tasksStore.error}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
import { userSettings } from '$lib/stores/user-settings.svelte';
|
||||
import { APP_VERSION } from '$lib/version';
|
||||
import { todoSettings, type TodoView, type KanbanCardSize } from '$lib/stores/settings.svelte';
|
||||
import { projectsStore } from '$lib/stores/projects.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Project } from '@todo/shared';
|
||||
|
||||
const projectsCtx: { readonly value: Project[] } = getContext('projects');
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { PRIORITY_OPTIONS } from '@todo/shared';
|
||||
import {
|
||||
|
|
@ -51,13 +54,13 @@
|
|||
// Project options for quick add (computed)
|
||||
let projectOptions = $derived([
|
||||
{ value: null, label: 'Inbox' },
|
||||
...projectsStore.projects.map((p) => ({ value: p.id, label: p.name })),
|
||||
...projectsCtx.value.map((p) => ({ value: p.id, label: p.name })),
|
||||
]);
|
||||
|
||||
onMount(async () => {
|
||||
// Load user settings and projects from server (only if authenticated)
|
||||
if (authStore.isAuthenticated) {
|
||||
await Promise.all([userSettings.load(), projectsStore.fetchProjects()]);
|
||||
await userSettings.load();
|
||||
}
|
||||
|
||||
// Initialize todo settings from localStorage
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { spiralStore } from '$lib/stores/spiral.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import SpiralCanvas from '$lib/components/SpiralCanvas.svelte';
|
||||
import { visualizeImageEmoji, COLORS, type ColorDefinition } from '@manacore/spiral-db';
|
||||
import type { Task } from '@todo/shared';
|
||||
|
||||
// Live tasks from layout context
|
||||
const allTasks: { readonly value: Task[] } = getContext('tasks');
|
||||
|
||||
// Get colors as array for iteration
|
||||
const colorsArray: ColorDefinition[] = Object.values(COLORS);
|
||||
|
|
@ -19,16 +22,10 @@
|
|||
let emojiView = $derived(spiralStore.image ? visualizeImageEmoji(spiralStore.image) : '');
|
||||
|
||||
// Import todos from main store on mount
|
||||
onMount(async () => {
|
||||
// Fetch tasks if not already loaded
|
||||
if (tasksStore.tasks.length === 0) {
|
||||
await tasksStore.fetchTasks({});
|
||||
}
|
||||
|
||||
// Import existing todos into spiral DB
|
||||
if (tasksStore.tasks.length > 0) {
|
||||
onMount(() => {
|
||||
if (allTasks.value.length > 0) {
|
||||
spiralStore.importTodos(
|
||||
tasksStore.tasks.map((t) => ({
|
||||
allTasks.value.map((t) => ({
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
priority: t.priority,
|
||||
|
|
@ -93,7 +90,7 @@
|
|||
|
||||
function handleReimport() {
|
||||
spiralStore.importTodos(
|
||||
tasksStore.tasks.map((t) => ({
|
||||
allTasks.value.map((t) => ({
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
priority: t.priority,
|
||||
|
|
|
|||
|
|
@ -1,47 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { CaretLeft, Tag } from '@manacore/shared-icons';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { labelsStore } from '$lib/stores/labels.svelte';
|
||||
import { CaretLeft, Tag as TagIcon } from '@manacore/shared-icons';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { filterByLabel } from '$lib/data/task-queries';
|
||||
import TaskList from '$lib/components/TaskList.svelte';
|
||||
import { TaskListSkeleton } from '$lib/components/skeletons';
|
||||
import type { Task } from '@todo/shared';
|
||||
|
||||
let isLoading = $state(true);
|
||||
// Live data from layout context
|
||||
const allTasks: { readonly value: Task[] } = getContext('tasks');
|
||||
const allTags: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
// Get tag ID from URL
|
||||
const tagId = $derived($page.params.id ?? '');
|
||||
|
||||
// Get tag from store
|
||||
const tag = $derived(labelsStore.getById(tagId));
|
||||
// Get tag — reactively via liveQuery
|
||||
const tag = $derived(allTags.value.find((t) => t.id === tagId));
|
||||
|
||||
// Get tasks with this tag
|
||||
const tagTasks = $derived(tagId ? tasksStore.getTasksByLabel(tagId) : []);
|
||||
// Get tasks with this tag — reactively via liveQuery
|
||||
const tagTasks = $derived(tagId ? filterByLabel(allTasks.value, tagId) : []);
|
||||
const incompleteTasks = $derived(tagTasks.filter((t) => !t.isCompleted));
|
||||
const completedTasks = $derived(tagTasks.filter((t) => t.isCompleted));
|
||||
|
||||
onMount(async () => {
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure tags are loaded
|
||||
if (labelsStore.labels.length === 0) {
|
||||
await labelsStore.fetchLabels();
|
||||
}
|
||||
|
||||
// Fetch all tasks to filter by tag
|
||||
await tasksStore.fetchAllTasks();
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -65,16 +45,14 @@
|
|||
{/if}
|
||||
</div>
|
||||
<a href="/tags" class="manage-button" aria-label="Tags verwalten">
|
||||
<Tag size={20} weight="bold" />
|
||||
<TagIcon size={20} weight="bold" />
|
||||
</a>
|
||||
</header>
|
||||
|
||||
{#if isLoading}
|
||||
<TaskListSkeleton />
|
||||
{:else if !tag}
|
||||
{#if !tag}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<Tag size={40} weight="light" />
|
||||
<TagIcon size={40} weight="light" />
|
||||
</div>
|
||||
<h2 class="empty-title">Tag nicht gefunden</h2>
|
||||
<p class="empty-description">Dieser Tag existiert nicht mehr.</p>
|
||||
|
|
@ -83,7 +61,7 @@
|
|||
{:else if tagTasks.length === 0}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon" style="background-color: {tag.color}20">
|
||||
<Tag size={40} weight="light" style="color: {tag.color}" />
|
||||
<TagIcon size={40} weight="light" style="color: {tag.color}" />
|
||||
</div>
|
||||
<h2 class="empty-title">Keine Aufgaben</h2>
|
||||
<p class="empty-description">
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { TagList, TagEditModal, ConfirmationModal, type Tag } from '@manacore/shared-ui';
|
||||
import { TagList, TagEditModal, ConfirmationModal, type Tag as UITag } from '@manacore/shared-ui';
|
||||
import { MagnifyingGlass, Plus, CaretLeft, Check } from '@manacore/shared-icons';
|
||||
import { labelsStore } from '$lib/stores/labels.svelte';
|
||||
import type { Label } from '@todo/shared';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
// Live tags from layout context
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
let searchQuery = $state('');
|
||||
let showModal = $state(false);
|
||||
let editingLabel = $state<Label | null>(null);
|
||||
let editingTag = $state<Tag | null>(null);
|
||||
let showDeleteConfirm = $state(false);
|
||||
let labelToDelete = $state<Tag | null>(null);
|
||||
let tagToDelete = $state<UITag | null>(null);
|
||||
|
||||
// Inline create state
|
||||
let newTagName = $state('');
|
||||
|
|
@ -31,18 +34,18 @@
|
|||
'#6b7280', // gray
|
||||
];
|
||||
|
||||
const filteredLabels = $derived.by(() => {
|
||||
if (!searchQuery.trim()) return labelsStore.labels;
|
||||
const filteredTags = $derived.by(() => {
|
||||
if (!searchQuery.trim()) return tagsCtx.value;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return labelsStore.labels.filter((l) => l.name.toLowerCase().includes(query));
|
||||
return tagsCtx.value.filter((t) => t.name.toLowerCase().includes(query));
|
||||
});
|
||||
|
||||
// Convert Label to Tag type for shared-ui components
|
||||
function labelToTag(label: Label): Tag {
|
||||
// Convert Tag to UITag type for shared-ui components
|
||||
function tagToUITag(tag: Tag): UITag {
|
||||
return {
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
color: tag.color,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +55,7 @@
|
|||
|
||||
isCreating = true;
|
||||
try {
|
||||
await labelsStore.createLabel({ name: newTagName.trim(), color: newTagColor });
|
||||
await tagMutations.createTag({ name: newTagName.trim(), color: newTagColor });
|
||||
newTagName = '';
|
||||
newTagColor = '#8b5cf6';
|
||||
} catch (e) {
|
||||
|
|
@ -69,68 +72,62 @@
|
|||
}
|
||||
}
|
||||
|
||||
function openEditModal(tag: Tag) {
|
||||
const label = labelsStore.labels.find((l) => l.id === tag.id);
|
||||
if (label) {
|
||||
editingLabel = label;
|
||||
function openEditModal(uiTag: UITag) {
|
||||
const tag = tagsCtx.value.find((t) => t.id === uiTag.id);
|
||||
if (tag) {
|
||||
editingTag = tag;
|
||||
showModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal = false;
|
||||
editingLabel = null;
|
||||
editingTag = null;
|
||||
}
|
||||
|
||||
async function handleSave(name: string, color: string) {
|
||||
try {
|
||||
if (editingLabel) {
|
||||
await labelsStore.updateLabel(editingLabel.id, { name, color });
|
||||
if (editingTag) {
|
||||
await tagMutations.updateTag(editingTag.id, { name, color });
|
||||
}
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
console.error('Failed to save label:', e);
|
||||
console.error('Failed to save tag:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
if (!editingLabel) return;
|
||||
if (!editingTag) return;
|
||||
|
||||
try {
|
||||
await labelsStore.deleteLabel(editingLabel.id);
|
||||
await tagMutations.deleteTag(editingTag.id);
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
console.error('Failed to delete label:', e);
|
||||
console.error('Failed to delete tag:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteFromList(tag: Tag) {
|
||||
labelToDelete = tag;
|
||||
function handleDeleteFromList(uiTag: UITag) {
|
||||
tagToDelete = uiTag;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
|
||||
async function confirmDeleteLabel() {
|
||||
if (!labelToDelete) return;
|
||||
async function confirmDeleteTag() {
|
||||
if (!tagToDelete) return;
|
||||
|
||||
try {
|
||||
await labelsStore.deleteLabel(labelToDelete.id);
|
||||
await tagMutations.deleteTag(tagToDelete.id);
|
||||
} catch (e) {
|
||||
console.error('Failed to delete label:', e);
|
||||
console.error('Failed to delete tag:', e);
|
||||
} finally {
|
||||
showDeleteConfirm = false;
|
||||
labelToDelete = null;
|
||||
tagToDelete = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTagClick(tag: Tag) {
|
||||
goto(`/tag/${tag.id}`);
|
||||
function handleTagClick(uiTag: UITag) {
|
||||
goto(`/tag/${uiTag.id}`);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (labelsStore.labels.length === 0) {
|
||||
labelsStore.fetchLabels();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -213,16 +210,16 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
{#if labelsStore.error}
|
||||
{#if tagMutations.error}
|
||||
<div class="error-banner" role="alert">
|
||||
<span>{labelsStore.error}</span>
|
||||
<span>{tagMutations.error}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Tag List using shared component -->
|
||||
<TagList
|
||||
tags={filteredLabels.map(labelToTag)}
|
||||
loading={labelsStore.loading}
|
||||
tags={filteredTags.map(tagToUITag)}
|
||||
loading={false}
|
||||
onEdit={openEditModal}
|
||||
onDelete={handleDeleteFromList}
|
||||
onClick={handleTagClick}
|
||||
|
|
@ -232,29 +229,29 @@
|
|||
: 'Erstelle deinen ersten Tag'}
|
||||
/>
|
||||
|
||||
{#if !labelsStore.loading && labelsStore.labels.length > 0}
|
||||
{#if tagsCtx.value.length > 0}
|
||||
<p class="tags-count">
|
||||
{labelsStore.labels.length}
|
||||
{labelsStore.labels.length === 1 ? 'Tag' : 'Tags'}
|
||||
{tagsCtx.value.length}
|
||||
{tagsCtx.value.length === 1 ? 'Tag' : 'Tags'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal using shared component -->
|
||||
<TagEditModal
|
||||
tag={editingLabel ? labelToTag(editingLabel) : null}
|
||||
tag={editingTag ? tagToUITag(editingTag) : null}
|
||||
isOpen={showModal}
|
||||
onClose={closeModal}
|
||||
onSave={handleSave}
|
||||
onDelete={editingLabel ? handleDelete : undefined}
|
||||
title={editingLabel ? 'Tag bearbeiten' : 'Neuer Tag'}
|
||||
saveLabel={editingLabel ? 'Speichern' : 'Erstellen'}
|
||||
onDelete={editingTag ? handleDelete : undefined}
|
||||
title={editingTag ? 'Tag bearbeiten' : 'Neuer Tag'}
|
||||
saveLabel={editingTag ? 'Speichern' : 'Erstellen'}
|
||||
deleteLabel="Löschen"
|
||||
cancelLabel="Abbrechen"
|
||||
namePlaceholder="Tag Name"
|
||||
colorLabel="Farbe"
|
||||
previewLabel="Vorschau"
|
||||
deleteConfirmMessage={`Tag "${editingLabel?.name || ''}" wirklich löschen?`}
|
||||
deleteConfirmMessage={`Tag "${editingTag?.name || ''}" wirklich löschen?`}
|
||||
/>
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
|
|
@ -262,12 +259,12 @@
|
|||
visible={showDeleteConfirm}
|
||||
onClose={() => {
|
||||
showDeleteConfirm = false;
|
||||
labelToDelete = null;
|
||||
tagToDelete = null;
|
||||
}}
|
||||
onConfirm={confirmDeleteLabel}
|
||||
onConfirm={confirmDeleteTag}
|
||||
variant="danger"
|
||||
title="Tag löschen?"
|
||||
message={`Der Tag "${labelToDelete?.name ?? ''}" wird unwiderruflich gelöscht.`}
|
||||
message={`Der Tag "${tagToDelete?.name ?? ''}" wird unwiderruflich gelöscht.`}
|
||||
confirmLabel="Löschen"
|
||||
cancelLabel="Abbrechen"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
/**
|
||||
* Tag Store - Uses shared createTagStore backed by central mana-core-auth
|
||||
* Tag Store — Local-First via Shared Tag Store
|
||||
* Tags are stored in shared IndexedDB ('manacore-tags'), accessible across all apps.
|
||||
* Use context ('tags') for reads, tagMutations for writes.
|
||||
*/
|
||||
import { browser } from '$app/environment';
|
||||
import { createTagStore } from '@manacore/shared-stores';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_CORE_AUTH_URL__;
|
||||
return injectedUrl || 'http://localhost:3001';
|
||||
}
|
||||
return 'http://localhost:3001';
|
||||
}
|
||||
|
||||
export const tagStore = createTagStore({
|
||||
authUrl: getAuthUrl(),
|
||||
getToken: () => authStore.getValidToken(),
|
||||
});
|
||||
export {
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
} from '@manacore/shared-stores';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@
|
|||
TagStrip,
|
||||
} from '@manacore/shared-ui';
|
||||
import type { PillNavItem, PillDropdownItem, QuickInputItem } from '@manacore/shared-ui';
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import {
|
||||
tagLocalStore,
|
||||
tagMutations,
|
||||
useAllTags as useAllSharedTags,
|
||||
} from '@manacore/shared-stores';
|
||||
|
||||
// Extend QuickInputItem for zitare-specific search results with href navigation
|
||||
interface ZitareSearchItem extends QuickInputItem {
|
||||
|
|
@ -31,11 +35,15 @@
|
|||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
|
||||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { setContext } from 'svelte';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
|
||||
import { QUOTES, type Quote } from '@zitare/content';
|
||||
import { zitareStore } from '$lib/data/local-store';
|
||||
|
||||
const allTags = useAllSharedTags();
|
||||
setContext('tags', allTags);
|
||||
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
// App switcher items
|
||||
|
|
@ -233,8 +241,8 @@
|
|||
}
|
||||
|
||||
async function handleAuthReady() {
|
||||
// Initialize local-first database
|
||||
await zitareStore.initialize();
|
||||
// Initialize local-first databases (app + shared tags)
|
||||
await Promise.all([zitareStore.initialize(), tagLocalStore.initialize()]);
|
||||
|
||||
// Initialize settings
|
||||
zitareSettings.initialize();
|
||||
|
|
@ -244,9 +252,10 @@
|
|||
await listsStore.loadLists();
|
||||
|
||||
if (authStore.isAuthenticated) {
|
||||
zitareStore.startSync(() => authStore.getValidToken());
|
||||
const getToken = () => authStore.getValidToken();
|
||||
zitareStore.startSync(getToken);
|
||||
tagMutations.startSync(getToken);
|
||||
userSettings.load();
|
||||
tagStore.fetchTags();
|
||||
} else if (shouldShowGuestWelcome('zitare')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
|
|
@ -296,7 +305,7 @@
|
|||
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
|
||||
{#if isTagStripVisible}
|
||||
<TagStrip
|
||||
tags={tagStore.tags.map((t) => ({
|
||||
tags={allTags.value.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
color: t.color || '#3b82f6',
|
||||
|
|
@ -305,7 +314,6 @@
|
|||
onToggle={() => {}}
|
||||
onClear={() => {}}
|
||||
managementHref="/tags"
|
||||
loading={tagStore.loading}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { tagStore } from '$lib/stores/tags.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
import { tagMutations } from '@manacore/shared-stores';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
onMount(() => {
|
||||
if (tagStore.tags.length === 0) {
|
||||
tagStore.fetchTags();
|
||||
}
|
||||
});
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -19,13 +16,11 @@
|
|||
Tags sind app-übergreifend — Änderungen gelten in allen ManaCore-Apps.
|
||||
</p>
|
||||
|
||||
{#if tagStore.loading}
|
||||
<p>Lädt...</p>
|
||||
{:else if tagStore.tags.length === 0}
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p>Keine Tags vorhanden.</p>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
{#each tagStore.tags as tag}
|
||||
{#each tagsCtx.value as tag}
|
||||
<div class="flex items-center gap-2 p-2 rounded-lg bg-card">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: {tag.color}"></span>
|
||||
<span>{tag.name}</span>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@manacore/local-store": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-tags": "workspace:*"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,3 +21,19 @@ export {
|
|||
type SimpleNavigationOptions,
|
||||
} from './navigation-simple';
|
||||
export { createTagStore, type TagStore, type TagStoreConfig } from './tags.svelte';
|
||||
export {
|
||||
tagLocalStore,
|
||||
tagCollection,
|
||||
tagGroupCollection,
|
||||
tagMutations,
|
||||
useAllTags,
|
||||
useAllTagGroups,
|
||||
getTagById,
|
||||
getTagsByIds,
|
||||
getTagColor,
|
||||
getTagsByGroup,
|
||||
toTag,
|
||||
toTagGroup,
|
||||
type LocalTag,
|
||||
type LocalTagGroup,
|
||||
} from './tags-local.svelte';
|
||||
|
|
|
|||
304
packages/shared-stores/src/tags-local.svelte.ts
Normal file
304
packages/shared-stores/src/tags-local.svelte.ts
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* Local-First Tag Store (Shared Across All Apps)
|
||||
*
|
||||
* Uses a shared IndexedDB database ('manacore-tags') that all apps read from.
|
||||
* Tags are synced to the server via mana-sync, just like any other collection.
|
||||
*
|
||||
* Architecture:
|
||||
* - Tags + TagGroups → shared IndexedDB ('manacore-tags'), one DB for all apps
|
||||
* - TagLinks (junction) → stay in each app's own IndexedDB (app-specific)
|
||||
* - Guest mode → default seed tags (Arbeit, Persönlich, Familie, Wichtig)
|
||||
* - Cross-app → all apps import the same store, read from the same DB
|
||||
*/
|
||||
|
||||
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
|
||||
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
|
||||
import type {
|
||||
Tag,
|
||||
TagGroup,
|
||||
CreateTagInput,
|
||||
UpdateTagInput,
|
||||
CreateTagGroupInput,
|
||||
UpdateTagGroupInput,
|
||||
} from '@manacore/shared-tags';
|
||||
|
||||
// ─── Local Types ───────────────────────────────────────────
|
||||
|
||||
export interface LocalTag extends BaseRecord {
|
||||
name: string;
|
||||
color: string;
|
||||
icon?: string | null;
|
||||
groupId?: string | null;
|
||||
userId?: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
export interface LocalTagGroup extends BaseRecord {
|
||||
name: string;
|
||||
color: string;
|
||||
icon?: string | null;
|
||||
userId?: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
// ─── Type Converters ───────────────────────────────────────
|
||||
|
||||
export function toTag(local: LocalTag): Tag {
|
||||
return {
|
||||
id: local.id,
|
||||
userId: local.userId ?? 'guest',
|
||||
name: local.name,
|
||||
color: local.color,
|
||||
icon: local.icon,
|
||||
groupId: local.groupId,
|
||||
sortOrder: local.sortOrder,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function toTagGroup(local: LocalTagGroup): TagGroup {
|
||||
return {
|
||||
id: local.id,
|
||||
userId: local.userId ?? 'guest',
|
||||
name: local.name,
|
||||
color: local.color,
|
||||
icon: local.icon,
|
||||
sortOrder: local.sortOrder,
|
||||
createdAt: local.createdAt ?? new Date().toISOString(),
|
||||
updatedAt: local.updatedAt ?? new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Guest Seed Data ───────────────────────────────────────
|
||||
|
||||
const guestTags: LocalTag[] = [
|
||||
{
|
||||
id: 'tag-arbeit',
|
||||
name: 'Arbeit',
|
||||
color: '#3B82F6',
|
||||
icon: 'Briefcase',
|
||||
sortOrder: 0,
|
||||
},
|
||||
{
|
||||
id: 'tag-persoenlich',
|
||||
name: 'Persönlich',
|
||||
color: '#10B981',
|
||||
icon: 'User',
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
id: 'tag-familie',
|
||||
name: 'Familie',
|
||||
color: '#EC4899',
|
||||
icon: 'Heart',
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
id: 'tag-wichtig',
|
||||
name: 'Wichtig',
|
||||
color: '#EF4444',
|
||||
icon: 'Star',
|
||||
sortOrder: 3,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Shared Store Instance ─────────────────────────────────
|
||||
|
||||
const SYNC_SERVER_URL =
|
||||
(typeof window !== 'undefined' &&
|
||||
(window as unknown as { __PUBLIC_SYNC_SERVER_URL__?: string }).__PUBLIC_SYNC_SERVER_URL__) ||
|
||||
(typeof import.meta !== 'undefined' && import.meta.env?.PUBLIC_SYNC_SERVER_URL) ||
|
||||
'http://localhost:3050';
|
||||
|
||||
export const tagLocalStore = createLocalStore({
|
||||
appId: 'tags',
|
||||
collections: [
|
||||
{
|
||||
name: 'tags',
|
||||
indexes: ['name', 'groupId', 'sortOrder'],
|
||||
guestSeed: guestTags,
|
||||
},
|
||||
{
|
||||
name: 'tagGroups',
|
||||
indexes: ['sortOrder'],
|
||||
},
|
||||
],
|
||||
sync: {
|
||||
serverUrl: SYNC_SERVER_URL,
|
||||
},
|
||||
});
|
||||
|
||||
// Typed collection accessors
|
||||
export const tagCollection = tagLocalStore.collection<LocalTag>('tags');
|
||||
export const tagGroupCollection = tagLocalStore.collection<LocalTagGroup>('tagGroups');
|
||||
|
||||
// ─── Live Query Hooks ──────────────────────────────────────
|
||||
|
||||
/** All tags, sorted by sortOrder. Auto-updates on any change. */
|
||||
export function useAllTags() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await tagCollection.getAll(undefined, {
|
||||
sortBy: 'sortOrder',
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
return locals.map(toTag);
|
||||
}, [] as Tag[]);
|
||||
}
|
||||
|
||||
/** All tag groups, sorted by sortOrder. Auto-updates on any change. */
|
||||
export function useAllTagGroups() {
|
||||
return useLiveQueryWithDefault(async () => {
|
||||
const locals = await tagGroupCollection.getAll(undefined, {
|
||||
sortBy: 'sortOrder',
|
||||
sortDirection: 'asc',
|
||||
});
|
||||
return locals.map(toTagGroup);
|
||||
}, [] as TagGroup[]);
|
||||
}
|
||||
|
||||
// ─── Pure Query Helpers ────────────────────────────────────
|
||||
|
||||
export function getTagById(tags: Tag[], id: string): Tag | undefined {
|
||||
return tags.find((t) => t.id === id);
|
||||
}
|
||||
|
||||
export function getTagsByIds(tags: Tag[], ids: string[]): Tag[] {
|
||||
return tags.filter((t) => ids.includes(t.id));
|
||||
}
|
||||
|
||||
export function getTagColor(tags: Tag[], id: string): string {
|
||||
return tags.find((t) => t.id === id)?.color || '#6b7280';
|
||||
}
|
||||
|
||||
export function getTagsByGroup(tags: Tag[], groupId: string | null): Tag[] {
|
||||
return tags.filter((t) => (t.groupId || null) === groupId);
|
||||
}
|
||||
|
||||
// ─── Mutation Service ──────────────────────────────────────
|
||||
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
export const tagMutations = {
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
||||
// === Store Lifecycle ===
|
||||
|
||||
async initialize() {
|
||||
await tagLocalStore.initialize();
|
||||
},
|
||||
|
||||
startSync(getToken: () => Promise<string | null>) {
|
||||
tagLocalStore.startSync(getToken);
|
||||
},
|
||||
|
||||
stopSync() {
|
||||
tagLocalStore.stopSync();
|
||||
},
|
||||
|
||||
// === Tags ===
|
||||
|
||||
async createTag(data: CreateTagInput): Promise<Tag> {
|
||||
error = null;
|
||||
try {
|
||||
const count = await tagCollection.count();
|
||||
const newLocal: LocalTag = {
|
||||
id: crypto.randomUUID(),
|
||||
name: data.name,
|
||||
color: data.color ?? '#3B82F6',
|
||||
icon: data.icon ?? null,
|
||||
groupId: data.groupId ?? null,
|
||||
sortOrder: data.sortOrder ?? count,
|
||||
};
|
||||
const inserted = await tagCollection.insert(newLocal);
|
||||
return toTag(inserted);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create tag';
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async updateTag(id: string, data: UpdateTagInput): Promise<Tag> {
|
||||
error = null;
|
||||
try {
|
||||
const updated = await tagCollection.update(id, data as Partial<LocalTag>);
|
||||
if (updated) return toTag(updated);
|
||||
throw new Error('Tag not found');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update tag';
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteTag(id: string): Promise<void> {
|
||||
error = null;
|
||||
try {
|
||||
await tagCollection.delete(id);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete tag';
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
// === Groups ===
|
||||
|
||||
async createGroup(data: CreateTagGroupInput): Promise<TagGroup> {
|
||||
error = null;
|
||||
try {
|
||||
const count = await tagGroupCollection.count();
|
||||
const newLocal: LocalTagGroup = {
|
||||
id: crypto.randomUUID(),
|
||||
name: data.name,
|
||||
color: data.color ?? '#6b7280',
|
||||
icon: data.icon ?? null,
|
||||
sortOrder: data.sortOrder ?? count,
|
||||
};
|
||||
const inserted = await tagGroupCollection.insert(newLocal);
|
||||
return toTagGroup(inserted);
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to create tag group';
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async updateGroup(id: string, data: UpdateTagGroupInput): Promise<TagGroup> {
|
||||
error = null;
|
||||
try {
|
||||
const updated = await tagGroupCollection.update(id, data as Partial<LocalTagGroup>);
|
||||
if (updated) return toTagGroup(updated);
|
||||
throw new Error('Tag group not found');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to update tag group';
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async deleteGroup(id: string): Promise<void> {
|
||||
error = null;
|
||||
try {
|
||||
await tagGroupCollection.delete(id);
|
||||
// Clear groupId on tags in deleted group
|
||||
const tagsInGroup = await tagCollection.getAll({ groupId: id } as Partial<LocalTag>);
|
||||
for (const tag of tagsInGroup) {
|
||||
await tagCollection.update(tag.id, { groupId: null } as Partial<LocalTag>);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to delete tag group';
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async reorderGroups(ids: string[]): Promise<void> {
|
||||
error = null;
|
||||
try {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
await tagGroupCollection.update(ids[i], { sortOrder: i } as Partial<LocalTagGroup>);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to reorder groups';
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
};
|
||||
2685
pnpm-lock.yaml
generated
2685
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue