Mega push vol2
This commit is contained in:
@@ -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 @@
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
{@render children()}
|
||||
<ToastContainer />
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Organizations | Root</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-dark">
|
||||
<header class="border-b border-light/10 bg-surface">
|
||||
<div
|
||||
|
||||
@@ -46,10 +46,30 @@ export const load: LayoutServerLoad = async ({ params, locals }) => {
|
||||
.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 ?? []
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Card } from "$lib/components/ui";
|
||||
|
||||
interface ActivityItem {
|
||||
id: string;
|
||||
action: string;
|
||||
entity_type: string;
|
||||
entity_name: string | null;
|
||||
created_at: string;
|
||||
profiles: { full_name: string | null; email: string } | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
org: { id: string; name: string; slug: string };
|
||||
@@ -11,6 +20,7 @@
|
||||
role: string;
|
||||
profiles: { full_name: string | null; email: string };
|
||||
}>;
|
||||
recentActivity?: ActivityItem[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,30 +47,51 @@
|
||||
},
|
||||
];
|
||||
|
||||
// Mock recent activity - will be replaced with real data from activity_log table
|
||||
const recentActivity = [
|
||||
{
|
||||
id: "1",
|
||||
action: "Created document",
|
||||
entity: "Project Brief",
|
||||
time: "2 hours ago",
|
||||
icon: "file",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
action: "Updated kanban card",
|
||||
entity: "Design Review",
|
||||
time: "4 hours ago",
|
||||
icon: "kanban",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
action: "Added team member",
|
||||
entity: "New Developer",
|
||||
time: "1 day ago",
|
||||
icon: "user",
|
||||
},
|
||||
];
|
||||
// Get icon based on entity type
|
||||
function getActivityIcon(entityType: string): string {
|
||||
switch (entityType) {
|
||||
case "document":
|
||||
return "file";
|
||||
case "kanban_card":
|
||||
case "kanban_board":
|
||||
return "kanban";
|
||||
case "calendar_event":
|
||||
return "calendar";
|
||||
case "member":
|
||||
return "user";
|
||||
default:
|
||||
return "activity";
|
||||
}
|
||||
}
|
||||
|
||||
// Format relative time
|
||||
function formatRelativeTime(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return "Just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 7) return `${days}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
// Format action text
|
||||
function formatAction(action: string, entityType: string): string {
|
||||
const typeMap: Record<string, string> = {
|
||||
document: "document",
|
||||
kanban_card: "task",
|
||||
kanban_board: "board",
|
||||
calendar_event: "event",
|
||||
member: "member",
|
||||
};
|
||||
const type = typeMap[entityType] || entityType;
|
||||
return `${action.charAt(0).toUpperCase() + action.slice(1)} ${type}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -148,74 +179,114 @@
|
||||
<section>
|
||||
<h2 class="text-xl font-heading text-light mb-4">Recent Activity</h2>
|
||||
<Card>
|
||||
<div class="divide-y divide-light/10">
|
||||
{#each recentActivity as activity}
|
||||
<div
|
||||
class="flex items-center gap-4 p-4 hover:bg-light/5 transition-colors"
|
||||
>
|
||||
{#if data.recentActivity && data.recentActivity.length > 0}
|
||||
<div class="divide-y divide-light/10">
|
||||
{#each data.recentActivity as activity}
|
||||
{@const icon = getActivityIcon(activity.entity_type)}
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center shrink-0"
|
||||
class="flex items-center gap-4 p-4 hover:bg-light/5 transition-colors"
|
||||
>
|
||||
{#if activity.icon === "file"}
|
||||
<svg
|
||||
class="w-5 h-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<polyline points="14,2 14,8 20,8" />
|
||||
</svg>
|
||||
{:else if activity.icon === "kanban"}
|
||||
<svg
|
||||
class="w-5 h-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
/>
|
||||
<line x1="9" y1="3" x2="9" y2="21" />
|
||||
<line x1="15" y1="3" x2="15" y2="21" />
|
||||
</svg>
|
||||
{:else if activity.icon === "user"}
|
||||
<svg
|
||||
class="w-5 h-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
|
||||
/>
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
{/if}
|
||||
<div
|
||||
class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center shrink-0"
|
||||
>
|
||||
{#if icon === "file"}
|
||||
<svg
|
||||
class="w-5 h-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<polyline points="14,2 14,8 20,8" />
|
||||
</svg>
|
||||
{:else if icon === "kanban"}
|
||||
<svg
|
||||
class="w-5 h-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
/>
|
||||
<line x1="9" y1="3" x2="9" y2="21" />
|
||||
<line x1="15" y1="3" x2="15" y2="21" />
|
||||
</svg>
|
||||
{:else if icon === "calendar"}
|
||||
<svg
|
||||
class="w-5 h-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="4"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
/>
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
<line x1="8" y1="2" x2="8" y2="6" />
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
{:else if icon === "user"}
|
||||
<svg
|
||||
class="w-5 h-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
|
||||
/>
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class="w-5 h-5 text-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12,6 12,12 16,14" />
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-light font-medium">
|
||||
{formatAction(
|
||||
activity.action,
|
||||
activity.entity_type,
|
||||
)}
|
||||
</p>
|
||||
<p class="text-sm text-light/50 truncate">
|
||||
{activity.entity_name || "Unknown"}
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs text-light/40 shrink-0"
|
||||
>{formatRelativeTime(activity.created_at)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-light font-medium">
|
||||
{activity.action}
|
||||
</p>
|
||||
<p class="text-sm text-light/50 truncate">
|
||||
{activity.entity}
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs text-light/40 shrink-0"
|
||||
>{activity.time}</span
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-6 text-center text-light/50">
|
||||
<p>No recent activity</p>
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -129,6 +129,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Calendar - {data.org.name} | Root</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="p-6 h-full overflow-auto">
|
||||
<header class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -54,6 +54,9 @@
|
||||
}
|
||||
|
||||
let editingBoardId = $state<string | null>(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),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -185,23 +236,48 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex h-full">
|
||||
<aside class="w-64 border-r border-light/10 flex flex-col">
|
||||
<aside
|
||||
class="{sidebarCollapsed
|
||||
? 'w-12'
|
||||
: 'w-64'} border-r border-light/10 flex flex-col transition-all duration-200"
|
||||
>
|
||||
<div
|
||||
class="p-4 border-b border-light/10 flex items-center justify-between"
|
||||
class="p-2 border-b border-light/10 flex items-center {sidebarCollapsed
|
||||
? 'justify-center'
|
||||
: 'justify-between gap-2'}"
|
||||
>
|
||||
<h2 class="font-semibold text-light">Boards</h2>
|
||||
<Button size="sm" onclick={() => (showCreateBoardModal = true)}>
|
||||
{#if !sidebarCollapsed}
|
||||
<h2 class="font-semibold text-light px-2">Boards</h2>
|
||||
<Button size="sm" onclick={() => (showCreateBoardModal = true)}>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</Button>
|
||||
{/if}
|
||||
<button
|
||||
class="p-1.5 rounded-lg hover:bg-light/10 text-light/50 hover:text-light transition-colors"
|
||||
onclick={() => (sidebarCollapsed = !sidebarCollapsed)}
|
||||
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
class="w-4 h-4 transition-transform {sidebarCollapsed
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<path d="m11 17-5-5 5-5M17 17l-5-5 5-5" />
|
||||
</svg>
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
||||
@@ -285,6 +361,8 @@
|
||||
onCardClick={handleCardClick}
|
||||
onCardMove={handleCardMove}
|
||||
onAddCard={handleAddCard}
|
||||
onAddColumn={handleAddColumn}
|
||||
onDeleteCard={handleCardDelete}
|
||||
/>
|
||||
{:else}
|
||||
<div class="h-full flex items-center justify-center text-light/40">
|
||||
@@ -352,6 +430,29 @@
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={showAddColumnModal}
|
||||
onClose={() => (showAddColumnModal = false)}
|
||||
title="Add Column"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<Input
|
||||
label="Column Name"
|
||||
bind:value={newColumnName}
|
||||
placeholder="e.g. To Do, In Progress, Done"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="ghost" onclick={() => (showAddColumnModal = false)}
|
||||
>Cancel</Button
|
||||
>
|
||||
<Button
|
||||
onclick={handleCreateColumn}
|
||||
disabled={!newColumnName.trim()}>Create</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<CardDetailModal
|
||||
card={selectedCard}
|
||||
isOpen={showCardDetailModal}
|
||||
|
||||
@@ -417,8 +417,33 @@
|
||||
await supabase.from("organizations").delete().eq("id", data.org.id);
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
async function leaveOrganization() {
|
||||
if (isOwner) {
|
||||
alert(
|
||||
"Owners cannot leave. Transfer ownership first or delete the organization.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Are you sure you want to leave ${data.org.name}?`))
|
||||
return;
|
||||
|
||||
const { error } = await supabase
|
||||
.from("org_members")
|
||||
.delete()
|
||||
.eq("org_id", data.org.id)
|
||||
.eq("user_id", data.user?.id);
|
||||
|
||||
if (!error) {
|
||||
window.location.href = "/";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Settings - {data.org.name} | Root</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="p-6 h-full overflow-auto">
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-light">Settings</h1>
|
||||
@@ -512,6 +537,27 @@
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{#if !isOwner}
|
||||
<Card>
|
||||
<div class="p-6 border-l-4 border-warning">
|
||||
<h2 class="text-lg font-semibold text-warning">
|
||||
Leave Organization
|
||||
</h2>
|
||||
<p class="text-sm text-light/50 mt-1">
|
||||
Leave this organization. You will need to be
|
||||
re-invited to rejoin.
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={leaveOrganization}
|
||||
>Leave {data.org.name}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if isOwner}
|
||||
<Card>
|
||||
<div class="p-6 border-l-4 border-error">
|
||||
|
||||
Reference in New Issue
Block a user