Mega push vol2

This commit is contained in:
AlacrisDevs
2026-02-05 01:36:06 +02:00
parent 1534e1f0af
commit 9af0ef5307
17 changed files with 1056 additions and 189 deletions

View File

@@ -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 />

View File

@@ -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

View File

@@ -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 ?? []
};
};

View File

@@ -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>

View File

@@ -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">

View File

@@ -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}

View File

@@ -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}

View File

@@ -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">