Rewrite page in Svelte 5 #49
7 changed files with 247 additions and 60 deletions
|
@ -4,14 +4,14 @@
|
||||||
import { OpenInNewWindow } from 'radix-icons-svelte';
|
import { OpenInNewWindow } from 'radix-icons-svelte';
|
||||||
import { quintInOut } from 'svelte/easing';
|
import { quintInOut } from 'svelte/easing';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { IconType, type Service } from '$lib/types/data-types';
|
import { IconType, type Server } from '$lib/types/data-types';
|
||||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||||
import type { Heartbeat } from '$lib/types/uptime-kuma-types';
|
import type { Heartbeat } from '$lib/types/uptime-kuma-types';
|
||||||
|
|
||||||
let { service, icons, monitor } = $props<{
|
let { server, icons, monitor } = $props<{
|
||||||
service: Service;
|
server: Server;
|
||||||
icons: Array<string>;
|
icons: Array<string>;
|
||||||
monitor: Heartbeat;
|
monitor?: Heartbeat;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let status = $state(4);
|
let status = $state(4);
|
||||||
|
@ -24,32 +24,36 @@
|
||||||
|
|
||||||
let img_source: string = $state('');
|
let img_source: string = $state('');
|
||||||
|
|
||||||
function checkForImage(service: Service) {
|
function checkForImage(server: Server) {
|
||||||
const rootSplit = service.icon.split('/');
|
const rootSplit = server.icon.split('/');
|
||||||
const root = rootSplit[rootSplit.length - 1];
|
const root = rootSplit[rootSplit.length - 1];
|
||||||
|
|
||||||
if (icons.includes(`${root}.${service.iconType}`)) {
|
if (icons.includes(`${root}.${server.iconType}`)) {
|
||||||
img_source = `${service.icon}.${service.iconType}`;
|
img_source = `${server.icon}.${server.iconType}`;
|
||||||
} else {
|
} else {
|
||||||
img_source = '';
|
img_source = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
if (typeof server.id === 'undefined') {
|
||||||
|
status = 99;
|
||||||
|
}
|
||||||
if (typeof monitor !== 'undefined') {
|
if (typeof monitor !== 'undefined') {
|
||||||
status = monitor.status;
|
status = monitor.status;
|
||||||
}
|
}
|
||||||
if (icons.length != 0) {
|
if (icons.length != 0 && typeof server.icon !== "undefined") {
|
||||||
const rootSplit = service.icon.split('/');
|
const rootSplit = server.icon.split('/');
|
||||||
const root = rootSplit[rootSplit.length - 1];
|
const root = rootSplit[rootSplit.length - 1];
|
||||||
|
|
||||||
if (service.iconType === IconType.SVG) {
|
if (server.iconType === IconType.SVG) {
|
||||||
checkForImage(service);
|
checkForImage(server);
|
||||||
} else {
|
}
|
||||||
if (icons.includes(`${root}-36.${service.iconType}`)) {
|
else {
|
||||||
img_source = `${service.icon}-36.${service.iconType}`;
|
if (icons.includes(`${root}-36.${server.iconType}`)) {
|
||||||
|
img_source = `${server.icon}-36.${server.iconType}`;
|
||||||
} else {
|
} else {
|
||||||
checkForImage(service);
|
checkForImage(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,15 +62,16 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex h-48 w-[28rem] flex-col gap-y-2 rounded-xl border-t-4
|
class="flex h-48 w-[28rem] flex-col gap-y-2 rounded-xl border-t-4
|
||||||
{status == 0
|
{status == 99 ? 'border-primary'
|
||||||
? 'border-offline'
|
: status == 0
|
||||||
: status == 1
|
? 'border-offline'
|
||||||
? 'border-online'
|
: status == 1
|
||||||
: status == 2
|
? 'border-online'
|
||||||
? 'border-pending'
|
: status == 2
|
||||||
: status == 3
|
? 'border-pending'
|
||||||
? 'border-maintenance'
|
: status == 3
|
||||||
: 'border-maintenance'}
|
? 'border-maintenance'
|
||||||
|
: 'border-maintenance'}
|
||||||
z-0 bg-black/55 p-4 backdrop-blur-sm"
|
z-0 bg-black/55 p-4 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row justify-between pb-4">
|
<div class="flex flex-row justify-between pb-4">
|
||||||
|
@ -75,22 +80,22 @@
|
||||||
on:mouseover={() => (hover.title = true)}
|
on:mouseover={() => (hover.title = true)}
|
||||||
on:mouseleave={() => (hover.title = false)}
|
on:mouseleave={() => (hover.title = false)}
|
||||||
>
|
>
|
||||||
{#if service.icon}
|
{#if typeof server.icon !== "undefined"}
|
||||||
{#if img_source != ''}
|
{#if img_source != ''}
|
||||||
<img
|
<img
|
||||||
width="24px"
|
width="24px"
|
||||||
class="h-6 w-6 cursor-pointer"
|
class="h-6 w-6 cursor-pointer"
|
||||||
src={img_source}
|
src={img_source}
|
||||||
alt="{service.name} Logo"
|
alt="{server.name} Logo"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Skeleton class="h-6 w-6 rounded-full" />
|
<Skeleton class="h-6 w-6 rounded-full" />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}{/if}
|
{:else}{/if}
|
||||||
<a
|
<a
|
||||||
href={service.href}
|
href={server.href}
|
||||||
class="font-bold {!hover.title || 'text-secondary'}"
|
class="font-bold {!hover.title || 'text-secondary'}"
|
||||||
>{service.name}</a
|
>{server.name}</a
|
||||||
>
|
>
|
||||||
{#if hover.title}
|
{#if hover.title}
|
||||||
<div
|
<div
|
||||||
|
@ -105,7 +110,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1
|
{#if typeof server.id !== "undefined"}
|
||||||
|
<h1
|
||||||
class="w-16 rounded-md border-b-2
|
class="w-16 rounded-md border-b-2
|
||||||
{status == 0
|
{status == 0
|
||||||
? 'border-offline'
|
? 'border-offline'
|
||||||
|
@ -137,13 +143,14 @@
|
||||||
? 'Maint.'
|
? 'Maint.'
|
||||||
: 'Loading'}
|
: 'Loading'}
|
||||||
</h1>
|
</h1>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-wrap text-center text-sm">{service.desc}</p>
|
<p class="text-wrap text-center text-sm">{server.desc}</p>
|
||||||
<p class="text-center text-sm font-bold text-destructive">{service.warn}</p>
|
<p class="text-center text-sm font-bold text-destructive">{server.warn}</p>
|
||||||
<div class="grid {service.extLink ? 'grid-cols-2' : 'grid-cols-1'} mt-auto justify-items-center">
|
<div class="grid {server.extLink ? 'grid-cols-2' : 'grid-cols-1'} mt-auto justify-items-center">
|
||||||
<a
|
<a
|
||||||
class="flex flex-row rounded-md border-x-2 px-2 text-sm hover:border-secondary hover:text-secondary"
|
class="flex flex-row rounded-md border-x-2 px-2 text-sm hover:border-secondary hover:text-secondary"
|
||||||
href={service.href}
|
href={server.href}
|
||||||
on:mouseover={() => (hover.link = true)}
|
on:mouseover={() => (hover.link = true)}
|
||||||
on:mouseleave={() => (hover.link = false)}
|
on:mouseleave={() => (hover.link = false)}
|
||||||
>
|
>
|
||||||
|
@ -160,10 +167,10 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
{#if service.extLink}
|
{#if server.extLink}
|
||||||
<a
|
<a
|
||||||
class="flex flex-row rounded-md border-x-2 px-2 text-sm hover:border-secondary hover:text-secondary"
|
class="flex flex-row rounded-md border-x-2 px-2 text-sm hover:border-secondary hover:text-secondary"
|
||||||
href={service.extLink}
|
href={server.extLink}
|
||||||
on:mouseover={() => (hover.ext = true)}
|
on:mouseover={() => (hover.ext = true)}
|
||||||
on:mouseleave={() => (hover.ext = false)}
|
on:mouseleave={() => (hover.ext = false)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export type Service = {
|
export type Service = {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly icon: string;
|
readonly icon?: string;
|
||||||
readonly iconType: IconType;
|
readonly iconType?: IconType;
|
||||||
readonly href: string;
|
readonly href: string;
|
||||||
readonly desc: string;
|
readonly desc: string;
|
||||||
readonly warn: string;
|
readonly warn: string;
|
||||||
|
@ -9,6 +9,17 @@ export type Service = {
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Server = {
|
||||||
|
readonly name: string;
|
||||||
|
readonly icon?: string;
|
||||||
|
readonly iconType?: IconType;
|
||||||
|
readonly connection?: string;
|
||||||
|
readonly href?: string;
|
||||||
|
readonly desc?: string;
|
||||||
|
readonly extLink?: string;
|
||||||
|
readonly id?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export enum IconType {
|
export enum IconType {
|
||||||
SVG = 'svg',
|
SVG = 'svg',
|
||||||
AVIF = 'avif',
|
AVIF = 'avif',
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export function GET() {
|
||||||
|
const content = fs.readFileSync('static/data/servers.json').toString();
|
||||||
|
|
||||||
|
const data = JSON.parse(content);
|
||||||
|
|
||||||
|
return json(data);
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { json } from '@sveltejs/kit';
|
||||||
export function GET() {
|
export function GET() {
|
||||||
const content = fs.readFileSync('static/data/services.json').toString();
|
const content = fs.readFileSync('static/data/services.json').toString();
|
||||||
|
|
||||||
let data = JSON.parse(content);
|
const data = JSON.parse(content);
|
||||||
|
|
||||||
return json(data);
|
return json(data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { io, Socket } from 'socket.io-client';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
const promise = getJwt();
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJwt(): Promise<string> {
|
||||||
|
const socket = io('https://status.neshweb.net/');
|
||||||
|
const credFile = './credentials.json';
|
||||||
|
let token = '';
|
||||||
|
let valid = false;
|
||||||
|
|
||||||
|
if (fs.existsSync(credFile)) {
|
||||||
|
const content = fs.readFileSync(credFile);
|
||||||
|
token = content.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('connect', async () => {
|
||||||
|
if (token == '') {
|
||||||
|
token = await login(socket);
|
||||||
|
valid = true;
|
||||||
|
} else {
|
||||||
|
socket.emit('loginByToken', token, async (res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
token = await login(socket);
|
||||||
|
}
|
||||||
|
valid = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
while (!valid) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(credFile, token);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(socket: Socket): Promise<string> {
|
||||||
|
let token = '';
|
||||||
|
socket.emit(
|
||||||
|
'login',
|
||||||
|
{ username: process.env.KUMA_USERNAME, password: process.env.KUMA_PASSWORD, token: '' },
|
||||||
|
(res: { token: string }) => {
|
||||||
|
token = res.token;
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
while (token == '') {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
|
@ -1,4 +1,106 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Server } from '$lib/types/data-types';
|
||||||
|
import { io } from 'socket.io-client';
|
||||||
|
import type { Heartbeat } from '$lib/types/uptime-kuma-types';
|
||||||
|
import ServerCard from "$lib/components/ServerCard.svelte";
|
||||||
|
|
||||||
|
let { data }: { data: { promise: Promise<string> } } = $props();
|
||||||
|
|
||||||
|
let token = $state();
|
||||||
|
|
||||||
|
data.promise.then((jwt) => {
|
||||||
|
token = jwt;
|
||||||
|
});
|
||||||
|
|
||||||
|
let servers: readonly Server[] = $state.frozen([]);
|
||||||
|
|
||||||
|
let icons: readonly string[] = $state.frozen([]);
|
||||||
|
|
||||||
|
let monitorList: Map<number, Heartbeat> = $state(new Map());
|
||||||
|
|
||||||
|
//let token = $props();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (token) {
|
||||||
|
const socket = io('https://status.neshweb.net/');
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
socket.emit('loginByToken', token, () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('importantHeartbeatList', (_, data) => {
|
||||||
|
let recent = data[data.length - 1];
|
||||||
|
let monitor: Heartbeat = {
|
||||||
|
monitorID: recent.monitor_id,
|
||||||
|
status: recent.status,
|
||||||
|
time: recent.time,
|
||||||
|
msg: recent.msg,
|
||||||
|
ping: recent.ping,
|
||||||
|
important: recent.important,
|
||||||
|
duration: recent.duration
|
||||||
|
};
|
||||||
|
monitorList.set(monitor.monitorID, monitor);
|
||||||
|
monitorList = new Map(monitorList.entries());
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('heartbeatList', (_, data) => {
|
||||||
|
let recent = data[data.length - 1];
|
||||||
|
let monitor: Heartbeat = {
|
||||||
|
monitorID: recent.monitor_id,
|
||||||
|
status: recent.status,
|
||||||
|
time: recent.time,
|
||||||
|
msg: recent.msg,
|
||||||
|
ping: recent.ping,
|
||||||
|
important: recent.important,
|
||||||
|
duration: recent.duration
|
||||||
|
};
|
||||||
|
monitorList.set(monitor.monitorID, monitor);
|
||||||
|
monitorList = new Map(monitorList.entries());
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('heartbeat', (data) => {
|
||||||
|
monitorList.set(data.monitorID, data);
|
||||||
|
monitorList = new Map(monitorList.entries());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function get(url: string): Promise<any> {
|
||||||
|
let res = await fetch(url);
|
||||||
|
if (res.ok) {
|
||||||
|
let data = await res.json();
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
get('/data/servers').then((data: Server[]) => {
|
||||||
|
servers = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
get('/assets/icons').then((data: string[]) => {
|
||||||
|
icons = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Servers</title>
|
<title>Servers</title>
|
||||||
<meta name="description" content="Overview of Game Servers running on neshweb.net" />
|
<meta name="description" content="Overview of Game Servers running on neshweb.net" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="flex max-h-full flex-row flex-wrap justify-center gap-10 overflow-auto p-8">
|
||||||
|
{#each servers as server}
|
||||||
|
{#if typeof server.id === 'undefined'}
|
||||||
|
<ServerCard {server} {icons} />
|
||||||
|
{:else}
|
||||||
|
<ServerCard {server} {icons} monitor={monitorList.get(server.id)} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,31 +1,26 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Minecraft",
|
"name": "Minecraft",
|
||||||
"icon": "/assets/icons/minecraft-logo",
|
"icon": "/assets/icons/minecraft-logo",
|
||||||
"iconType": "avif",
|
"iconType": "avif",
|
||||||
"href": "https://minecraft.neshweb.net/",
|
"connection": "minecraft.neshweb.net",
|
||||||
"desc": "View all currently available Minecraft Servers and their mods"
|
"href": "https://minecraft.neshweb.net/",
|
||||||
|
"desc": "View all currently available Minecraft Servers and their mods",
|
||||||
|
"id": 38
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Ready or Not",
|
"name": "Ready or Not",
|
||||||
"icon": "/assets/icons/ron-logo",
|
"icon": "/assets/icons/ron-logo",
|
||||||
"iconType": "avif",
|
"iconType": "avif",
|
||||||
"href": "https://readyornot.neshweb.net/",
|
"connection": "minecraft.neshweb.net",
|
||||||
"desc": "Collection of Floor Plans for the Game 'Ready or Not'"
|
"href": "https://readyornot.neshweb.net/",
|
||||||
|
"desc": "Collection of Floor Plans for the Game 'Ready or Not'"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Zomboid",
|
"name": "Factorio"
|
||||||
"icon": "/assets/icons/zomboid-logo",
|
|
||||||
"iconType": "avif",
|
|
||||||
"ip": "91.13.248.30",
|
|
||||||
"status": "Online"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Factorio",
|
"name": "Space Engineers",
|
||||||
"status": "Online"
|
"id": 13
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Space Engineers",
|
|
||||||
"status": "Online"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
Loading…
Reference in a new issue