Initial open source release

This commit is contained in:
Ronni Skansing
2025-08-21 16:14:09 +02:00
commit 11cf01f08e
488 changed files with 97180 additions and 0 deletions
@@ -0,0 +1,212 @@
<script>
import { menu } from '$lib/consts/navigation';
import { page } from '$app/stores';
import { scrollBarClassesVertical } from '$lib/utils/scrollbar';
import { shouldHideMenuItem } from '$lib/utils/common';
let isExpanded = false;
const icons = {
dashboard: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>`,
campaigns_overview: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 5h6c2 0 4 2 4 4v4c0 3-3 5-5 5 1.5-1.5 1.5-3 1.5-3" />
<path stroke-linecap="round" stroke-linejoin="round" d="M14.5 15l-2 2" />
<circle cx="14" cy="5" r="1" fill="currentColor" />
</svg>`,
campaign_templates: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>`,
ip_filters: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" />
</svg>
`,
webhooks: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" />
</svg>`,
recipients_overview: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>`,
recipient_groups: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
</svg>`,
domains_overview: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
</svg>`,
pages: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>`,
assets: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>`,
emails_overview: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
</svg>`,
attachments: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
</svg>`,
smtp_configurations: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0Zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 1 0-2.636 6.364M16.5 12V8.25" />
</svg>
`,
api_senders: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
</svg>
`
};
const getIconForRoute = (route) => {
const iconMap = {
'/dashboard/': 'dashboard',
'/campaign/': 'campaigns_overview',
'/campaign-template/': 'campaign_templates',
'/ip-filter/': 'ip_filters',
'/webhook/': 'webhooks',
'/recipient/': 'recipients_overview',
'/recipient/group/': 'recipient_groups',
'/domain/': 'domains_overview',
'/page/': 'pages',
'/asset/': 'assets',
'/email/': 'emails_overview',
'/attachment/': 'attachments',
'/smtp-configuration/': 'smtp_configurations',
'/api-sender/': 'api_senders'
};
return icons[iconMap[route] || 'dashboard']; // fallback to dashboard if route not found
};
</script>
<div class="flex">
<nav
class="hidden lg:flex flex-col transition-all fixed top-16 z-10 bg-gradient-to-b from-pc-darkblue to-indigo-400 rounded-br-lg overflow-y-auto overflow-x-hidden min-h-0 max-h-[calc(100vh-4rem)] box-content border-r-[1px] border-pc-darkblue"
class:w-40={isExpanded}
class:w-12={!isExpanded}
>
<div
class="sticky top-0 bg-highlight-blue/20 border-b w-full border-blue-700/30 transform-none"
>
<button
class="w-full flex items-center justify-center rounded-md hover:bg-blue-600/30 transition-colors group px-3 py-2"
on:click={() => (isExpanded = !isExpanded)}
>
<svg
class="text-blue-100 duration-200 w-6"
class:rotate-180={!isExpanded}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5"
/>
</svg>
</button>
</div>
<!-- Navigation Items -->
<div
class="flex flex-col py-4 flex-1 overflow-y-auto {scrollBarClassesVertical} [&::-webkit-scrollbar-track]:bg-cta-blue"
>
{#each menu as link}
{#if link.type === 'submenu'}
<div class="py-1 mt-4 first:mt-0">
{#if isExpanded}
<div class="px-3 py-2 text-xs font-semibold text-blue-100 uppercase tracking-wider">
{link.label}
</div>
{/if}
<div>
{#each link.items as item, i (i)}
<a
class="flex items-center px-3 py-2 text-sm transition-all duration-150 relative group
{$page.url.pathname === item.route
? 'text-white font-medium bg-active-blue shadow-md'
: 'text-blue-100 hover:shadow-md hover:bg-highlight-blue hover:text-white'}"
class:hidden={shouldHideMenuItem(item.route)}
draggable="false"
href={item.route}
title={item.label}
>
<!-- Icon -->
<div class="flex-shrink-0">
{@html getIconForRoute(item.route)}
</div>
{#if isExpanded}
<span class="ml-3 truncate">
{#if i === 0}
Overview
{:else if item.singleLabel}
{item.singleLabel}
{:else}
{item.label}
{/if}
</span>
{/if}
{#if $page.url.pathname === item.route}
<div class="absolute left-0 top-0 bottom-0 w-1 bg-white"></div>
{/if}
</a>
{/each}
</div>
</div>
{:else}
<a
class="flex items-center px-3 py-2 text-sm transition-all duration-150 relative group
{$page.url.pathname === link.route
? 'text-white font-medium bg-active-blue shadow-md'
: 'text-blue-100 hover:text-white'}"
draggable="false"
href={link.route}
>
<!-- Icon -->
<div class="flex-shrink-0">
{@html icons[link.label]}
</div>
{#if isExpanded}
<span class="ml-3 truncate">{link.label}</span>
{:else}
<div
class="absolute left-14 rounded bg-gray-900 text-white px-2 py-1 ml-6 text-sm
invisible opacity-0 -translate-x-3 group-hover:visible group-hover:opacity-100 group-hover:translate-x-0
transition-all duration-150 whitespace-nowrap z-50 shadow-lg"
>
{link.label}
</div>
{/if}
{#if $page.url.pathname === link.route}
<div class="absolute left-0 top-0 bottom-0 w-1 bg-white"></div>
{/if}
</a>
{/if}
{/each}
</div>
</nav>
<!-- Main Content -->
<div class="flex-1">
<slot />
</div>
</div>
@@ -0,0 +1,183 @@
<script>
import { AppStateService } from '$lib/service/appState';
import { onMount } from 'svelte';
import Logo from './Logo.svelte';
const appState = AppStateService.instance;
export let isProfileMenuVisible = false;
export let isMobileMenuVisible = false;
export let toggleChangeCompanyModal;
let isUpdateAvailable = false;
let isInstalled = false;
let context = {
current: '',
companyName: ''
};
let username = '';
/*
let updateURL = 'https://user.phishing.club/downloads';
if (import.meta.env.DEV) {
updateURL = 'https://localhost:8009/downloads';
}
*/
onMount(() => {
const unsub = appState.subscribe((s) => {
context = {
current: s.context.current,
companyName: s.context.companyName
};
isInstalled = s.installStatus === AppStateService.INSTALL.INSTALLED;
const u = appState?.getUser();
if (u.username) {
username = u.username;
}
isUpdateAvailable = s.isUpdateAvailable;
});
return () => {
unsub();
};
});
// check if there is a context in local storage
if (!context.companyName) {
try {
const ctxString = localStorage.getItem('context');
const ctx = JSON.parse(ctxString);
appState.setCompanyContext(ctx.id, ctx.name);
} catch (e) {
// do nothing failure to parse is expected if there is nothing
}
}
function getInitials(username) {
return username
.split(' ')
.map((word) => word.charAt(0))
.join('')
.toUpperCase()
.slice(0, 2);
}
function profilePattern(username) {
// Create consistent hash
const hash = username.split('').reduce((acc, char) => {
return char.charCodeAt(0) + ((acc << 5) - acc);
}, 0);
// Generate base colors
const hue = Math.abs(hash) % 360;
const colors = {
primary: `hsl(${hue}, 70%, 50%)`,
secondary: `hsl(${(hue + 120) % 360}, 70%, 50%)`,
accent: `hsl(${(hue + 240) % 360}, 70%, 50%)`
};
// Generate pattern parameters
const params = {
rotation: hash % 360,
segments: 6 + (hash % 6),
waves: 3 + (hash % 4),
amplitude: 5 + (hash % 10)
};
return { colors, params };
}
function generatePath(params, radius = 25, centerX = 25, centerY = 25) {
let path = '';
const points = [];
const steps = 100;
for (let i = 0; i <= steps; i++) {
const angle = (i / steps) * Math.PI * 2;
const segment = (angle * params.segments) % (Math.PI * 2);
const wave = Math.sin(angle * params.waves) * params.amplitude;
const r = radius + wave;
const x = centerX + r * Math.cos(angle + params.rotation * (Math.PI / 180));
const y = centerY + r * Math.sin(angle + params.rotation * (Math.PI / 180));
points.push({ x, y });
path += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
}
return { path, points };
}
$: pattern = profilePattern(username);
$: initials = getInitials(username || 'U');
</script>
<div class="sticky top-0 z-20 col-span-12 h-16 bg-pc-darkblue flex justify-between items-center">
<Logo />
{#if isInstalled}
<div class="hidden lg:flex flex-row items-center px-8 h-full justify-self-end">
{#if context.current === AppStateService.CONTEXT.COMPANY}
<p class="text-slate-300 uppercase font-bold text-lg mr-4">
{context.companyName}
</p>
{/if}
{#if isUpdateAvailable}
<a
class="flex items-center gap-2 mr-8 text-lg font-medium text-white bg-gradient-to-r from-indigo-500 to-purple-500 rounded-md px-4 py-2 transition-all duration-300 transform hover:-translate-y-0.5 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-offset-2 active:scale-95 fixed bottom-4 right-2 shadow-md shadow-black"
href={'/settings/update'}
>
<span class=""></span>
<span>Update Available</span>
</a>
{/if}
<div class="relative ml-10 flex items-center">
<button
id="toggle-profile-menu"
class="group flex items-center"
on:click={() => (isProfileMenuVisible = !isProfileMenuVisible)}
>
<!-- Main Circle with Initials -->
<div
class="w-10 h-10 rounded-full bg-cta-blue hover:bg-indigo-500 flex items-center justify-center text-white font-medium relative"
>
{initials}
<div class="absolute -bottom-1 -right-1 w-5 h-5"></div>
</div>
<!-- Dropdown Indicator -->
<svg
class="w-4 h-4 ml-2 text-gray-300 transition-transform duration-200 group-hover:text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div>
</div>
<button
class="flex w-14 mr-4 lg:hidden"
on:click={() => (isMobileMenuVisible = !isMobileMenuVisible)}
>
<img class="" src="/mob-menu-button.svg" alt="toggle mobile menu" />
</button>
{/if}
</div>
<style>
button {
filter: contrast(1.1) saturate(1.2);
}
button:hover {
filter: contrast(1.2) saturate(1.3);
}
</style>
@@ -0,0 +1,13 @@
<script>
import { goto } from '$app/navigation';
</script>
<div
on:click={() => goto('/dashboard/')}
on:keydown={(e) => e.key === 'Enter' && goto('/dashboard/')}
tabindex="0"
role="button"
class="flex items-center w-40 sm:w-40 md:w-42 lg:w-56 justify-center py-4 my-6 rounded-md ml-4"
>
<img draggable="false" src="/logo-white.svg" alt="logo" />
</div>
@@ -0,0 +1,17 @@
<script>
import { page } from '$app/stores';
export let href = '#';
export let hidden = false;
</script>
<a
class="py-2 px-4 text-white hover:bg-active-blue hover:text-white hover:rounded-md"
class:bg-highlight-blue={$page.url.pathname === href}
class:font-semibold={$page.url.pathname === href}
class:rounded-md={$page.url.pathname === href}
class:hidden
{href}
on:click
>
<slot />
</a>
@@ -0,0 +1,71 @@
<script>
import { page } from '$app/stores';
import { menu, mobileTopMenu } from '$lib/consts/navigation';
import MenuLink from './MenuLink.svelte';
import { shouldHideMenuItem } from '$lib/utils/common';
export let visible = false;
export let username = '';
export let onClickLogout;
</script>
{#if visible}
<div class="fixed top-0 left-0 w-full h-full bg-pc-darkblue z-40 overflow-y-auto pb-4">
<div class="flex justify-between h-16">
<img class="w-40 sm:w-40 md:w-42 lg:w-56 ml-4" src="/logo-white.svg" alt="logo" />
<button class="mr-4 w-14" on:click={() => (visible = !visible)}>
<img class="w-3/4" src="/mob-menu-close.svg" alt="close mobile menu" />
</button>
</div>
<div>
<div class="flex flex-col px-4 py-4 rounded-b-xl">
<div class="flex py-6 border-b-2 border-white mb-4">
<!-- <div class="bg-slate-50 w-16 h-16 rounded-full" /> -->
<div>
<h1 class="font-bold text-3xl ml-6 text-white">{username ?? ''}</h1>
<button
on:click={onClickLogout}
class="bg-cta-blue hover:bg-pc-lightblue uppercase font-bold ml-6 mt-2 py text-white rounded-md"
>
<p class="py px-8">Log Out</p>
</button>
</div>
</div>
<div class="flex flex-col text-white">
{#each mobileTopMenu as link}
<a
class="pl-5 py-2 hover:bg-cta-blue hover:text-white rounded-md"
class:bg-gray-600={$page.url.pathname === link.route}
class:hidden={shouldHideMenuItem(link.route)}
on:click={() => (visible = !visible)}
target={link.external ? '_blank' : '_self'}
href={link.route}>{link.label}</a
>
{/each}
</div>
</div>
</div>
<div>
<div class="flex flex-col bg-cta-blue px-4 pt-4">
{#each menu as link}
{#if link.type === 'submenu'}
<div class="text-white font-semibold text-xl">{link.label}</div>
{#each link.items as item, i (i)}
<MenuLink href={item.route} on:click={() => (visible = !visible)}>
{#if i === 0}
Overview
{:else if item.singleLabel}
{item.singleLabel}
{:else}
{item.label}
{/if}
</MenuLink>
{/each}
{:else}
<MenuLink href={link.route}>{link.label}</MenuLink>
{/if}
{/each}
</div>
</div>
</div>
{/if}
@@ -0,0 +1,64 @@
<script>
import { page } from '$app/stores';
import { fade } from 'svelte/transition';
import { topMenu } from '$lib/consts/navigation';
import { shouldHideMenuItem } from '$lib/utils/common';
export let logout;
export let visible = false;
const handleClickOutsideNavigation = (event) => {
const profileMenuElement = document.getElementById('profile-menu');
const profileToggleElement = document.getElementById('toggle-profile-menu');
const clickOutsideMenu = profileMenuElement && !profileMenuElement.contains(event.target);
const clickOutsideToggleButton =
profileToggleElement && !profileToggleElement.contains(event.target);
if (clickOutsideMenu && clickOutsideToggleButton) {
visible = false;
}
};
$: {
if (visible) {
// add event listener to listen for a click outside the nav element
document.addEventListener('click', handleClickOutsideNavigation);
} else {
// remove event listener
document.removeEventListener('click', handleClickOutsideNavigation);
}
}
</script>
{#if visible}
<nav
id="profile-menu"
class="lg:flex flex-col h-fit lg:col-start-10 lg:col-span-3 row-start-1 xl:col-start-11 xl:col-span-2 2xl:col-start-11 2xl:col-span-2 sticky top-20 z-30"
>
<div
class="flex flex-col bg-gradient-to-b from-cta-blue to-indigo-500 rounded-md"
transition:fade={{ duration: 150 }}
>
{#each topMenu as item}
<a
class="pl-5 py-2 text-white last:rounded-md first:rounded-t-md"
class:hover:shadow-md={$page.url.pathname !== item.route}
class:hover:bg-highlight-blue={$page.url.pathname !== item.route}
class:bg-active-blue={$page.url.pathname === item.route}
class:shadow-md={$page.url.pathname === item.route}
class:hidden={shouldHideMenuItem(item.route)}
target={item.external ? '_blank' : '_self'}
draggable="false"
on:click={() => {
visible = false;
}}
href={item.route}>{item.label}</a
>
{/each}
<button
on:click={logout}
class="bg-white uppercase font-bold hover:bg-pc-lightblue py-2 mx-4 my-4 rounded-md"
>
<p class="text-cta-blue py px-8">Log Out</p>
</button>
</div>
</nav>
{/if}