Mega push vol 5, working on messaging now
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user