generated from wessel/boilerplate
feat: Push initial code
This commit is contained in:
1
src/app.css
Normal file
1
src/app.css
Normal file
@@ -0,0 +1 @@
|
||||
@import 'tailwindcss';
|
||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
12
src/app.html
Normal file
12
src/app.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
6
src/routes/+layout.svelte
Normal file
6
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
167
src/routes/+page.svelte
Normal file
167
src/routes/+page.svelte
Normal file
@@ -0,0 +1,167 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import services, { type Service } from './services';
|
||||
|
||||
let selectedService: Service | null = null;
|
||||
let currentTime = '';
|
||||
|
||||
// Group services by category
|
||||
const groupedServices: Record<string, Service[]> = services.reduce((acc, service) => {
|
||||
if (!acc[service.category]) {
|
||||
acc[service.category] = [];
|
||||
}
|
||||
acc[service.category].push(service);
|
||||
return acc;
|
||||
}, {} as Record<string, Service[]>);
|
||||
|
||||
function selectService(service: Service) {
|
||||
selectedService = service;
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
currentTime = now.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
updateTime();
|
||||
const interval = setInterval(updateTime, 1000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Terminal-style homepage layout -->
|
||||
<div class="terminal">
|
||||
<!-- Header -->
|
||||
<header class="terminal-header">
|
||||
<div class="header-left">
|
||||
<span class="terminal-title">NOVA</span>
|
||||
<span class="terminal-path">/home/root/services</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="terminal-time">{currentTime}</span>
|
||||
<div class="terminal-controls">
|
||||
<span class="control minimize">─</span>
|
||||
<span class="control maximize">□</span>
|
||||
<span class="control close">×</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="terminal-body">
|
||||
<!-- Left sidebar with services -->
|
||||
<aside class="services-sidebar">
|
||||
<div class="prompt-line">
|
||||
<span class="prompt-prefix">
|
||||
<span class="prompt-bracket">[</span><span class="prompt-user">user</span><span class="prompt-at">@</span><span class="prompt-host">nova</span><span class="prompt-bracket">]</span><span class="prompt-path">~/services</span><span class="prompt-dollar">$ </span>
|
||||
</span>
|
||||
<span class="command">ls -la services/</span>
|
||||
</div>
|
||||
|
||||
{#each Object.entries(groupedServices) as [category, categoryServices]}
|
||||
<div class="service-category">
|
||||
<div class="category-header">
|
||||
<span class="folder-icon">📁</span>
|
||||
<span class="category-name">{category.toLowerCase()}/</span>
|
||||
</div>
|
||||
|
||||
{#each categoryServices as service}
|
||||
<button
|
||||
class="service-item {selectedService?.name === service.name ? 'selected' : ''}"
|
||||
on:click={() => selectService(service)}
|
||||
>
|
||||
<span class="service-icon">{service.icon}</span>
|
||||
<span class="service-name">{service.name}</span>
|
||||
<span class="service-ext">.service</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if selectedService}
|
||||
<div class="service-info">
|
||||
<div class="prompt-line">
|
||||
<span class="prompt-prefix">
|
||||
<span class="prompt-bracket">[</span><span class="prompt-user">user</span><span class="prompt-at">@</span><span class="prompt-host">nova</span><span class="prompt-bracket">]</span><span class="prompt-path">~/services</span><span class="prompt-dollar">$ </span>
|
||||
</span>
|
||||
<span class="command">systemctl status {selectedService.name.toLowerCase()}</span>
|
||||
</div>
|
||||
<div class="status-output">
|
||||
<div class="status-line">● {selectedService.name.toLowerCase()}.service - {selectedService.name}</div>
|
||||
<div class="status-line"> Loaded: loaded (/etc/systemd/system/{selectedService.name.toLowerCase()}.service; enabled)</div>
|
||||
<div class="status-line active"> Active: <span class="status-active">active (running)</span> since $(date)</div>
|
||||
<div class="status-line"> Main PID: $(echo $RANDOM)</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</aside>
|
||||
|
||||
<!-- Right content area with iframe -->
|
||||
<main class="content-area">
|
||||
{#if selectedService}
|
||||
<div class="iframe-header">
|
||||
<div class="iframe-controls">
|
||||
<span class="iframe-title">{selectedService.name} - {selectedService.url}</span>
|
||||
<button class="external-link" on:click={() => selectedService && window.open(selectedService.url, '_blank')}>
|
||||
↗ Open External
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
src={selectedService.url}
|
||||
title={selectedService.name}
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="placeholder">
|
||||
<div class="placeholder-content">
|
||||
<div class="ascii-art">
|
||||
<pre>{`
|
||||
███╗ ██╗ ██████╗ ██╗ ██╗ █████╗
|
||||
████╗ ██║██╔═══██╗██║ ██║██╔══██╗
|
||||
██╔██╗ ██║██║ ██║██║ ██║███████║
|
||||
██║╚██╗██║██║ ██║╚██╗ ██╔╝██╔══██║
|
||||
██║ ╚████║╚██████╔╝ ╚████╔╝ ██║ ██║
|
||||
╚═╝ ╚═══╝ ╚═════╝ ╚═══╝ ╚═╝ ╚═╝
|
||||
`}</pre>
|
||||
</div>
|
||||
<div class="welcome-text">
|
||||
<p class="prompt-line">
|
||||
<span class="prompt-prefix">
|
||||
<span class="prompt-bracket">[</span><span class="prompt-user">user</span><span class="prompt-at">@</span><span class="prompt-host">nova</span><span class="prompt-bracket">]</span><span class="prompt-path">~</span><span class="prompt-dollar">$ </span>
|
||||
</span>
|
||||
<span class="command">echo "Welcome to Nova Terminal Dashboard"</span>
|
||||
</p>
|
||||
<p class="output">Welcome to Nova Terminal Dashboard</p>
|
||||
<p class="prompt-line">
|
||||
<span class="prompt-prefix">
|
||||
<span class="prompt-bracket">[</span><span class="prompt-user">user</span><span class="prompt-at">@</span><span class="prompt-host">nova</span><span class="prompt-bracket">]</span><span class="prompt-path">~</span><span class="prompt-dollar">$ </span>
|
||||
</span>
|
||||
<span class="command">echo "Select a service from the left panel to view"</span>
|
||||
</p>
|
||||
<p class="output">Select a service from the left panel to view</p>
|
||||
<p class="prompt-line">
|
||||
<span class="prompt-prefix">
|
||||
<span class="prompt-bracket">[</span><span class="prompt-user">user</span><span class="prompt-at">@</span><span class="prompt-host">nova</span><span class="prompt-bracket">]</span><span class="prompt-path">~</span><span class="prompt-dollar">$ </span>
|
||||
</span>
|
||||
<span class="typing-cursor">█</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use './main.scss'
|
||||
</style>
|
||||
390
src/routes/main.scss
Normal file
390
src/routes/main.scss
Normal file
@@ -0,0 +1,390 @@
|
||||
// Terminal-style homepage styles
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #0a0a0a;
|
||||
color: #00ff00;
|
||||
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
border: 2px solid #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: linear-gradient(to bottom, #2d2d2d, #1a1a1a);
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.terminal-path {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.terminal-time {
|
||||
color: #00ff00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.minimize {
|
||||
background: #ffbd2e;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.maximize {
|
||||
background: #28ca42;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.close {
|
||||
background: #ff5f56;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.terminal-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
height: calc(100vh - 50px);
|
||||
}
|
||||
|
||||
.services-sidebar {
|
||||
width: 350px;
|
||||
background: #000;
|
||||
border-right: 1px solid #333;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
.prompt-line {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.prompt-prefix {
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Fish shell-style prompt colors
|
||||
.prompt-bracket {
|
||||
color: #c0241e; // Dark red for brackets
|
||||
}
|
||||
|
||||
.prompt-user {
|
||||
color: #d69923; // Gold/yellow for username
|
||||
}
|
||||
|
||||
.prompt-at {
|
||||
color: #99AAB5; // Light blue-grey for @
|
||||
}
|
||||
|
||||
.prompt-host {
|
||||
color: #679D69; // Green for hostname
|
||||
}
|
||||
|
||||
.prompt-path {
|
||||
color: #888; // Grey for path
|
||||
}
|
||||
|
||||
.prompt-dollar {
|
||||
color: #99AAB5; // Light blue-grey for $
|
||||
}
|
||||
|
||||
.command {
|
||||
color: #fff;
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.service-category {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
color: #00aaff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
margin: 2px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.service-item:hover {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.service-item.selected {
|
||||
background: #004400;
|
||||
color: #00ff00;
|
||||
border-left: 3px solid #00ff00;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.service-ext {
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.service-info {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.status-output {
|
||||
margin-left: 20px;
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
color: #ccc;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.status-line.active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
color: #00ff00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.iframe-header {
|
||||
background: #1a1a1a;
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iframe-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.iframe-title {
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.external-link {
|
||||
background: #333;
|
||||
border: 1px solid #555;
|
||||
color: #ccc;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.external-link:hover {
|
||||
background: #444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.ascii-art {
|
||||
color: #00ff00;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.ascii-art pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.welcome-text {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.output {
|
||||
color: #ccc;
|
||||
margin: 4px 0 8px 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.typing-cursor {
|
||||
animation: blink 1s infinite;
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
|
||||
0%,
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
51%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar styling for webkit browsers */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.terminal-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.services-sidebar {
|
||||
width: 100%;
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.terminal-path {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
22
src/routes/services.ts
Normal file
22
src/routes/services.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface Service {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
const services: Service[] = [
|
||||
{ name: 'Jellyfin', url: 'https://jf.wessel.gg', icon: '🎬', category: 'Media' },
|
||||
{ name: 'Jellyseerr', url: 'https://js.wessel.gg', icon: '📽️', category: 'Media' },
|
||||
{ name: 'Kavita', url: 'https://kv.wessel.gg', icon: '📚', category: 'Media' },
|
||||
{ name: 'Navidrome', url: 'https://nd.wessel.gg', icon: '🎵', category: 'Media' },
|
||||
{ name: 'Proxmox', url: 'https://pm.wessel.gg', icon: '🖥️', category: 'Infrastructure' },
|
||||
{ name: 'TrueNAS SCALE', url: 'https://tn.wessel.gg', icon: '💾', category: 'Infrastructure' },
|
||||
{ name: 'Portainer', url: 'https://pt.wessel.gg', icon: '🐳', category: 'Infrastructure' },
|
||||
{ name: 'Sonarr', url: 'http://192.168.2.194:8989', icon: '🎥', category: 'Automation' },
|
||||
{ name: 'Radarr', url: 'http://192.168.2.194:7878', icon: '🎥', category: 'Automation' },
|
||||
{ name: 'Prowlarr', url: 'http://prowlarr.local', icon: '🔍', category: 'Automation' },
|
||||
{ name: 'QBittorrent', url: 'http://transmission.local', icon: '📥', category: 'Automation' },
|
||||
];
|
||||
|
||||
export default services;
|
||||
Reference in New Issue
Block a user