Mega push vol 5, working on messaging now

This commit is contained in:
AlacrisDevs
2026-02-07 01:31:55 +02:00
parent d8bbfd9dc3
commit e55881b38b
77 changed files with 8478 additions and 1554 deletions

View File

@@ -1,7 +1,15 @@
<script lang="ts">
import { page, navigating } from "$app/stores";
import { goto } from "$app/navigation";
import type { Snippet } from "svelte";
import { Avatar, Logo } from "$lib/components/ui";
import { getContext } from "svelte";
import { on } from "svelte/events";
import { Avatar, Logo, PageSkeleton } from "$lib/components/ui";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
import { hasPermission, type Permission } from "$lib/utils/permissions";
import { setContext } from "svelte";
import * as m from "$lib/paraglide/messages";
interface Member {
id: string;
@@ -15,6 +23,13 @@
};
}
interface UserProfile {
id: string;
email: string;
full_name: string | null;
avatar_url: string | null;
}
interface Props {
data: {
org: {
@@ -23,41 +38,96 @@
slug: string;
avatar_url?: string | null;
};
role: string;
userRole: string;
userPermissions: string[] | null;
members: Member[];
profile: UserProfile;
};
children: Snippet;
}
let { data, children }: Props = $props();
const supabase = getContext<SupabaseClient<Database>>("supabase");
const isAdmin = $derived(
data.userRole === "owner" || data.userRole === "admin",
);
// Provide a permission checker via context so any child component can use it
const canAccess = (permission: Permission | string): boolean =>
hasPermission(data.userRole, data.userPermissions, permission);
setContext("canAccess", canAccess);
// Sidebar collapses on all pages except org overview
const isOrgOverview = $derived($page.url.pathname === `/${data.org.slug}`);
let sidebarHovered = $state(false);
const sidebarCollapsed = $derived(!isOrgOverview && !sidebarHovered);
// User dropdown
let showUserMenu = $state(false);
let menuContainerEl = $state<HTMLElement | null>(null);
// Attach click-outside and Escape listeners only while menu is open.
// Uses svelte/events 'on' to respect Svelte 5 event delegation order.
$effect(() => {
if (!showUserMenu) return;
// Defer so the opening click doesn't immediately close the menu
const timer = setTimeout(() => {
cleanupClick = on(document, "click", (e: MouseEvent) => {
if (
menuContainerEl &&
!menuContainerEl.contains(e.target as Node)
) {
showUserMenu = false;
}
});
}, 0);
const cleanupKey = on(document, "keydown", (e: Event) => {
if ((e as KeyboardEvent).key === "Escape") showUserMenu = false;
});
let cleanupClick: (() => void) | undefined;
return () => {
clearTimeout(timer);
cleanupClick?.();
cleanupKey();
};
});
async function handleLogout() {
await supabase.auth.signOut();
goto("/");
}
const navItems = $derived([
{
href: `/${data.org.slug}/documents`,
label: "Files",
icon: "cloud",
},
{
href: `/${data.org.slug}/calendar`,
label: "Calendar",
icon: "calendar_today",
},
// Only show settings for admins
...(isAdmin
...(canAccess("documents.view")
? [
{
href: `/${data.org.slug}/documents`,
label: m.nav_files(),
icon: "cloud",
},
]
: []),
...(canAccess("calendar.view")
? [
{
href: `/${data.org.slug}/calendar`,
label: m.nav_calendar(),
icon: "calendar_today",
},
]
: []),
// Settings requires settings.view or admin role
...(canAccess("settings.view")
? [
{
href: `/${data.org.slug}/settings`,
label: "Settings",
label: m.nav_settings(),
icon: "settings",
},
]
@@ -110,7 +180,7 @@
<p
class="text-body-sm text-white font-body capitalize whitespace-nowrap"
>
{data.role}
{data.userRole}
</p>
</div>
</a>
@@ -152,10 +222,107 @@
{/each}
</nav>
<!-- Logo at bottom -->
<div class="mt-auto">
<a href="/" title="Back to organizations">
<Logo size={sidebarCollapsed ? "sm" : "md"} />
<!-- User Section + Logo at bottom -->
<div class="mt-auto flex flex-col gap-3">
<!-- User Avatar + Quick Menu -->
<div
class="relative user-menu-container"
bind:this={menuContainerEl}
>
<button
type="button"
class="flex items-center gap-2 p-1 rounded-[32px] hover:bg-dark transition-colors w-full"
onclick={() => (showUserMenu = !showUserMenu)}
aria-expanded={showUserMenu}
aria-haspopup="true"
>
<div
class="shrink-0 transition-all duration-300 {sidebarCollapsed
? 'w-8 h-8'
: 'w-10 h-10'}"
>
<Avatar
name={data.profile.full_name || data.profile.email}
src={data.profile.avatar_url}
size="sm"
/>
</div>
<div
class="min-w-0 flex-1 overflow-hidden text-left transition-all duration-300 {sidebarCollapsed
? 'opacity-0 max-w-0'
: 'opacity-100 max-w-[200px]'}"
>
<p
class="font-body text-body-sm text-white truncate whitespace-nowrap leading-tight"
>
{data.profile.full_name || "User"}
</p>
<p
class="font-body text-[11px] text-light/50 truncate whitespace-nowrap leading-tight"
>
{data.profile.email}
</p>
</div>
</button>
{#if showUserMenu}
<div
class="absolute bottom-full left-0 mb-2 py-1 bg-dark border border-light/10 rounded-xl shadow-xl min-w-[200px] z-50"
>
<a
href="/{data.org.slug}/account"
class="flex items-center gap-3 px-3 py-2 text-sm text-light hover:bg-light/5 transition-colors"
onclick={() => (showUserMenu = false)}
>
<span
class="material-symbols-rounded text-light/50"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>
person
</span>
<span>{m.user_menu_account_settings()}</span>
</a>
<a
href="/"
class="flex items-center gap-3 px-3 py-2 text-sm text-light hover:bg-light/5 transition-colors"
onclick={() => (showUserMenu = false)}
>
<span
class="material-symbols-rounded text-light/50"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>
swap_horiz
</span>
<span>{m.user_menu_switch_org()}</span>
</a>
<div class="border-t border-light/10 my-1"></div>
<button
type="button"
class="w-full flex items-center gap-3 px-3 py-2 text-sm text-error hover:bg-error/10 transition-colors"
onclick={handleLogout}
>
<span
class="material-symbols-rounded text-error/60"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>
logout
</span>
<span>{m.user_menu_logout()}</span>
</button>
</div>
{/if}
</div>
<!-- Logo -->
<a
href="/"
title="Back to organizations"
class="flex items-center justify-center"
>
<Logo
size={sidebarCollapsed ? "sm" : "md"}
showText={!sidebarCollapsed}
/>
</a>
</div>
</aside>
@@ -163,17 +330,19 @@
<!-- Main Content Area -->
<main class="flex-1 bg-night rounded-[32px] overflow-auto relative">
{#if $navigating}
<div
class="absolute inset-0 z-10 flex items-center justify-center bg-night/80 backdrop-blur-sm"
>
<span
class="material-symbols-rounded text-primary animate-spin"
style="font-size: 40px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 40;"
>
progress_activity
</span>
</div>
{@const target = $navigating.to?.url.pathname ?? ""}
{@const skeletonVariant = target.includes("/kanban")
? "kanban"
: target.includes("/documents")
? "files"
: target.includes("/calendar")
? "calendar"
: target.includes("/settings")
? "settings"
: "default"}
<PageSkeleton variant={skeletonVariant} />
{:else}
{@render children()}
{/if}
{@render children()}
</main>
</div>