200 lines
14 KiB
Svelte
200 lines
14 KiB
Svelte
<script>
|
|
let { page = $bindable("dashboard"), dark = false, toggleTheme = () => {}, logout = () => {}, sidebarOpen = $bindable(false) } = $props();
|
|
|
|
const links = [
|
|
{ id: "dashboard", label: "Overview", icon: "grid" },
|
|
{ id: "docker", label: "Docker", icon: "box" },
|
|
{ id: "files", label: "Files", icon: "folder" },
|
|
{ id: "terminal", label: "Terminal", icon: "terminal" },
|
|
];
|
|
|
|
const LAN_IP = "192.168.31.222";
|
|
const TS_IP = "100.78.131.124";
|
|
let lanReachable = $state(false);
|
|
|
|
if (typeof window !== "undefined" && location.hostname !== LAN_IP) {
|
|
fetch(`http://${LAN_IP}:4000/`, { mode: "no-cors", signal: AbortSignal.timeout(1500) })
|
|
.then(() => { lanReachable = true; })
|
|
.catch(() => {});
|
|
}
|
|
|
|
const media = [
|
|
{ label: "Navidrome", port: 4533, remoteHref: "https://music.jimmygan.com:8443", icon: "music" },
|
|
{ label: "Jellyfin", port: 8096, icon: "play" },
|
|
{ label: "Audiobookshelf", port: 13378, icon: "headphones" },
|
|
{ label: "Immich", port: 2283, icon: "image" },
|
|
];
|
|
|
|
const tools = [
|
|
{ id: "openclaw", label: "OpenClaw", icon: "openclaw" },
|
|
{ id: "gitea", label: "Repos", icon: "git" },
|
|
{ label: "Gitea Web", port: 3300, icon: "git", external: true },
|
|
{ label: "Stirling PDF", port: 8030, icon: "pdf", external: true },
|
|
{ label: "Vaultwarden", port: 8222, icon: "lock", external: true },
|
|
{ label: "Speedtest", port: 8080, icon: "speedtest", external: true },
|
|
];
|
|
|
|
function extHref(svc) {
|
|
if (lanReachable && svc.port) return `http://${LAN_IP}:${svc.port}`;
|
|
if (svc.remoteHref) return svc.remoteHref;
|
|
if (svc.port) {
|
|
const host = /^\d+\.\d+\.\d+\.\d+$/.test(location.hostname) ? location.hostname : TS_IP;
|
|
return `http://${host}:${svc.port}`;
|
|
}
|
|
return svc.href;
|
|
}
|
|
|
|
function navigate(id) {
|
|
page = id;
|
|
sidebarOpen = false;
|
|
}
|
|
</script>
|
|
|
|
<!-- Mobile overlay -->
|
|
{#if sidebarOpen}
|
|
<button class="fixed inset-0 bg-black/40 z-40 md:hidden" onclick={() => sidebarOpen = false} aria-label="Close sidebar"></button>
|
|
{/if}
|
|
|
|
<aside class="fixed inset-y-0 left-0 z-50 w-[240px] h-screen flex flex-col shrink-0 border-r border-surface-200 bg-white transition-transform duration-200 md:static md:translate-x-0 {dark ? 'bg-surface-800 border-surface-700' : ''} {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}">
|
|
<!-- Logo -->
|
|
<div class="px-5 py-5 flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-md">
|
|
<svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" /></svg>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-sm font-bold tracking-tight {dark ? 'text-white' : 'text-surface-900'}">NAS Dashboard</h1>
|
|
<p class="text-[10px] text-surface-400 font-medium">DS224+ · Synology</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nav Links -->
|
|
<nav class="flex-1 px-3 mt-2 overflow-y-auto">
|
|
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2">Main</p>
|
|
{#each links as link}
|
|
<button
|
|
class="w-full text-left px-3 py-2.5 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 transition-all duration-150
|
|
{page === link.id
|
|
? 'bg-primary-50 text-primary-700 shadow-sm ' + (dark ? 'bg-primary-900/30 text-primary-300' : '')
|
|
: 'text-surface-500 hover:bg-surface-100 hover:text-surface-700 ' + (dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : '')
|
|
}"
|
|
onclick={() => navigate(link.id)}
|
|
>
|
|
<span class="w-5 h-5 flex items-center justify-center">
|
|
{#if link.icon === "grid"}
|
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
|
{:else if link.icon === "box"}
|
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
|
|
{:else if link.icon === "folder"}
|
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
|
{:else if link.icon === "terminal"}
|
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
{/if}
|
|
</span>
|
|
{link.label}
|
|
</button>
|
|
{/each}
|
|
|
|
<div class="my-4 border-t border-surface-200 {dark ? 'border-surface-700' : ''}"></div>
|
|
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2">Media</p>
|
|
{#each media as svc}
|
|
<a
|
|
href={extHref(svc)}
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="w-full text-left px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 text-surface-500 hover:bg-surface-100 hover:text-surface-700 transition-all duration-150 {dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : ''}"
|
|
>
|
|
<span class="w-5 h-5 flex items-center justify-center">
|
|
{#if svc.icon === "music"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z" /></svg>
|
|
{:else if svc.icon === "play"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
{:else if svc.icon === "headphones"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 18v-6a9 9 0 0118 0v6M3 18a3 3 0 003 3h0a3 3 0 003-3v-2a3 3 0 00-3-3h0a3 3 0 00-3 3v2zm18 0a3 3 0 01-3 3h0a3 3 0 01-3-3v-2a3 3 0 013-3h0a3 3 0 013 3v2z" /></svg>
|
|
{:else if svc.icon === "image"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
{/if}
|
|
</span>
|
|
{svc.label}
|
|
<svg class="w-3 h-3 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
|
</a>
|
|
{/each}
|
|
|
|
<div class="my-4 border-t border-surface-200 {dark ? 'border-surface-700' : ''}"></div>
|
|
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2">Tools</p>
|
|
{#each tools as item}
|
|
{#if item.external || item.port}
|
|
<a
|
|
href={extHref(item)}
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="w-full text-left px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 text-surface-500 hover:bg-surface-100 hover:text-surface-700 transition-all duration-150 {dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : ''}"
|
|
>
|
|
<span class="w-5 h-5 flex items-center justify-center">
|
|
{#if item.icon === "openclaw"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg>
|
|
{:else if item.icon === "git"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
|
|
{:else if item.icon === "pdf"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
|
{:else if item.icon === "lock"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
|
{:else if item.icon === "speedtest"}
|
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
{/if}
|
|
</span>
|
|
{item.label}
|
|
<svg class="w-3 h-3 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
|
</a>
|
|
{:else}
|
|
<button
|
|
class="w-full text-left px-3 py-2.5 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 transition-all duration-150
|
|
{page === item.id
|
|
? 'bg-primary-50 text-primary-700 shadow-sm ' + (dark ? 'bg-primary-900/30 text-primary-300' : '')
|
|
: 'text-surface-500 hover:bg-surface-100 hover:text-surface-700 ' + (dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : '')
|
|
}"
|
|
onclick={() => navigate(item.id)}
|
|
>
|
|
<span class="w-5 h-5 flex items-center justify-center">
|
|
{#if item.icon === "openclaw"}
|
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg>
|
|
{:else if item.icon === "git"}
|
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
{/if}
|
|
</span>
|
|
{item.label}
|
|
</button>
|
|
{/if}
|
|
{/each}
|
|
</nav>
|
|
|
|
<!-- Bottom actions -->
|
|
<div class="px-4 pb-4 pt-2 border-t border-surface-200 {dark ? 'border-surface-700' : ''}">
|
|
<button
|
|
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 hover:bg-surface-100 transition-all duration-150 mb-1 {dark ? 'hover:bg-surface-700 text-surface-400' : ''} {page === 'settings' ? 'bg-primary-50 text-primary-700 shadow-sm ' + (dark ? 'bg-primary-900/30 text-primary-300' : '') : ''}"
|
|
onclick={() => navigate('settings')}
|
|
>
|
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
|
Settings
|
|
</button>
|
|
<button
|
|
onclick={toggleTheme}
|
|
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 hover:bg-surface-100 transition-all duration-150 mb-1 {dark ? 'hover:bg-surface-700 text-surface-400' : ''}"
|
|
>
|
|
{#if dark}
|
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
|
Light Mode
|
|
{:else}
|
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
|
|
Dark Mode
|
|
{/if}
|
|
</button>
|
|
<button
|
|
onclick={logout}
|
|
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 hover:bg-surface-100 transition-all duration-150 hover:text-red-600 {dark ? 'hover:bg-surface-700 text-surface-400 hover:text-red-400' : ''}"
|
|
>
|
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
</aside>
|