beeta/frontend/src/routes/+layout.svelte

570 lines
19 KiB
Svelte
Raw Normal View History

2025-08-03 21:53:15 -04:00
<script>
import { onMount } from 'svelte';
2026-01-05 22:54:27 -05:00
import { auth, isAuthenticated, isAdmin, isModerator, isStreamer, isRestreamer, isUploader, isTexter, isStickerCreator, isWatchCreator, userColor } from '$lib/stores/auth';
import { ubercoinBalance, formatUbercoin, fetchBalance } from '$lib/stores/ubercoin';
import { siteSettings } from '$lib/stores/siteSettings';
2025-08-03 21:53:15 -04:00
import { page } from '$app/stores';
2026-01-05 22:54:27 -05:00
import { browser } from '$app/environment';
import ChatTerminal from '$lib/components/chat/ChatTerminal.svelte';
import EbookReaderOverlay from '$lib/components/EbookReaderOverlay.svelte';
import ChessGameOverlay from '$lib/components/ChessGameOverlay.svelte';
import GlobalAudioPlayer from '$lib/components/GlobalAudioPlayer.svelte';
2025-08-03 21:53:15 -04:00
import '../app.css';
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
let showDropdown = false;
2026-01-05 22:54:27 -05:00
// Check if we're on a popout page (no nav/overlays needed)
$: isPopoutPage = $page.url.pathname.startsWith('/chat/popout') || $page.url.pathname.startsWith('/chat/terminal');
2025-08-03 21:53:15 -04:00
// Close dropdown when route changes
$: if ($page) {
showDropdown = false;
}
2026-01-05 22:54:27 -05:00
async function loadSiteSettings() {
try {
const response = await fetch('/api/settings/site');
if (response.ok) {
const data = await response.json();
const settings = data.settings || {};
siteSettings.set({
site_title: settings.site_title || 'Stream',
logo_path: settings.logo_path || '',
logo_display_mode: settings.logo_display_mode || 'text'
});
}
} catch (e) {
console.error('Failed to load site settings', e);
}
}
2025-08-03 21:53:15 -04:00
onMount(() => {
2026-01-05 22:54:27 -05:00
auth.init().then(() => {
fetchBalance();
});
loadSiteSettings();
2025-08-03 21:53:15 -04:00
// Close dropdown when clicking outside
const handleClickOutside = (event) => {
if (!event.target.closest('.user-menu')) {
showDropdown = false;
}
};
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
});
function toggleDropdown() {
showDropdown = !showDropdown;
}
</script>
2026-01-05 22:54:27 -05:00
<svelte:head>
<title>{$siteSettings.site_title}</title>
{#if $siteSettings.logo_path}
<link rel="icon" type="image/png" href={$siteSettings.logo_path} />
<link rel="apple-touch-icon" href={$siteSettings.logo_path} />
{/if}
</svelte:head>
2025-08-03 21:53:15 -04:00
<style>
2026-01-05 22:54:27 -05:00
:global(:root) {
--nav-padding-y: 0.3rem;
--nav-margin-bottom: 0.33rem;
--nav-height: 2.66rem; /* content(32px/2rem) + padding(0.6rem) + border(1px) */
}
2025-08-03 21:53:15 -04:00
.nav {
background: #111;
border-bottom: 1px solid var(--border);
2026-01-05 22:54:27 -05:00
padding: var(--nav-padding-y) 0;
margin-bottom: var(--nav-margin-bottom);
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.nav-container {
padding: 0 2rem;
display: flex;
align-items: center;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.nav-brand {
2026-01-05 22:54:27 -05:00
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.1rem;
2025-08-03 21:53:15 -04:00
font-weight: 600;
color: var(--white);
text-decoration: none;
2026-01-05 22:54:27 -05:00
flex-shrink: 0;
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
.nav-brand-logo {
max-height: 32px;
max-width: 120px;
object-fit: contain;
}
.nav-brand-text {
background: linear-gradient(135deg, #8b5cf6, #a855f7);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
2025-08-03 21:53:15 -04:00
.nav-links {
display: flex;
gap: 1rem;
align-items: center;
2026-01-05 22:54:27 -05:00
flex-shrink: 0;
margin-left: auto;
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.nav-link {
color: var(--white);
text-decoration: none;
transition: color 0.2s;
padding: 0.5rem 1rem;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.nav-link:hover {
color: var(--primary);
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.user-menu {
position: relative;
2026-01-05 22:54:27 -05:00
flex-shrink: 0;
margin-left: auto;
2025-08-03 21:53:15 -04:00
}
2025-08-10 07:55:39 -04:00
.user-avatar-btn {
2026-01-05 22:54:27 -05:00
width: 32px;
height: 32px;
2025-08-10 07:55:39 -04:00
border-radius: 50%;
background: var(--gray);
border: 2px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--white);
font-weight: 600;
2026-01-05 22:54:27 -05:00
font-size: 0.85rem;
2025-08-10 07:55:39 -04:00
transition: all 0.2s;
padding: 0;
overflow: hidden;
position: relative;
}
.user-avatar-btn.has-color {
background: var(--user-color);
}
.user-avatar-btn.has-color.with-image {
2026-01-05 22:54:27 -05:00
background: transparent;
2025-08-10 07:55:39 -04:00
border-color: var(--user-color);
border-width: 3px;
}
2025-08-03 21:53:15 -04:00
.user-avatar-btn:hover {
2025-08-10 07:55:39 -04:00
transform: scale(1.05);
2025-08-03 21:53:15 -04:00
}
2025-08-10 07:55:39 -04:00
.user-avatar-btn.has-color:hover {
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2);
}
.user-avatar-btn img {
width: 100%;
height: 100%;
object-fit: cover;
border: none;
}
2025-08-03 21:53:15 -04:00
.dropdown {
position: absolute;
right: 0;
top: calc(100% + 0.5rem);
2026-01-05 22:54:27 -05:00
background: #0a0a0a;
2025-08-03 21:53:15 -04:00
border: 1px solid var(--border);
2026-01-05 22:54:27 -05:00
border-radius: 12px;
min-width: 220px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
z-index: 10000;
2025-08-03 21:53:15 -04:00
overflow: hidden;
2026-01-05 22:54:27 -05:00
animation: dropdownFadeIn 0.15s ease-out;
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
2025-08-03 21:53:15 -04:00
.dropdown-header {
2026-01-05 22:54:27 -05:00
position: relative;
padding: 1rem 1rem 0.75rem;
2025-08-03 21:53:15 -04:00
border-bottom: 1px solid var(--border);
2026-01-05 22:54:27 -05:00
overflow: hidden;
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
.dropdown-header-bg {
position: absolute;
inset: 0;
z-index: 0;
}
.dropdown-header-bg img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.7;
}
.dropdown-header-bg::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(10, 10, 10, 0.1) 0%, rgba(10, 10, 10, 0.6) 100%);
}
.dropdown-header-content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.dropdown-ubercoin {
position: relative;
display: block;
text-align: center;
margin-top: 6px;
font-size: 0.85rem;
color: #ffd700;
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.dropdown-ubercoin .coin-icon {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background: linear-gradient(135deg, #ffd700 0%, #ffb300 100%);
border-radius: 50%;
font-size: 0.65rem;
font-weight: bold;
color: #000;
}
.dropdown-badges {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
margin-top: 8px;
}
.role-badge {
font-size: 0.65rem;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.role-badge.admin {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.role-badge.moderator {
background: rgba(168, 85, 247, 0.2);
color: #a855f7;
}
.role-badge.streamer {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.role-badge.restreamer {
background: rgba(20, 184, 166, 0.2);
color: #14b8a6;
}
.role-badge.uploader {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.role-badge.texter {
background: rgba(251, 146, 60, 0.2);
color: #fb923c;
}
.role-badge.sticker-creator {
background: rgba(236, 72, 153, 0.2);
color: #ec4899;
}
.role-badge.watch-creator {
background: rgba(99, 102, 241, 0.2);
color: #6366f1;
}
2025-08-03 21:53:15 -04:00
.dropdown-username {
2026-01-05 22:54:27 -05:00
position: relative;
2025-08-03 21:53:15 -04:00
font-weight: 600;
margin-bottom: 0.25rem;
text-decoration: none;
2026-01-05 22:54:27 -05:00
display: block;
text-align: center;
transition: filter 0.2s;
padding-left: 1.25rem;
padding-right: 1.25rem;
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.dropdown-username:hover {
2026-01-05 22:54:27 -05:00
filter: brightness(1.3);
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
2025-08-10 07:55:39 -04:00
.user-color-dot {
2026-01-05 22:54:27 -05:00
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 10px;
height: 10px;
2025-08-10 07:55:39 -04:00
border-radius: 50%;
2026-01-05 22:54:27 -05:00
box-shadow: 0 0 6px currentColor;
2025-08-10 07:55:39 -04:00
}
2026-01-05 22:54:27 -05:00
.dropdown-menu {
padding: 0.5rem;
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.dropdown-item {
2026-01-05 22:54:27 -05:00
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 0.6rem 0.75rem;
color: var(--gray);
2025-08-03 21:53:15 -04:00
text-decoration: none;
2026-01-05 22:54:27 -05:00
transition: all 0.15s ease;
border-radius: 8px;
margin: 2px 0;
font-size: 0.9rem;
border: none;
background: transparent;
width: 100%;
cursor: pointer;
text-align: center;
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.dropdown-item:hover {
2026-01-05 22:54:27 -05:00
background: rgba(139, 92, 246, 0.15);
color: var(--white);
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
.dropdown-item:hover :global(svg) {
color: var(--primary);
}
.dropdown-item :global(svg) {
position: absolute;
left: 0.75rem;
width: 18px;
height: 18px;
color: var(--gray);
transition: color 0.15s ease;
}
2025-08-03 21:53:15 -04:00
.dropdown-divider {
height: 1px;
background: var(--border);
2026-01-05 22:54:27 -05:00
margin: 0.5rem 0.5rem;
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
.dropdown-item.logout {
2026-01-05 22:54:27 -05:00
color: var(--gray);
}
.dropdown-item.logout:hover {
background: rgba(239, 68, 68, 0.15);
color: var(--error);
}
.dropdown-item.logout:hover :global(svg) {
2025-08-03 21:53:15 -04:00
color: var(--error);
}
</style>
2026-01-05 22:54:27 -05:00
{#if !isPopoutPage}
2025-08-03 21:53:15 -04:00
<nav class="nav">
<div class="nav-container">
2026-01-05 22:54:27 -05:00
<a href="/" class="nav-brand">
{#if $siteSettings.logo_display_mode === 'image' || $siteSettings.logo_display_mode === 'both'}
{#if $siteSettings.logo_path}
<img src={$siteSettings.logo_path} alt={$siteSettings.site_title} class="nav-brand-logo" />
{/if}
{/if}
{#if $siteSettings.logo_display_mode === 'text' || $siteSettings.logo_display_mode === 'both'}
<span class="nav-brand-text">{$siteSettings.site_title}</span>
{/if}
{#if !$siteSettings.logo_display_mode || ($siteSettings.logo_display_mode === 'image' && !$siteSettings.logo_path)}
<span class="nav-brand-text">{$siteSettings.site_title}</span>
{/if}
</a>
2025-08-03 21:53:15 -04:00
{#if !$auth.loading}
{#if $isAuthenticated}
<div class="user-menu">
2026-01-05 22:54:27 -05:00
<button
class="user-avatar-btn"
2025-08-10 07:55:39 -04:00
class:has-color={$userColor}
class:with-image={$auth.user.avatarUrl}
style="--user-color: {$userColor}"
on:click={toggleDropdown}
>
2025-08-03 21:53:15 -04:00
{#if $auth.user.avatarUrl}
<img src={$auth.user.avatarUrl} alt={$auth.user.username} />
{:else}
{$auth.user.username.charAt(0).toUpperCase()}
{/if}
</button>
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
{#if showDropdown}
<div class="dropdown">
<div class="dropdown-header">
2026-01-05 22:54:27 -05:00
{#if $auth.user.bannerUrl}
<div class="dropdown-header-bg">
<img
src={$auth.user.bannerUrl}
alt=""
style="object-position: {$auth.user.bannerPositionX ?? 50}% {$auth.user.bannerPosition ?? 50}%;"
/>
</div>
{:else}
<div
class="dropdown-header-bg"
style="background: linear-gradient(135deg, {$userColor} 0%, {$userColor}66 100%);"
></div>
{/if}
<div class="dropdown-header-content">
<a href="/profile/{$auth.user.username}" class="dropdown-username" style="color: {$userColor};">
<span
class="user-color-dot"
style="background: {$userColor}; color: {$userColor}"
></span>
{$auth.user.username}
</a>
<div class="dropdown-ubercoin">
<span class="coin-icon">Ü</span>
<span>{formatUbercoin($ubercoinBalance)}</span>
</div>
{#if $isAdmin || $isModerator || $isStreamer || $isRestreamer || $isUploader || $isTexter || $isStickerCreator || $isWatchCreator}
<div class="dropdown-badges">
{#if $isAdmin}<span class="role-badge admin">Admin</span>{/if}
{#if $isModerator}<span class="role-badge moderator">Mod</span>{/if}
{#if $isStreamer}<span class="role-badge streamer">Streamer</span>{/if}
{#if $isRestreamer}<span class="role-badge restreamer">Restreamer</span>{/if}
{#if $isUploader}<span class="role-badge uploader">Uploader</span>{/if}
{#if $isTexter}<span class="role-badge texter">Texter</span>{/if}
{#if $isStickerCreator}<span class="role-badge sticker-creator">Stickers</span>{/if}
{#if $isWatchCreator}<span class="role-badge watch-creator">Watch</span>{/if}
</div>
2025-08-03 21:53:15 -04:00
{/if}
</div>
</div>
2026-01-05 22:54:27 -05:00
<div class="dropdown-menu">
<a href="/settings" class="dropdown-item">
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
Settings
</a>
<a href="/forums" class="dropdown-item">
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Forums
2025-08-03 21:53:15 -04:00
</a>
2026-01-05 22:54:27 -05:00
<a href="/games" class="dropdown-item">
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="6" width="20" height="12" rx="2"/>
<path d="M6 12h4M8 10v4M15 11h.01M18 13h.01"/>
</svg>
Games
2025-08-03 21:53:15 -04:00
</a>
2026-01-05 22:54:27 -05:00
<a href="/stats" class="dropdown-item">
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 20V10M12 20V4M6 20v-6"/>
</svg>
Stats
</a>
{#if $isStreamer || $isUploader}
<a href="/my-realms" class="dropdown-item">
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polygon points="10 8 16 12 10 16 10 8" fill="currentColor"/>
</svg>
My Realms
</a>
{/if}
{#if $isAdmin}
<a href="/admin" class="dropdown-item">
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
Admin
</a>
{/if}
<div class="dropdown-divider"></div>
<button class="dropdown-item logout" on:click={() => auth.logout()}>
<svg class="dropdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/>
</svg>
Logout
</button>
</div>
2025-08-03 21:53:15 -04:00
</div>
{/if}
</div>
{:else}
<div class="nav-links">
<a href="/login" class="nav-link">Login</a>
</div>
{/if}
{/if}
</div>
</nav>
2026-01-05 22:54:27 -05:00
{#if browser}
<GlobalAudioPlayer />
<ChatTerminal />
<EbookReaderOverlay />
<ChessGameOverlay />
{/if}
{/if}
2025-08-03 21:53:15 -04:00
<slot />