mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-19 08:57:15 +02:00
Initial open source release
This commit is contained in:
@@ -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}
|
||||
Reference in New Issue
Block a user