Files
console/src/lib/layout/shell.svelte
T
2025-02-03 17:22:29 +01:00

215 lines
6.4 KiB
Svelte

<script lang="ts">
import { beforeNavigate } from '$app/navigation';
import { Navbar, Sidebar } from '$lib/components';
import { page } from '$app/stores';
import { log } from '$lib/stores/logs';
import { wizard } from '$lib/stores/wizard';
import { activeHeaderAlert } from '$routes/(console)/store';
import { type ComponentType, setContext } from 'svelte';
import { writable } from 'svelte/store';
import { showSubNavigation } from '$lib/stores/layout';
import { organization, organizationList } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { tierToPlan } from '$lib/stores/billing';
import { type Models } from '@appwrite.io/console';
import { isCloud } from '$lib/system';
import SideNavigation from '$lib/layout/navigation.svelte';
import { isTabletViewport } from '$lib/stores/viewport';
import { hasOnboardingDismissed } from '$lib/helpers/onboarding';
export let showSideNavigation = false;
export let showHeader = true;
export let showFooter = true;
export let loadedProjects: Array<{ name: string; $id: string; isSelected: boolean }> = [];
export let projects: Array<Models.Project> = [];
$: selectedProject = loadedProjects.find((project) => project.isSelected);
let y: number;
let showContentTransition = true;
page.subscribe(({ url }) => {
$showSubNavigation = url.searchParams.get('openNavbar') === 'true';
showContentTransition = !url.pathname.includes('organization');
});
/**
* Cancel navigation when wizard is open and triggered by popstate
*/
beforeNavigate((n) => {
if (n.willUnload) return;
if (!($wizard.show || $wizard.cover)) return;
if (n.type === 'popstate') {
n.cancel();
}
if (n.type !== 'leave') {
wizard.hide();
}
});
const isNarrow = setContext('isNarrow', writable(false));
const hasSubNavigation = setContext('hasSubNavigation', writable(false));
$: sideSize = $hasSubNavigation ? ($isNarrow ? '17rem' : '25rem') : '12.5rem';
$: navbarProps = {
logo: {
src: 'https://appwrite.io/images/logos/logo.svg',
alt: 'Logo Appwrite'
},
avatar: sdk.forConsole.avatars.getInitials($user?.name, 80, 80).toString(),
organizations: $organizationList.teams.map((org) => {
return {
name: org.name,
$id: org.$id,
tierName: isCloud ? tierToPlan(org.billingPlan).name : null,
isSelected: $organization?.$id === org.$id,
projects: loadedProjects
};
})
};
let sideBarIsOpen = false;
let showAccountMenu = false;
let subNavigation: undefined | ComponentType = $page.data.subNavigation;
let state: undefined | 'open' | 'closed' | 'icons' = 'closed';
$: state = sideBarIsOpen ? 'open' : 'closed';
$: state = !$isTabletViewport ? 'icons' : sideBarIsOpen ? 'open' : 'closed';
function handleResize() {
sideBarIsOpen = false;
showAccountMenu = false;
}
$: progressCard = function getProgressCard() {
if (selectedProject && !hasOnboardingDismissed(selectedProject.$id)) {
const currentProject = projects.find((project) => project.$id === selectedProject.$id);
return {
title: 'Get started',
percentage: currentProject && currentProject.platforms.length ? 100 : 33
};
}
return undefined;
};
</script>
<svelte:window bind:scrollY={y} on:resize={handleResize} />
<main
class:grid-with-side={showSideNavigation}
class:is-open={$showSubNavigation}
class:u-hide={$wizard.show || $log.show || $wizard.cover}
class:is-fixed-layout={$activeHeaderAlert?.show}
style:--p-side-size={sideSize}>
{#if showHeader}
<Navbar {...navbarProps} bind:sideBarIsOpen bind:showAccountMenu />
{/if}
<Sidebar
project={selectedProject}
progressCard={progressCard()}
avatar={navbarProps.avatar}
bind:subNavigation
bind:sideBarIsOpen
bind:showAccountMenu
bind:state />
<SideNavigation bind:state bind:subNavigation />
<div
class="content"
class:has-transition={showContentTransition}
class:icons-content={state === 'icons'}
class:no-sidebar={!showSideNavigation}>
<section class="main-content" data-test={showSideNavigation}>
{#if $activeHeaderAlert?.show}
<svelte:component this={$activeHeaderAlert.component} />
{/if}
{#if $page.data?.header}
<svelte:component this={$page.data.header} />
{/if}
<slot />
{#if showFooter && showSideNavigation}
<slot name="footer" />
{/if}
</section>
{#if showFooter && !showSideNavigation}
<slot name="footer" />
{/if}
</div>
<button
class:overlay={sideBarIsOpen}
on:click={() => {
sideBarIsOpen = false;
}}></button>
</main>
<style lang="scss">
.content {
width: 100%;
margin-block-start: 48px;
@media (min-width: 1024px) {
width: 100%;
padding-left: 190px;
&.icons-content {
padding-left: 54px;
}
}
}
.has-transition {
@media (min-width: 1024px) {
transition: all 0.3s ease-in-out;
}
}
.no-sidebar {
padding-left: 0;
}
.main-content {
min-height: calc(100vh - 30px);
}
:global(main:has(.level-2-nav)) {
.main-content {
@media (min-width: 1024px) {
padding-left: 200px;
}
}
}
.overlay {
position: fixed;
width: 100vw;
height: 100vh;
right: 0;
top: 0;
z-index: 10;
background-color: #56565c1a;
backdrop-filter: blur(5px);
transition: backdrop-filter 0.2s ease-in-out;
@media (min-width: 1024px) {
display: none;
}
}
main {
min-height: 100vh;
&:not(.grid-with-side) {
display: flex;
flex-direction: column;
}
}
@media (min-width: 1199px) {
.grid-with-side {
grid-template-columns: auto 1fr !important;
}
}
</style>