diff --git a/src/lib/components/calendar/Calendar.svelte b/src/lib/components/calendar/Calendar.svelte index 6d5c78a..cbb6dbc 100644 --- a/src/lib/components/calendar/Calendar.svelte +++ b/src/lib/components/calendar/Calendar.svelte @@ -1,28 +1,47 @@
-

{monthYear}

+

{headerTitle()}

+ +
+ + + +
-
- {#each weekDays as day} -
- {day} -
- {/each} + + {#if currentView === "month"} +
+ {#each weekDays as day} +
+ {day} +
+ {/each} - {#each days as day} - {@const dayEvents = getEventsForDay(day)} - {@const isToday = isSameDay(day, today)} - {@const inMonth = isCurrentMonth(day)} - + {/each} + {#if dayEvents.length > 3} +

+ +{dayEvents.length - 3} more +

+ {/if} +
+ + {/each} +
+ {/if} + + + {#if currentView === "week"} +
+ {#each weekDates as day} + {@const dayEvents = getEventsForDay(day)} + {@const isToday = isSameDay(day, today)} +
+
+
+ {weekDays[day.getDay()]} +
+
+ {day.getDate()} +
+
+
+ {#each dayEvents as event} + + {/each} +
-
- {#each dayEvents.slice(0, 3) as event} + {/each} +
+ {/if} + + + {#if currentView === "day"} + {@const dayEvents = getEventsForDay(currentDate)} +
+ {#if dayEvents.length === 0} +
+

No events for this day

+
+ {:else} +
+ {#each dayEvents as event} {/each} - {#if dayEvents.length > 3} -

+{dayEvents.length - 3} more

- {/if}
- - {/each} -
+ {/if} +
+ {/if}
diff --git a/src/lib/components/documents/Editor.svelte b/src/lib/components/documents/Editor.svelte index 83c858b..f9ac297 100644 --- a/src/lib/components/documents/Editor.svelte +++ b/src/lib/components/documents/Editor.svelte @@ -28,18 +28,36 @@ let element: HTMLDivElement; let editor: Editor | null = $state(null); + let saveStatus = $state<"idle" | "saving" | "saved" | "error">("idle"); let saveTimeout: ReturnType | null = null; + let statusTimeout: ReturnType | null = null; function triggerAutoSave() { if (saveTimeout) clearTimeout(saveTimeout); - saveTimeout = setTimeout(() => { - if (editor && onSave) { - onSave(editor.getJSON()); - } + saveStatus = "idle"; + saveTimeout = setTimeout(async () => { + await saveNow(); }, 1000); // Auto-save after 1 second of inactivity } + async function saveNow() { + if (editor && onSave) { + saveStatus = "saving"; + try { + await onSave(editor.getJSON()); + saveStatus = "saved"; + // Reset status after 2 seconds + if (statusTimeout) clearTimeout(statusTimeout); + statusTimeout = setTimeout(() => { + saveStatus = "idle"; + }, 2000); + } catch { + saveStatus = "error"; + } + } + } + onMount(() => { editor = new Editor({ element, @@ -69,6 +87,7 @@ onDestroy(() => { if (saveTimeout) clearTimeout(saveTimeout); + if (statusTimeout) clearTimeout(statusTimeout); editor?.destroy(); }); @@ -110,6 +129,67 @@
+ + +
+ {/if} {#if card.color} -
+
{/if} -

{card.title}

+

{card.title}

{#if card.description} -

{card.description}

+

+ {card.description} +

{/if} {#if card.due_date}
- + {formatDueDate(card.due_date)}
@@ -137,7 +187,13 @@ class="mt-2 w-full py-2 text-sm text-light/50 hover:text-light hover:bg-light/5 rounded-lg transition-colors flex items-center justify-center gap-1" onclick={() => onAddCard?.(column.id)} > - + @@ -152,7 +208,13 @@ class="flex-shrink-0 w-72 h-12 bg-light/5 hover:bg-light/10 rounded-xl flex items-center justify-center gap-2 text-light/50 hover:text-light transition-colors" onclick={() => onAddColumn?.()} > - + @@ -160,3 +222,24 @@ {/if}
+ + diff --git a/src/lib/components/ui/ToastContainer.svelte b/src/lib/components/ui/ToastContainer.svelte new file mode 100644 index 0000000..3b4d815 --- /dev/null +++ b/src/lib/components/ui/ToastContainer.svelte @@ -0,0 +1,14 @@ + + +
+ {#each $toasts as toast (toast.id)} + toasts.remove(toast.id)} + /> + {/each} +
diff --git a/src/lib/components/ui/index.ts b/src/lib/components/ui/index.ts index 68d8518..4d2c339 100644 --- a/src/lib/components/ui/index.ts +++ b/src/lib/components/ui/index.ts @@ -9,3 +9,4 @@ export { default as Modal } from './Modal.svelte'; export { default as Spinner } from './Spinner.svelte'; export { default as Toggle } from './Toggle.svelte'; export { default as Toast } from './Toast.svelte'; +export { default as ToastContainer } from './ToastContainer.svelte'; diff --git a/src/lib/stores/toast.ts b/src/lib/stores/toast.ts new file mode 100644 index 0000000..afcefa9 --- /dev/null +++ b/src/lib/stores/toast.ts @@ -0,0 +1,48 @@ +import { writable } from 'svelte/store'; + +export type ToastVariant = 'success' | 'error' | 'warning' | 'info'; + +export interface Toast { + id: string; + message: string; + variant: ToastVariant; + duration?: number; +} + +function createToastStore() { + const { subscribe, update } = writable([]); + + function add(message: string, variant: ToastVariant = 'info', duration = 5000) { + const id = crypto.randomUUID(); + const toast: Toast = { id, message, variant, duration }; + + update((toasts) => [...toasts, toast]); + + if (duration > 0) { + setTimeout(() => remove(id), duration); + } + + return id; + } + + function remove(id: string) { + update((toasts) => toasts.filter((t) => t.id !== id)); + } + + function clear() { + update(() => []); + } + + return { + subscribe, + add, + remove, + clear, + success: (message: string, duration?: number) => add(message, 'success', duration), + error: (message: string, duration?: number) => add(message, 'error', duration), + warning: (message: string, duration?: number) => add(message, 'warning', duration), + info: (message: string, duration?: number) => add(message, 'info', duration) + }; +} + +export const toasts = createToastStore(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 0b57e9d..48b6d92 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,6 +3,7 @@ import favicon from "$lib/assets/favicon.svg"; import { createClient } from "$lib/supabase"; import { setContext } from "svelte"; + import { ToastContainer } from "$lib/components/ui"; let { children, data } = $props(); @@ -12,3 +13,4 @@ {@render children()} + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 0fd0fca..98bb4a0 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -48,6 +48,10 @@ } + + Organizations | Root + +
{ .eq('org_id', org.id) .limit(10); + // Fetch recent activity + const { data: recentActivity } = await locals.supabase + .from('activity_log') + .select(` + id, + action, + entity_type, + entity_id, + entity_name, + created_at, + profiles:user_id ( + full_name, + email + ) + `) + .eq('org_id', org.id) + .order('created_at', { ascending: false }) + .limit(10); + return { org, role: membership.role, userRole: membership.role, - members: members ?? [] + members: members ?? [], + recentActivity: recentActivity ?? [] }; }; diff --git a/src/routes/[orgSlug]/+page.svelte b/src/routes/[orgSlug]/+page.svelte index 11a4f02..b005438 100644 --- a/src/routes/[orgSlug]/+page.svelte +++ b/src/routes/[orgSlug]/+page.svelte @@ -1,6 +1,15 @@ @@ -148,74 +179,114 @@

Recent Activity

-
- {#each recentActivity as activity} -
+ {#if data.recentActivity && data.recentActivity.length > 0} +
+ {#each data.recentActivity as activity} + {@const icon = getActivityIcon(activity.entity_type)}
- {#if activity.icon === "file"} - - - - - {:else if activity.icon === "kanban"} - - - - - - {:else if activity.icon === "user"} - - - - - {/if} -
-
-

- {activity.action} -

-

- {activity.entity} -

+
+ {#if icon === "file"} + + + + + {:else if icon === "kanban"} + + + + + + {:else if icon === "calendar"} + + + + + + + {:else if icon === "user"} + + + + + {:else} + + + + + {/if} +
+
+

+ {formatAction( + activity.action, + activity.entity_type, + )} +

+

+ {activity.entity_name || "Unknown"} +

+
+ {formatRelativeTime(activity.created_at)}
- {activity.time} -
- {/each} -
+ {/each} +
+ {:else} +
+

No recent activity

+
+ {/if}
diff --git a/src/routes/[orgSlug]/calendar/+page.svelte b/src/routes/[orgSlug]/calendar/+page.svelte index eac4dd6..5124a6d 100644 --- a/src/routes/[orgSlug]/calendar/+page.svelte +++ b/src/routes/[orgSlug]/calendar/+page.svelte @@ -129,6 +129,10 @@ } + + Calendar - {data.org.name} | Root + +
diff --git a/src/routes/[orgSlug]/documents/+page.svelte b/src/routes/[orgSlug]/documents/+page.svelte index 8594f89..018bdeb 100644 --- a/src/routes/[orgSlug]/documents/+page.svelte +++ b/src/routes/[orgSlug]/documents/+page.svelte @@ -37,6 +37,14 @@ } } + function handleDoubleClick(doc: Document) { + if (doc.type === "document") { + // Open document in new window + const url = `/${data.org.slug}/documents/${doc.id}`; + window.open(url, "_blank", "width=900,height=700"); + } + } + function handleAdd(folderId: string | null) { parentFolderId = folderId; showCreateModal = true; @@ -199,6 +207,7 @@ items={documentTree} selectedId={selectedDoc?.id ?? null} onSelect={handleSelect} + onDoubleClick={handleDoubleClick} onAdd={handleAdd} onMove={handleMove} onEdit={handleEdit} diff --git a/src/routes/[orgSlug]/kanban/+page.svelte b/src/routes/[orgSlug]/kanban/+page.svelte index 838b845..9216940 100644 --- a/src/routes/[orgSlug]/kanban/+page.svelte +++ b/src/routes/[orgSlug]/kanban/+page.svelte @@ -54,6 +54,9 @@ } let editingBoardId = $state(null); + let showAddColumnModal = $state(false); + let newColumnName = $state(""); + let sidebarCollapsed = $state(false); function openEditBoardModal(board: KanbanBoardType) { editingBoardId = board.id; @@ -105,6 +108,38 @@ showCardDetailModal = true; } + function handleAddColumn() { + newColumnName = ""; + showAddColumnModal = true; + } + + async function handleCreateColumn() { + if (!selectedBoard || !newColumnName.trim()) return; + + const position = selectedBoard.columns.length; + const { data: newColumn, error } = await supabase + .from("kanban_columns") + .insert({ + board_id: selectedBoard.id, + name: newColumnName.trim(), + position, + }) + .select() + .single(); + + if (!error && newColumn && selectedBoard) { + selectedBoard = { + ...selectedBoard, + columns: [ + ...selectedBoard.columns, + { ...newColumn, cards: [] as KanbanCard[] }, + ], + }; + } + showAddColumnModal = false; + newColumnName = ""; + } + function handleCardCreated(newCard: KanbanCard) { if (!selectedBoard) return; selectedBoard = { @@ -173,7 +208,23 @@ async function handleCardDelete(cardId: string) { if (!selectedBoard) return; - await loadBoard(selectedBoard.id); + + // Delete from database + const { error } = await supabase + .from("kanban_cards") + .delete() + .eq("id", cardId); + + if (!error) { + // Update local state + selectedBoard = { + ...selectedBoard, + columns: selectedBoard.columns.map((col) => ({ + ...col, + cards: col.cards.filter((c) => c.id !== cardId), + })), + }; + } } @@ -185,23 +236,48 @@
-