ShyyTAUI/src/routes/+layout.svelte
2025-09-24 13:41:04 +02:00

738 lines
No EOL
20 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { discordDataStore, discordTokenStore, authTokenStore } from '$lib/stores';
import { slide } from 'svelte/transition';
let isAuthenticated = false;
let profileDropdownOpen = false;
let mobileMenuOpen = false;
let userProfile: { name: string; avatar?: string } | null = null;
onMount(() => {
// Check authentication on mount
isAuthenticated = !!$authTokenStore && !!$discordDataStore;
if (isAuthenticated) {
userProfile = {
name: $discordDataStore.global_name,
avatar: `https://cdn.discordapp.com/avatars/${$discordDataStore.id}/${$discordDataStore.avatar}.png`
};
}
});
function toggleProfileDropdown() {
profileDropdownOpen = !profileDropdownOpen;
}
function toggleMobileMenu() {
mobileMenuOpen = !mobileMenuOpen;
}
function logout() {
authTokenStore.set(null);
discordDataStore.set(null);
discordTokenStore.set(null);
isAuthenticated = false;
userProfile = null;
profileDropdownOpen = false;
mobileMenuOpen = false;
goto('/');
}
function handleClickOutside(event: MouseEvent) {
const dropdown = document.querySelector('.profile-container');
const mobileMenu = document.querySelector('.mobile-menu');
const hamburger = document.querySelector('.hamburger-button');
if (dropdown && !dropdown.contains(event.target as Node) && profileDropdownOpen) {
profileDropdownOpen = false;
}
if (mobileMenu && !mobileMenu.contains(event.target as Node) &&
hamburger && !hamburger.contains(event.target as Node) && mobileMenuOpen) {
mobileMenuOpen = false;
}
}
function handleNavClick() {
mobileMenuOpen = false;
}
</script>
<svelte:window on:click={handleClickOutside} />
<div class="layout dark-mode">
<header>
<div class="navbar">
<div class="navbar-section logo-section">
<img class="logo-svg" src="/assets/LogoTextC_DevW.svg" alt="">
</div>
<!-- Desktop Navigation -->
<nav class="navbar-section nav-links desktop-nav">
<a href="/" class:active={$page.url.pathname === '/'}>
<span>Home</span>
<div class="hover-indicator"></div>
</a>
<a href="/tournaments" class:active={$page.url.pathname === '/tournaments'}>
<span>View Tournaments</span>
<div class="hover-indicator"></div>
</a>
<a href="/authTokens" class:active={$page.url.pathname === '/authTokens'}>
<span>My Tokens</span>
<div class="hover-indicator"></div>
</a>
</nav>
<!-- Desktop Profile -->
<div class="navbar-section profile-container desktop-profile">
<button class="profile-button" on:click={toggleProfileDropdown}>
{#if isAuthenticated && userProfile?.avatar}
<img src={userProfile.avatar} alt="Profile" class="profile-image" />
{:else}
<span class="material-icons">account_circle</span>
<span class="login-text">{isAuthenticated ? userProfile?.name : 'Login'}</span>
{/if}
</button>
{#if profileDropdownOpen}
<div class="profile-dropdown" transition:slide={{duration: 200}}>
{#if isAuthenticated}
<a href="/authTokens" class="dropdown-item" on:click={handleNavClick}>
<span class="material-icons">vpn_key</span>
<span>Manage Auth (MAT)</span>
</a>
<div class="divider"></div>
<button class="dropdown-item logout" on:click={logout}>
<span class="material-icons">exit_to_app</span>
<span>Logout</span>
</button>
{:else}
<a href="/discordAuth" class="dropdown-item" on:click={handleNavClick}>
<span class="material-icons">login</span>
<span>Login</span>
</a>
{/if}
</div>
{/if}
</div>
<!-- Mobile Hamburger Menu -->
<button class="hamburger-button mobile-only" on:click={toggleMobileMenu}>
<span class="material-icons">{mobileMenuOpen ? 'close' : 'menu'}</span>
</button>
</div>
<!-- Mobile Menu -->
{#if mobileMenuOpen}
<div class="mobile-menu" transition:slide={{duration: 300}}>
<nav class="mobile-nav-links">
<a href="/" class:active={$page.url.pathname === '/'} on:click={handleNavClick}>
<span class="material-icons">home</span>
<span>Home</span>
</a>
<a href="/tournaments" class:active={$page.url.pathname === '/tournaments'} on:click={handleNavClick}>
<span class="material-icons">sports_esports</span>
<span>View Tournaments</span>
</a>
<a href="/authTokens" class:active={$page.url.pathname === '/authTokens'} on:click={handleNavClick}>
<span class="material-icons">vpn_key</span>
<span>My Tokens</span>
</a>
</nav>
<div class="mobile-profile-section">
{#if isAuthenticated}
<div class="mobile-user-info">
{#if userProfile?.avatar}
<img src={userProfile.avatar} alt="Profile" class="mobile-profile-image" />
{:else}
<span class="material-icons">account_circle</span>
{/if}
<span class="mobile-user-name">{userProfile?.name}</span>
</div>
<div class="mobile-profile-divider"></div>
<button class="mobile-logout-button" on:click={logout}>
<span class="material-icons">exit_to_app</span>
<span>Logout</span>
</button>
{:else}
<a href="/discordAuth" class="mobile-login-button" on:click={handleNavClick}>
<span class="material-icons">login</span>
<span>Login</span>
</a>
{/if}
</div>
</div>
{/if}
</header>
<main>
<div class="content-container">
<slot />
</div>
</main>
<footer>
<div class="footer-content">
<div class="footer-section brand">
<h3>Tournament Assistant</h3>
<p>Helping you manage tournaments with ease</p>
</div>
<div class="footer-section links">
<h4>Links</h4>
<div class="footer-links">
<a href="https://github.com/ServerBP" target="_blank" rel="noopener noreferrer">
<span class="material-icons">code</span>
GitHub
</a>
<a href="https://tournamentassistant.net" target="_blank" rel="noopener noreferrer">
<span class="material-icons">sports_esports</span>
Tournament Assistant
</a>
<a href="/documentation" target="_blank" rel="noopener noreferrer">
<span class="material-icons">menu_book</span>
Documentation
</a>
</div>
</div>
<div class="footer-section contact">
<h4>Contact</h4>
<a href="mailto:support@beatkhana.com" class="contact-link">
<span class="material-icons">email</span>
support@beatkhana.com
</a>
<a href="https://discord.gg/AnkmKk6AD8" class="contact-link">
<span class="material-icons">forum</span>
Join our Discord
</a>
</div>
</div>
<div class="footer-bottom">
<div class="copyright">
&copy; {new Date().getFullYear()} Luna & Moon. All rights reserved.
</div>
</div>
</footer>
</div>
<style>
/* Dark mode variables */
:root {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--bg-tertiary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #b3b3b3;
--accent-color: #4f46e5;
--accent-hover: #6366f1;
--accent-glow: rgba(99, 102, 241, 0.5);
--danger-color: #ef4444;
--danger-hover: #dc2626;
--border-color: #333333;
--navbar-height: 4rem;
--footer-bg: #191919;
--success-color: #22c55e;
--warning-color: #f59e0b;
--error-color: #ef4444;
--comfortable-red: #d9534f;
--comfortable-red-hover: #c9302c;
--pick-green: #28a745;
--pick-green-hover: #218838;
}
/* Global styles */
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
/* Header and navbar */
header {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.navbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: var(--navbar-height);
padding: 0 1.5rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.navbar-section {
display: flex;
align-items: center;
}
.logo-section {
justify-content: flex-start;
}
.logo-svg {
height: 40px;
width: auto;
max-width: none;
}
/* Desktop Navigation */
.desktop-nav {
justify-content: center;
gap: 2rem;
height: 100%;
}
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
font-weight: 500;
padding: 0 0.75rem;
height: 100%;
display: flex;
align-items: center;
position: relative;
transition: color 0.2s ease;
}
.nav-links a:hover {
color: var(--text-primary);
}
.nav-links a.active {
color: var(--text-primary);
}
.nav-links a .hover-indicator {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 3px;
background: linear-gradient(90deg, transparent, var(--accent-color), transparent);
transition: width 0.3s ease;
opacity: 0;
}
.nav-links a:hover .hover-indicator {
width: 100%;
opacity: 1;
box-shadow: 0 0 8px var(--accent-glow);
}
.nav-links a.active .hover-indicator {
width: 100%;
opacity: 1;
box-shadow: 0 0 12px var(--accent-glow);
background: linear-gradient(90deg, transparent, var(--accent-hover), transparent);
}
/* Desktop Profile */
.desktop-profile {
position: relative;
justify-content: flex-end;
}
.profile-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background-color: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
border-radius: 0.375rem;
transition: background-color 0.2s ease;
}
.profile-button:hover {
background-color: var(--bg-tertiary);
}
.profile-image {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--accent-color);
}
.material-icons {
font-size: 1.5rem;
}
.login-text {
font-weight: 500;
}
/* Profile dropdown */
.profile-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0.75rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
width: 220px;
z-index: 10;
overflow: hidden;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: var(--text-primary);
text-decoration: none;
transition: background-color 0.2s ease;
width: 100%;
text-align: left;
border: none;
background: none;
font-size: 0.875rem;
cursor: pointer;
}
.dropdown-item:hover {
background-color: var(--bg-tertiary);
}
.dropdown-item.logout {
color: var(--danger-color);
}
.dropdown-item.logout:hover {
background-color: rgba(239, 68, 68, 0.1);
}
.divider {
height: 1px;
background-color: var(--border-color);
margin: 0.5rem 0;
}
/* Mobile-specific styles */
.mobile-only {
display: none;
}
@media (min-width: 769px) {
.mobile-only {
display: none !important;
}
}
.hamburger-button {
background: none;
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 0.5rem;
border-radius: 0.375rem;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.hamburger-button:hover {
background-color: var(--bg-tertiary);
}
.mobile-menu {
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: 1rem 0;
}
.mobile-nav-links {
display: flex;
flex-direction: column;
padding: 0 1.5rem;
}
.mobile-nav-links a {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 0;
color: var(--text-secondary);
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.mobile-nav-links a:last-child {
border-bottom: none;
}
.mobile-nav-links a:hover,
.mobile-nav-links a.active {
color: var(--accent-color);
}
.mobile-nav-links .material-icons {
font-size: 1.25rem;
}
.mobile-profile-section {
margin-top: 1rem;
padding: 1rem 1.5rem 0;
border-top: 1px solid var(--border-color);
}
.mobile-user-info {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.mobile-profile-image {
width: 2rem;
height: 2rem;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--accent-color);
}
.mobile-user-name {
color: var(--text-primary);
font-weight: 500;
}
.mobile-profile-divider {
height: 1px;
background-color: var(--border-color);
margin: 1rem 0;
}
.mobile-logout-button,
.mobile-login-button {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 0;
background: none;
border: none;
color: var(--danger-color);
text-decoration: none;
font-size: 1rem;
cursor: pointer;
width: 100%;
text-align: left;
transition: color 0.2s ease;
}
.mobile-login-button {
color: var(--accent-color);
}
.mobile-logout-button:hover {
color: var(--danger-hover);
}
.mobile-login-button:hover {
color: var(--accent-hover);
}
/* Main content */
main {
flex: 1;
padding: 2rem 1.5rem;
}
.content-container {
max-width: 1250px;
margin: 0 auto;
width: 100%;
overflow-y: visible;
}
/* Footer */
footer {
background-color: var(--footer-bg);
border-top: 1px solid var(--border-color);
margin-top: auto;
padding-top: 2.5rem;
}
.footer-content {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
padding: 0 1.5rem 2.5rem;
}
.footer-section h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.footer-section h4 {
font-size: 1rem;
margin-bottom: 1rem;
color: var(--accent-color);
font-weight: 600;
}
.footer-section p {
color: var(--text-secondary);
font-size: 0.875rem;
margin-bottom: 1rem;
line-height: 1.5;
}
.footer-links {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.footer-links a, .contact-link {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s ease;
font-size: 0.875rem;
}
.footer-links a:hover, .contact-link:hover {
color: var(--text-primary);
}
.footer-links .material-icons, .contact-link .material-icons {
font-size: 1.125rem;
}
.contact-link {
margin-bottom: 0.75rem;
}
.footer-bottom {
background-color: rgba(0, 0, 0, 0.2);
padding: 1rem 0;
text-align: center;
}
.copyright {
color: var(--text-secondary);
font-size: 0.75rem;
}
/* Media queries */
@media (max-width: 768px) {
/* Hide desktop navigation and profile */
.desktop-nav,
.desktop-profile {
display: none;
}
/* Show mobile hamburger */
.mobile-only {
display: flex !important;
}
/* Adjust navbar layout for mobile */
.navbar {
grid-template-columns: auto 1fr auto;
padding: 0 1rem;
}
.logo-section {
justify-content: flex-start;
overflow: hidden;
min-width: 0;
}
.logo-svg {
height: 32px;
max-width: calc(100vw - 8rem);
width: auto;
}
.hamburger-button {
flex-shrink: 0;
justify-self: end;
}
/* Additional breakpoint for very narrow screens like folded phones */
@media (max-width: 480px) {
.logo-svg {
height: 28px;
max-width: calc(100vw - 6rem);
}
.navbar {
padding: 0 0.75rem;
}
}
@media (max-width: 360px) {
.logo-svg {
height: 24px;
max-width: calc(100vw - 5rem);
}
.navbar {
padding: 0 0.5rem;
}
}
/* Footer adjustments */
.footer-content {
grid-template-columns: 1fr;
text-align: center;
}
.footer-links {
align-items: center;
justify-content: center;
}
.contact-link {
justify-content: center;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.navbar {
padding: 0 1rem;
}
.footer-content {
grid-template-columns: 1fr 1fr;
}
.footer-section.brand {
grid-column: span 2;
text-align: center;
}
}
</style>