handleDragOver(e, column.id)}
ondragleave={handleDragLeave}
ondrop={(e) => handleDrop(e, column.id)}
@@ -93,37 +109,71 @@
{#each column.cards as card}
handleDragStart(e, card)}
onclick={() => onCardClick?.(card)}
- onkeydown={(e) => e.key === 'Enter' && onCardClick?.(card)}
+ onkeydown={(e) =>
+ e.key === "Enter" && onCardClick?.(card)}
role="listitem"
tabindex="0"
>
+ {#if canEdit}
+
+ {/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)}
>
-
+
+
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}
+
+ {/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 @@