feat(homelab)!: create interface for homelab management, use templating for route generation & support more options and route types
This commit is contained in:
30
nix/homelab/frontend/src/App.svelte
Normal file
30
nix/homelab/frontend/src/App.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
createQuery,
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from "@tanstack/svelte-query";
|
||||
import Actions from "./lib/minecraft/Actions.svelte";
|
||||
import Stats from "./lib/minecraft/Stats.svelte";
|
||||
import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
</script>
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<main class="flex justify-center mt-8">
|
||||
<div class="w-full max-w-2xl px-4">
|
||||
<div class="flex justify-center">
|
||||
<h1 class="text-2xl font-semibold mb-8">Management</h1>
|
||||
</div>
|
||||
|
||||
<!-- Minecraft Section -->
|
||||
<section class="bg-zinc-800 rounded-lg p-6">
|
||||
<h2 class="text-xl mb-4">Minecraft</h2>
|
||||
<Actions />
|
||||
<Stats />
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<SvelteQueryDevtools />
|
||||
</QueryClientProvider>
|
||||
86
nix/homelab/frontend/src/app.css
Normal file
86
nix/homelab/frontend/src/app.css
Normal file
@@ -0,0 +1,86 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
/* background-color: #242424; */
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* a { */
|
||||
/* font-weight: 500; */
|
||||
/* color: #646cff; */
|
||||
/* text-decoration: inherit; */
|
||||
/* } */
|
||||
/* a:hover { */
|
||||
/* color: #535bf2; */
|
||||
/* } */
|
||||
|
||||
/* body { */
|
||||
/* margin: 0; */
|
||||
/* display: flex; */
|
||||
/* place-items: center; */
|
||||
/* min-width: 320px; */
|
||||
/* min-height: 100vh; */
|
||||
/* } */
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
/* #app { */
|
||||
/* max-width: 1280px; */
|
||||
/* margin: 0 auto; */
|
||||
/* padding: 2rem; */
|
||||
/* text-align: center; */
|
||||
/* } */
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s, background-color 0.2s;
|
||||
color: #1a1b26
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
@apply bg-zinc-700
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
1
nix/homelab/frontend/src/assets/svelte.svg
Normal file
1
nix/homelab/frontend/src/assets/svelte.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
30
nix/homelab/frontend/src/lib/Button.svelte
Normal file
30
nix/homelab/frontend/src/lib/Button.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { LoaderCircle } from "@lucide/svelte";
|
||||
import type { ButtonRootProps } from "bits-ui";
|
||||
import { Button } from "bits-ui";
|
||||
type Props = ButtonRootProps & {
|
||||
loading?: boolean;
|
||||
["loading-overwrite"]?: boolean;
|
||||
};
|
||||
|
||||
const {
|
||||
children,
|
||||
loading,
|
||||
disabled,
|
||||
"loading-overwrite": loadingOverwite = true,
|
||||
...props
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<Button.Root disabled={loading || disabled} {...props}>
|
||||
{#if loading}
|
||||
<span class="flex gap-2">
|
||||
{#if !loadingOverwite}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
<LoaderCircle class="animate-spin" />
|
||||
</span>
|
||||
{:else}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
</Button.Root>
|
||||
26
nix/homelab/frontend/src/lib/minecraft/Actions.svelte
Normal file
26
nix/homelab/frontend/src/lib/minecraft/Actions.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "bits-ui";
|
||||
|
||||
function syncCreativeWorld() {
|
||||
console.log("Syncing creative world...");
|
||||
}
|
||||
|
||||
function backupMainWorld() {
|
||||
console.log("Backing up main world...");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex gap-3 mb-6">
|
||||
<Button.Root
|
||||
onclick={syncCreativeWorld}
|
||||
class="bg-[#7aa2f7] hover:bg-[#7dcfff]"
|
||||
>
|
||||
Sync Creative World
|
||||
</Button.Root>
|
||||
<Button.Root
|
||||
onclick={backupMainWorld}
|
||||
class="bg-[#bb9af7] hover:bg-[#c0caf5]"
|
||||
>
|
||||
Backup Main World
|
||||
</Button.Root>
|
||||
</div>
|
||||
37
nix/homelab/frontend/src/lib/minecraft/Stats.svelte
Normal file
37
nix/homelab/frontend/src/lib/minecraft/Stats.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { createQuery } from "@tanstack/svelte-query";
|
||||
import { fetchStats } from "./stats";
|
||||
import { LoaderCircle } from "@lucide/svelte";
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: ["minecraft-server-stats"],
|
||||
queryFn: () => fetchStats(),
|
||||
refetchInterval: 10000,
|
||||
staleTime: 5000,
|
||||
}));
|
||||
</script>
|
||||
|
||||
{#snippet statItem(label: string, value?: string | number)}
|
||||
<div class="bg-zinc-700 rounded p-3">
|
||||
<div class="text-sm">{label}</div>
|
||||
<div class="text-xl text-white">{value}</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="border border-zinc-600 rounded-lg p-4">
|
||||
<h3 class="text-lg font-medium mb-3">
|
||||
Server Stats {#if query.isError}
|
||||
<span class="text-sm text-red-500"
|
||||
>an error occured fetching server stats</span
|
||||
>
|
||||
{:else if query.isPending}
|
||||
<LoaderCircle class="ml-1 w-4 h-4 inline-block animate-spin" />
|
||||
{/if}
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-4 text-zinc-400">
|
||||
{@render statItem("Players Online", query.data?.players_online ?? "--")}
|
||||
{@render statItem("Server Status", query.data?.status ?? "--")}
|
||||
{@render statItem("Uptime", query.data?.uptime ?? "--")}
|
||||
{@render statItem("World Size", query.data?.world_size ?? "--")}
|
||||
</div>
|
||||
</div>
|
||||
14
nix/homelab/frontend/src/lib/minecraft/stats.ts
Normal file
14
nix/homelab/frontend/src/lib/minecraft/stats.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
type StatsResponse = {
|
||||
status: string;
|
||||
players_online: number;
|
||||
max_players: number;
|
||||
uptime: string;
|
||||
world_size: string;
|
||||
};
|
||||
|
||||
export const fetchStats = async () => {
|
||||
const response = await fetch(
|
||||
"http://localhost:5173/api/minecraft-server-stats",
|
||||
);
|
||||
return (await response.json()) as StatsResponse;
|
||||
};
|
||||
9
nix/homelab/frontend/src/main.ts
Normal file
9
nix/homelab/frontend/src/main.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')!,
|
||||
})
|
||||
|
||||
export default app
|
||||
Reference in New Issue
Block a user