You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
532 lines
15 KiB
532 lines
15 KiB
<script> |
|
import { isStandaloneMode, relayUrl, relayInfo, relayConnectionStatus, savedRelays, saveRelay } from "./stores.js"; |
|
import { getApiBase, connectToRelay, normalizeWsUrl } from "./config.js"; |
|
|
|
export let isDarkTheme = false; |
|
export let isLoggedIn = false; |
|
export let userRole = ""; |
|
export let currentEffectiveRole = ""; |
|
export let userProfile = null; |
|
export let userPubkey = ""; |
|
|
|
// Event dispatchers |
|
import { createEventDispatcher } from "svelte"; |
|
const dispatch = createEventDispatcher(); |
|
|
|
// Dropdown state |
|
let showDropdown = false; |
|
let isConnecting = false; |
|
let connectingUrl = ""; |
|
|
|
function openSettingsDrawer() { |
|
dispatch("openSettingsDrawer"); |
|
} |
|
|
|
function toggleMobileMenu() { |
|
dispatch("toggleMobileMenu"); |
|
} |
|
|
|
function openLoginModal() { |
|
dispatch("openLoginModal"); |
|
} |
|
|
|
function openRelayModal() { |
|
dispatch("openRelayModal"); |
|
} |
|
|
|
function toggleDropdown(event) { |
|
event.stopPropagation(); |
|
showDropdown = !showDropdown; |
|
} |
|
|
|
function closeDropdown() { |
|
showDropdown = false; |
|
} |
|
|
|
async function switchToRelay(url) { |
|
console.log('[Header] switchToRelay called with:', url); |
|
if (isConnecting || url === $relayUrl) { |
|
console.log('[Header] Skipping - already connecting or same relay'); |
|
return; |
|
} |
|
|
|
isConnecting = true; |
|
connectingUrl = url; |
|
|
|
try { |
|
console.log('[Header] Calling connectToRelay...'); |
|
const result = await connectToRelay(url); |
|
console.log('[Header] connectToRelay result:', result); |
|
if (result.success) { |
|
const wsUrl = normalizeWsUrl(url); |
|
saveRelay(url, wsUrl); |
|
dispatch("relayChanged", { info: result.info }); |
|
closeDropdown(); |
|
} else { |
|
console.log('[Header] Connection failed:', result.error); |
|
} |
|
} catch (error) { |
|
console.error("[Header] Failed to switch relay:", error); |
|
} finally { |
|
isConnecting = false; |
|
connectingUrl = ""; |
|
} |
|
} |
|
|
|
function handleManageRelays(event) { |
|
event.stopPropagation(); |
|
closeDropdown(); |
|
openRelayModal(); |
|
} |
|
|
|
// Close dropdown when clicking outside |
|
function handleClickOutside(event) { |
|
if (showDropdown) { |
|
closeDropdown(); |
|
} |
|
} |
|
|
|
// Get display name for relay - always show host URL |
|
// Explicitly reference $relayUrl in reactive statement for proper tracking |
|
$: relayDisplayName = getRelayHost($relayUrl); |
|
|
|
function getRelayHost(storeUrl) { |
|
try { |
|
// In standalone mode, use the stored relay URL |
|
// In embedded mode (no stored URL), use the current origin |
|
const url = storeUrl || getApiBase(); |
|
console.log("[Header] getRelayHost - storeUrl:", storeUrl, "resolved url:", url); |
|
const parsed = new URL(url); |
|
return parsed.host; |
|
} catch { |
|
return storeUrl || "local"; |
|
} |
|
} |
|
|
|
function formatRelayUrl(url) { |
|
try { |
|
const parsed = new URL(url.startsWith('http') ? url : 'https://' + url); |
|
return parsed.host; |
|
} catch { |
|
return url; |
|
} |
|
} |
|
|
|
function isCurrentRelay(url) { |
|
return $relayUrl === url && $relayConnectionStatus === "connected"; |
|
} |
|
</script> |
|
|
|
<svelte:window on:click={handleClickOutside} /> |
|
|
|
<header class="main-header" class:dark-theme={isDarkTheme}> |
|
<div class="header-content"> |
|
<button class="mobile-menu-btn" on:click={toggleMobileMenu} aria-label="Toggle menu"> |
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
<path d="M3 12h18M3 6h18M3 18h18" /> |
|
</svg> |
|
</button> |
|
<img src="/orly.png" alt="ORLY Logo" class="logo" /> |
|
<div class="header-title"> |
|
<span class="app-title"> |
|
ORLY? dashboard |
|
{#if isLoggedIn && userRole} |
|
<span class="permission-badge" |
|
>{currentEffectiveRole}</span |
|
> |
|
{/if} |
|
</span> |
|
</div> |
|
|
|
<!-- Relay indicator - dropdown only in standalone mode --> |
|
<div class="relay-dropdown-container"> |
|
{#if $isStandaloneMode} |
|
<button |
|
class="relay-indicator" |
|
on:click={toggleDropdown} |
|
title="Click to switch relays" |
|
> |
|
<span class="relay-status" class:connected={$relayConnectionStatus === "connected"} class:error={$relayConnectionStatus === "error"}></span> |
|
<span class="relay-name">{relayDisplayName}</span> |
|
<span class="dropdown-arrow" class:open={showDropdown}>▾</span> |
|
</button> |
|
|
|
{#if showDropdown} |
|
<!-- svelte-ignore a11y-click-events-have-key-events --> |
|
<!-- svelte-ignore a11y-no-static-element-interactions --> |
|
<div class="relay-dropdown" on:click|stopPropagation> |
|
{#if $savedRelays.length > 0} |
|
<div class="dropdown-section"> |
|
<div class="dropdown-label">Saved Relays</div> |
|
{#each $savedRelays as relay} |
|
<button |
|
class="dropdown-item" |
|
class:current={isCurrentRelay(relay.url)} |
|
class:connecting={connectingUrl === relay.url} |
|
on:click={() => switchToRelay(relay.url)} |
|
disabled={isConnecting} |
|
> |
|
<span class="item-status" class:connected={isCurrentRelay(relay.url)}></span> |
|
<span class="item-url-label">{relay.name}</span> |
|
{#if connectingUrl === relay.url} |
|
<span class="connecting-indicator">...</span> |
|
{/if} |
|
</button> |
|
{/each} |
|
</div> |
|
<div class="dropdown-divider"></div> |
|
{/if} |
|
<button class="dropdown-item manage-btn" on:click={handleManageRelays}> |
|
Manage Relays... |
|
</button> |
|
</div> |
|
{/if} |
|
{:else} |
|
<!-- Embedded mode: static indicator, no dropdown --> |
|
<div class="relay-indicator static" title="Connected to {relayDisplayName}"> |
|
<span class="relay-status" class:connected={$relayConnectionStatus === "connected"} class:error={$relayConnectionStatus === "error"}></span> |
|
<span class="relay-name">{relayDisplayName}</span> |
|
</div> |
|
{/if} |
|
</div> |
|
|
|
<div class="header-buttons"> |
|
{#if isLoggedIn} |
|
<button class="user-profile-btn" on:click={openSettingsDrawer}> |
|
{#if userProfile?.picture} |
|
<img |
|
src={userProfile.picture} |
|
alt="User avatar" |
|
class="user-avatar" |
|
/> |
|
{:else} |
|
<div class="user-avatar-placeholder">👤</div> |
|
{/if} |
|
<span class="user-name"> |
|
{userProfile?.name || userPubkey} |
|
</span> |
|
</button> |
|
{:else} |
|
<button class="login-btn" on:click={openLoginModal} |
|
>Log in</button |
|
> |
|
{/if} |
|
</div> |
|
</div> |
|
</header> |
|
|
|
<style> |
|
.main-header { |
|
color: var(--text-color); |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
height: 3em; |
|
background: var(--header-bg); |
|
border: 0; |
|
z-index: 1000; |
|
display: flex; |
|
align-items: stretch; |
|
padding: 0 0.25em; |
|
} |
|
|
|
.header-content { |
|
display: flex; |
|
align-items: stretch; |
|
width: 100%; |
|
padding: 0; |
|
margin: 0; |
|
} |
|
|
|
.mobile-menu-btn { |
|
display: none; |
|
align-items: center; |
|
justify-content: center; |
|
background: transparent; |
|
border: none; |
|
color: var(--text-color); |
|
cursor: pointer; |
|
padding: 0.5em; |
|
margin-right: 0.25em; |
|
} |
|
|
|
.mobile-menu-btn svg { |
|
width: 1.5em; |
|
height: 1.5em; |
|
} |
|
|
|
.mobile-menu-btn:hover { |
|
background: var(--card-bg); |
|
border-radius: 4px; |
|
} |
|
|
|
@media (max-width: 640px) { |
|
.mobile-menu-btn { |
|
display: flex; |
|
} |
|
} |
|
|
|
.logo { |
|
height: 2.5em; |
|
width: auto; |
|
flex-shrink: 0; |
|
align-self: center; |
|
} |
|
|
|
.header-title { |
|
flex: 1; |
|
display: flex; |
|
align-items: center; |
|
align-self: center; |
|
} |
|
|
|
.app-title { |
|
font-size: 1.2em; |
|
font-weight: 600; |
|
color: var(--text-color); |
|
display: flex; |
|
align-items: center; |
|
gap: 0.5em; |
|
} |
|
|
|
.permission-badge { |
|
background: var(--primary); |
|
color: var(--text-color); |
|
padding: 0.2em 0.5em; |
|
border-radius: 0.5em; |
|
font-size: 0.7em; |
|
font-weight: 500; |
|
text-transform: uppercase; |
|
letter-spacing: 0.5px; |
|
} |
|
|
|
.header-buttons { |
|
display: flex; |
|
align-items: stretch; |
|
align-self: stretch; |
|
margin-left: auto; |
|
} |
|
|
|
.login-btn, |
|
.user-profile-btn { |
|
background: transparent; |
|
color: var(--button-text); |
|
border: 0; |
|
cursor: pointer; |
|
font-size: 1em; |
|
transition: background-color 0.2s; |
|
flex-shrink: 0; |
|
padding: 0.5em; |
|
margin: 0; |
|
display: flex !important; |
|
align-items: center !important; |
|
justify-content: center; |
|
} |
|
|
|
.login-btn:hover, |
|
.user-profile-btn:hover { |
|
background: var(--card-bg); |
|
} |
|
|
|
.user-profile-btn { |
|
gap: 0.5em; |
|
justify-content: flex-start; |
|
padding: 0 0.5em; |
|
} |
|
|
|
.user-avatar { |
|
height: 2.5em; |
|
width: 2.5em; |
|
border-radius: 50%; |
|
object-fit: cover; |
|
flex-shrink: 0; |
|
align-self: center; |
|
vertical-align: middle; |
|
} |
|
|
|
.user-avatar-placeholder { |
|
height: 2.5em; |
|
width: 2.5em; |
|
border-radius: 50%; |
|
background: var(--bg-color); |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-size: 1.2em; |
|
flex-shrink: 0; |
|
align-self: center; |
|
} |
|
|
|
.user-name { |
|
font-weight: 500; |
|
white-space: nowrap; |
|
line-height: 1; |
|
align-self: center; |
|
max-width: none !important; |
|
overflow: visible !important; |
|
text-overflow: unset !important; |
|
width: auto !important; |
|
color: var(--text-color); |
|
} |
|
|
|
/* Relay dropdown container */ |
|
.relay-dropdown-container { |
|
position: relative; |
|
align-self: center; |
|
} |
|
|
|
/* Relay indicator */ |
|
.relay-indicator { |
|
display: flex; |
|
align-items: center; |
|
gap: 6px; |
|
padding: 4px 10px; |
|
margin: 0 8px; |
|
background: var(--muted); |
|
border: 1px solid var(--border-color); |
|
border-radius: 4px; |
|
cursor: pointer; |
|
font-size: 0.85em; |
|
color: var(--text-color); |
|
transition: background-color 0.2s, border-color 0.2s; |
|
} |
|
|
|
.relay-indicator:hover:not(.static) { |
|
background: var(--card-bg); |
|
border-color: var(--primary); |
|
} |
|
|
|
.relay-indicator.static { |
|
cursor: default; |
|
} |
|
|
|
.relay-status { |
|
width: 8px; |
|
height: 8px; |
|
border-radius: 50%; |
|
background: var(--warning); |
|
flex-shrink: 0; |
|
} |
|
|
|
.relay-status.connected { |
|
background: var(--success); |
|
} |
|
|
|
.relay-status.error { |
|
background: var(--danger); |
|
} |
|
|
|
.relay-name { |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
max-width: 200px; |
|
} |
|
|
|
.dropdown-arrow { |
|
font-size: 0.7em; |
|
transition: transform 0.2s; |
|
margin-left: 2px; |
|
} |
|
|
|
.dropdown-arrow.open { |
|
transform: rotate(180deg); |
|
} |
|
|
|
/* Dropdown menu */ |
|
.relay-dropdown { |
|
position: absolute; |
|
top: calc(100% + 4px); |
|
left: 0; |
|
min-width: 250px; |
|
background: var(--bg-color); |
|
border: 1px solid var(--border-color); |
|
border-radius: 6px; |
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
|
z-index: 1001; |
|
overflow: hidden; |
|
} |
|
|
|
.dropdown-section { |
|
padding: 4px 0; |
|
} |
|
|
|
.dropdown-label { |
|
padding: 6px 12px; |
|
font-size: 0.75em; |
|
color: var(--muted-foreground); |
|
text-transform: uppercase; |
|
letter-spacing: 0.5px; |
|
font-weight: 500; |
|
} |
|
|
|
.dropdown-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
width: 100%; |
|
padding: 8px 12px; |
|
background: transparent; |
|
border: none; |
|
cursor: pointer; |
|
text-align: left; |
|
color: var(--text-color); |
|
font-size: 0.9em; |
|
transition: background-color 0.15s; |
|
} |
|
|
|
.dropdown-item:hover:not(:disabled) { |
|
background: var(--tab-hover-bg); |
|
} |
|
|
|
.dropdown-item:disabled { |
|
opacity: 0.6; |
|
cursor: not-allowed; |
|
} |
|
|
|
.dropdown-item.current { |
|
background: rgba(16, 185, 129, 0.1); |
|
} |
|
|
|
.dropdown-item.connecting { |
|
background: rgba(234, 179, 8, 0.1); |
|
} |
|
|
|
.item-status { |
|
width: 6px; |
|
height: 6px; |
|
border-radius: 50%; |
|
background: var(--muted-foreground); |
|
flex-shrink: 0; |
|
} |
|
|
|
.item-status.connected { |
|
background: var(--success); |
|
} |
|
|
|
.item-url-label { |
|
flex: 1; |
|
font-family: monospace; |
|
font-size: 0.85em; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
} |
|
|
|
.connecting-indicator { |
|
color: var(--warning); |
|
font-weight: bold; |
|
} |
|
|
|
.dropdown-divider { |
|
height: 1px; |
|
background: var(--border-color); |
|
margin: 4px 0; |
|
} |
|
|
|
.manage-btn { |
|
color: var(--primary); |
|
font-weight: 500; |
|
} |
|
</style>
|
|
|