This commit is contained in:
parent
94a8d2a8e7
commit
9d5c6db7da
11 changed files with 556 additions and 526 deletions
|
@ -7,9 +7,9 @@
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
data-sveltekit-preload-data="hover"
|
data-sveltekit-preload-data="hover"
|
||||||
style="background-image: url('/assets/background.avif')"
|
style="background-image: url('/assets/background.avif')"
|
||||||
class="overflow-hidden h-screen"
|
class="h-screen overflow-hidden"
|
||||||
>
|
>
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
150
src/app.pcss
150
src/app.pcss
|
@ -3,113 +3,113 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 40% 96.1%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 214.3 31.8% 91.4%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 222.2 47.4% 11.2%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 210 40% 96.1%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 210 40% 96.1%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--destructive: 0 72.2% 50.6%;
|
--destructive: 0 72.2% 50.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 222.2 84% 4.9%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--muted: 217.2 32.6% 17.5%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
--popover: 222.2 84% 4.9%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
--card: 222.2 84% 4.9%;
|
||||||
--card-foreground: 210 40% 98%;
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
--border: 217.2 32.6% 17.5%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
--primary: 210 40% 98%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--accent: 217.2 32.6% 17.5%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--ring: hsl(212.7,26.8%,83.9);
|
--ring: hsl(212.7, 26.8%, 83.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nordlys {
|
.nordlys {
|
||||||
--background: 0 0% 0%; /* #000000 */
|
--background: 0 0% 0%; /* #000000 */
|
||||||
--foreground: 173 80% 40%; /* #14b8a6 */
|
--foreground: 173 80% 40%; /* #14b8a6 */
|
||||||
|
|
||||||
--muted: 183 100% 96%; /* #ecfeff */
|
--muted: 183 100% 96%; /* #ecfeff */
|
||||||
--muted-foreground: 176 61% 19%; /* #134e4a */
|
--muted-foreground: 176 61% 19%; /* #134e4a */
|
||||||
|
|
||||||
--popover: 0 0% 0%; /* #000000 */
|
--popover: 0 0% 0%; /* #000000 */
|
||||||
--popover-foreground: 173 80% 40%; /* #14b8a6 */
|
--popover-foreground: 173 80% 40%; /* #14b8a6 */
|
||||||
|
|
||||||
--card: 0 0% 0%; /* #000000 */
|
--card: 0 0% 0%; /* #000000 */
|
||||||
--card-foreground: 173 80% 40%; /* #14b8a6 */
|
--card-foreground: 173 80% 40%; /* #14b8a6 */
|
||||||
|
|
||||||
--border: 183 100% 96%; /* #ecfeff */
|
--border: 183 100% 96%; /* #ecfeff */
|
||||||
--input: 183 100% 96%; /* #ecfeff */
|
--input: 183 100% 96%; /* #ecfeff */
|
||||||
|
|
||||||
--primary: 173 80% 40%; /* #14b8a6 */
|
--primary: 173 80% 40%; /* #14b8a6 */
|
||||||
--primary-foreground: 221 39% 11%; /* #111827 */
|
--primary-foreground: 221 39% 11%; /* #111827 */
|
||||||
|
|
||||||
--secondary: 183 100% 96%; /* #ecfeff */
|
--secondary: 183 100% 96%; /* #ecfeff */
|
||||||
--secondary-foreground: 173 80% 40%; /* #14b8a6 */
|
--secondary-foreground: 173 80% 40%; /* #14b8a6 */
|
||||||
|
|
||||||
--accent: 183 100% 96%; /* #ecfeff */
|
--accent: 183 100% 96%; /* #ecfeff */
|
||||||
--accent-foreground: 173 80% 40%; /* #14b8a6 */
|
--accent-foreground: 173 80% 40%; /* #14b8a6 */
|
||||||
|
|
||||||
--destructive: 0 70% 35%; /* #991b1b */
|
--destructive: 0 70% 35%; /* #991b1b */
|
||||||
--destructive-foreground: 173 80% 40%; /* #14b8a6 */
|
--destructive-foreground: 173 80% 40%; /* #14b8a6 */
|
||||||
|
|
||||||
--offline: var(--destructive);
|
--offline: var(--destructive);
|
||||||
--online: 142 76% 36%; /* #16a34a */
|
--online: 142 76% 36%; /* #16a34a */
|
||||||
--pending: 25 95% 53%; /* #f97316 */
|
--pending: 25 95% 53%; /* #f97316 */
|
||||||
--maintenance: 224 76% 48%; /* #1d4ed8 */
|
--maintenance: 224 76% 48%; /* #1d4ed8 */
|
||||||
|
|
||||||
/* that border thingy when you tab through stuff */
|
/* that border thingy when you tab through stuff */
|
||||||
--ring: hsl(168 84% 78%); /* #99f6e4 */
|
--ring: hsl(168 84% 78%); /* #99f6e4 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,66 +1,100 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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';
|
||||||
|
|
||||||
let {service} = $props();
|
let { service } = $props();
|
||||||
|
|
||||||
let hover = $state({
|
let hover = $state({
|
||||||
title: false,
|
title: false,
|
||||||
link: false,
|
link: false,
|
||||||
ext: false
|
ext: false
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col border-t-4 rounded-xl bg-black/55 border-maintenance p-4 gap-y-2 w-[30rem] h-48">
|
<div
|
||||||
<div class="flex flex-row justify-between pb-4">
|
class="flex h-48 w-[30rem] flex-col gap-y-2 rounded-xl border-t-4 border-maintenance bg-black/55 p-4"
|
||||||
<div class="flex flex-row gap-1 items-center" on:mouseover={() => hover.title = true} on:mouseleave={() => hover.title = false}>
|
>
|
||||||
{#if service.icon}
|
<div class="flex flex-row justify-between pb-4">
|
||||||
<img width="24px" class="h-6 w-6 cursor-pointer" src={service.icon} alt="{service.name} Logo">
|
<div
|
||||||
{:else}
|
class="flex flex-row items-center gap-1"
|
||||||
{/if}
|
on:mouseover={() => (hover.title = true)}
|
||||||
<a href={service.href} class="text-accent font-bold {hover.title ? 'text-primary': 'text-secondary'}">{service.name}</a>
|
on:mouseleave={() => (hover.title = false)}
|
||||||
{#if hover.title}
|
>
|
||||||
<div transition:slide={{ delay: 100, duration: 200, easing: quintInOut, axis: 'x' }} class="grid items-center">
|
{#if service.icon}
|
||||||
<OpenInNewWindow color={ hover.title ? "hsl(var(--primary))" : "hsl(var(--secondary)"} class="self-center"/>
|
<img
|
||||||
</div>
|
width="24px"
|
||||||
{/if}
|
class="h-6 w-6 cursor-pointer"
|
||||||
</div>
|
src={service.icon}
|
||||||
|
alt="{service.name} Logo"
|
||||||
|
/>
|
||||||
|
{:else}{/if}
|
||||||
|
<a
|
||||||
|
href={service.href}
|
||||||
|
class="font-bold text-accent {hover.title ? 'text-primary' : 'text-secondary'}"
|
||||||
|
>{service.name}</a
|
||||||
|
>
|
||||||
|
{#if hover.title}
|
||||||
|
<div
|
||||||
|
transition:slide={{ delay: 100, duration: 200, easing: quintInOut, axis: 'x' }}
|
||||||
|
class="grid items-center"
|
||||||
|
>
|
||||||
|
<OpenInNewWindow
|
||||||
|
color={hover.title ? 'hsl(var(--primary))' : 'hsl(var(--secondary)'}
|
||||||
|
class="self-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 class="border-b-2 rounded-md text-sm text-center w-16 text-maintenance border-maintenance">Loading</h1>
|
<h1 class="w-16 rounded-md border-b-2 border-maintenance text-center text-sm text-maintenance">
|
||||||
</div>
|
Loading
|
||||||
<p class="text-sm text-center text-accent text-wrap">{service.desc}</p>
|
</h1>
|
||||||
<p class="text-sm text-center font-bold text-destructive">{service.warn}</p>
|
</div>
|
||||||
<div class="grid {service.extLink ? 'grid-cols-2' : 'grid-cols-1'} justify-items-center mt-auto">
|
<p class="text-wrap text-center text-sm text-accent">{service.desc}</p>
|
||||||
<a
|
<p class="text-center text-sm font-bold text-destructive">{service.warn}</p>
|
||||||
class="flex flex-row text-sm border-x-2 rounded-md px-2 text-accent hover:border-primary hover:text-primary"
|
<div class="grid {service.extLink ? 'grid-cols-2' : 'grid-cols-1'} mt-auto justify-items-center">
|
||||||
href={service.href}
|
<a
|
||||||
on:mouseover={() => hover.link = true}
|
class="flex flex-row rounded-md border-x-2 px-2 text-sm text-accent hover:border-primary hover:text-primary"
|
||||||
on:mouseleave={() => hover.link = false}
|
href={service.href}
|
||||||
>
|
on:mouseover={() => (hover.link = true)}
|
||||||
Open
|
on:mouseleave={() => (hover.link = false)}
|
||||||
{#if hover.link}
|
>
|
||||||
<div transition:slide={{ delay: 100, duration: 200, easing: quintInOut, axis: 'x' }} class="pl-1 pr-0 grid items-center">
|
Open
|
||||||
<OpenInNewWindow color={ hover.link ? "hsl(var(--primary))" : "hsl(var(--secondary)"} class="self-center"/>
|
{#if hover.link}
|
||||||
</div>
|
<div
|
||||||
{/if}
|
transition:slide={{ delay: 100, duration: 200, easing: quintInOut, axis: 'x' }}
|
||||||
</a>
|
class="grid items-center pl-1 pr-0"
|
||||||
{#if service.extLink}
|
>
|
||||||
<a
|
<OpenInNewWindow
|
||||||
class="flex flex-row text-sm border-x-2 rounded-md px-2 text-accent hover:border-primary hover:text-primary"
|
color={hover.link ? 'hsl(var(--primary))' : 'hsl(var(--secondary)'}
|
||||||
href={service.extLink}
|
class="self-center"
|
||||||
on:mouseover={() => hover.ext = true}
|
/>
|
||||||
on:mouseleave={() => hover.ext = false}
|
</div>
|
||||||
>
|
{/if}
|
||||||
Official Site
|
</a>
|
||||||
{#if hover.ext}
|
{#if service.extLink}
|
||||||
<div transition:slide={{ delay: 100, duration: 200, easing: quintInOut, axis: 'x' }} class="pl-1 pr-0 grid items-center">
|
<a
|
||||||
<OpenInNewWindow color={ hover.ext ? "hsl(var(--primary))" : "hsl(var(--secondary)"} class="self-center"/>
|
class="flex flex-row rounded-md border-x-2 px-2 text-sm text-accent hover:border-primary hover:text-primary"
|
||||||
</div>
|
href={service.extLink}
|
||||||
{/if}
|
on:mouseover={() => (hover.ext = true)}
|
||||||
</a>
|
on:mouseleave={() => (hover.ext = false)}
|
||||||
{/if}
|
>
|
||||||
</div>
|
Official Site
|
||||||
|
{#if hover.ext}
|
||||||
|
<div
|
||||||
|
transition:slide={{ delay: 100, duration: 200, easing: quintInOut, axis: 'x' }}
|
||||||
|
class="grid items-center pl-1 pr-0"
|
||||||
|
>
|
||||||
|
<OpenInNewWindow
|
||||||
|
color={hover.ext ? 'hsl(var(--primary))' : 'hsl(var(--secondary)'}
|
||||||
|
class="self-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,155 +1,155 @@
|
||||||
{
|
{
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "Nextcloud",
|
"name": "Nextcloud",
|
||||||
"icon": "/assets/icons/nextcloud-logo.svg",
|
"icon": "/assets/icons/nextcloud-logo.svg",
|
||||||
"href": "https://nextcloud.neshweb.net/",
|
"href": "https://nextcloud.neshweb.net/",
|
||||||
"desc": "Self-hosted Cloud Storage Service",
|
"desc": "Self-hosted Cloud Storage Service",
|
||||||
"warn": "Note: Registration requires approval",
|
"warn": "Note: Registration requires approval",
|
||||||
"extLink": "https://nextcloud.com/",
|
"extLink": "https://nextcloud.com/",
|
||||||
"id": 7
|
"id": 7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Kavita",
|
"name": "Kavita",
|
||||||
"icon": "/assets/icons/kavita-logo.svg",
|
"icon": "/assets/icons/kavita-logo.svg",
|
||||||
"href": "https://kavita.neshweb.net",
|
"href": "https://kavita.neshweb.net",
|
||||||
"desc": "Self-hosted Manga Library",
|
"desc": "Self-hosted Manga Library",
|
||||||
"warn": "Registration via Admin invite",
|
"warn": "Registration via Admin invite",
|
||||||
"id": 5
|
"id": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Images",
|
"name": "Images",
|
||||||
"icon": "/assets/icons/images-logo.svg",
|
"icon": "/assets/icons/images-logo.svg",
|
||||||
"href": "https://imgs.neshweb.net/",
|
"href": "https://imgs.neshweb.net/",
|
||||||
"desc": "Self-hosted Chevereto Image Service",
|
"desc": "Self-hosted Chevereto Image Service",
|
||||||
"warn": "",
|
"warn": "",
|
||||||
"extLink": "https://chevereto.com/",
|
"extLink": "https://chevereto.com/",
|
||||||
"id": 4
|
"id": 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Calibre Web",
|
"name": "Calibre Web",
|
||||||
"icon": "/assets/icons/calibre-logo.avif",
|
"icon": "/assets/icons/calibre-logo.avif",
|
||||||
"href": "https://calibre.neshweb.net/",
|
"href": "https://calibre.neshweb.net/",
|
||||||
"desc": "Self-hosted Ebook Library Service",
|
"desc": "Self-hosted Ebook Library Service",
|
||||||
"warn": "Note: Registration only via Admin",
|
"warn": "Note: Registration only via Admin",
|
||||||
"id": 6
|
"id": 6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "PeerTube",
|
"name": "PeerTube",
|
||||||
"icon": "/assets/icons/peertube-logo.svg",
|
"icon": "/assets/icons/peertube-logo.svg",
|
||||||
"href": "https://tube.neshweb.net/",
|
"href": "https://tube.neshweb.net/",
|
||||||
"desc": "Self-hosted PeerTube Instance",
|
"desc": "Self-hosted PeerTube Instance",
|
||||||
"warn": "Note: Registration only via Admin",
|
"warn": "Note: Registration only via Admin",
|
||||||
"id": 8
|
"id": 8
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Mastodon",
|
"name": "Mastodon",
|
||||||
"icon": "/assets/icons/mastodon-logo.svg",
|
"icon": "/assets/icons/mastodon-logo.svg",
|
||||||
"href": "https://mastodon.neshweb.net/",
|
"href": "https://mastodon.neshweb.net/",
|
||||||
"desc": "Self-hosted Mastodon Instance",
|
"desc": "Self-hosted Mastodon Instance",
|
||||||
"warn": "Note: Registration requires approval",
|
"warn": "Note: Registration requires approval",
|
||||||
"id": 3
|
"id": 3
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Vaultwarden",
|
"name": "Vaultwarden",
|
||||||
"icon": "/assets/icons/vaultwarden-logo.svg",
|
"icon": "/assets/icons/vaultwarden-logo.svg",
|
||||||
"href": "https://vault.neshweb.net",
|
"href": "https://vault.neshweb.net",
|
||||||
"desc": "Self-hosted Password Manager",
|
"desc": "Self-hosted Password Manager",
|
||||||
"warn": "Note: Invite only",
|
"warn": "Note: Invite only",
|
||||||
"id": 9
|
"id": 9
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Jellyfin",
|
"name": "Jellyfin",
|
||||||
"icon": "/assets/icons/jellyfin-logo.svg",
|
"icon": "/assets/icons/jellyfin-logo.svg",
|
||||||
"href": "https://jellyfin.neshweb.net/",
|
"href": "https://jellyfin.neshweb.net/",
|
||||||
"desc": "Open-Source, Self-Hosted Media Platform",
|
"desc": "Open-Source, Self-Hosted Media Platform",
|
||||||
"warn": "Note: Registration only via Admin",
|
"warn": "Note: Registration only via Admin",
|
||||||
"id": 37
|
"id": 37
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Navidrome",
|
"name": "Navidrome",
|
||||||
"icon": "/assets/icons/navidrome-logo.avif",
|
"icon": "/assets/icons/navidrome-logo.avif",
|
||||||
"href": "https://navidrome.neshweb.net/",
|
"href": "https://navidrome.neshweb.net/",
|
||||||
"desc": "Open-Source, Self-Hosted Music Streaming Platform",
|
"desc": "Open-Source, Self-Hosted Music Streaming Platform",
|
||||||
"warn": "Note: Registration only via Admin",
|
"warn": "Note: Registration only via Admin",
|
||||||
"id": 10
|
"id": 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Gitlab",
|
"name": "Gitlab",
|
||||||
"icon": "/assets/icons/gitlab-logo.svg",
|
"icon": "/assets/icons/gitlab-logo.svg",
|
||||||
"href": "https://gitlab.neshweb.net/",
|
"href": "https://gitlab.neshweb.net/",
|
||||||
"desc": "Self-hosted Git Service",
|
"desc": "Self-hosted Git Service",
|
||||||
"warn": "Note: Registration only via Admin",
|
"warn": "Note: Registration only via Admin",
|
||||||
"id": 2
|
"id": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Forgejo",
|
"name": "Forgejo",
|
||||||
"icon": "/assets/icons/forgejo-logo.svg",
|
"icon": "/assets/icons/forgejo-logo.svg",
|
||||||
"href": "https://forgejo.neshweb.net/",
|
"href": "https://forgejo.neshweb.net/",
|
||||||
"desc": "Self-hosted Git Service",
|
"desc": "Self-hosted Git Service",
|
||||||
"warn": "Note: Registration only via Admin",
|
"warn": "Note: Registration only via Admin",
|
||||||
"id": 36
|
"id": 36
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Portainer",
|
"name": "Portainer",
|
||||||
"icon": "/assets/icons/portainer-logo.avif",
|
"icon": "/assets/icons/portainer-logo.avif",
|
||||||
"href": "https://portainer.neshweb.net/",
|
"href": "https://portainer.neshweb.net/",
|
||||||
"desc": "Docker Container Manager",
|
"desc": "Docker Container Manager",
|
||||||
"warn": "Note: Admin Only",
|
"warn": "Note: Admin Only",
|
||||||
"id": 34
|
"id": 34
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Nginx",
|
"name": "Nginx",
|
||||||
"icon": "/assets/icons/npm-logo.avif",
|
"icon": "/assets/icons/npm-logo.avif",
|
||||||
"href": "https://nginx.neshweb.net/",
|
"href": "https://nginx.neshweb.net/",
|
||||||
"desc": "Web-based Nginx Proxy Manager",
|
"desc": "Web-based Nginx Proxy Manager",
|
||||||
"warn": "Note: Admin Only",
|
"warn": "Note: Admin Only",
|
||||||
"id": 31
|
"id": 31
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Proxmox",
|
"name": "Proxmox",
|
||||||
"icon": "/assets/icons/proxmox-logo.avif",
|
"icon": "/assets/icons/proxmox-logo.avif",
|
||||||
"href": "https://proxmox.neshweb.net/",
|
"href": "https://proxmox.neshweb.net/",
|
||||||
"desc": "Hypervisor Webinterface",
|
"desc": "Hypervisor Webinterface",
|
||||||
"warn": "Note: Admin Only",
|
"warn": "Note: Admin Only",
|
||||||
"id": 33
|
"id": 33
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Dockge",
|
"name": "Dockge",
|
||||||
"icon": "/assets/icons/dockge-logo.avif",
|
"icon": "/assets/icons/dockge-logo.avif",
|
||||||
"href": "https://dockge.neshweb.net/",
|
"href": "https://dockge.neshweb.net/",
|
||||||
"desc": "Docker Compose WebUI",
|
"desc": "Docker Compose WebUI",
|
||||||
"warn": "Note: Admin Only",
|
"warn": "Note: Admin Only",
|
||||||
"id": 35
|
"id": 35
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"games": {
|
"games": {
|
||||||
"minecraft": {
|
"minecraft": {
|
||||||
"name": "Minecraft",
|
"name": "Minecraft",
|
||||||
"icon": "/assets/icons/minecraft-logo.avif",
|
"icon": "/assets/icons/minecraft-logo.avif",
|
||||||
"href": "https://minecraft.neshweb.net/",
|
"href": "https://minecraft.neshweb.net/",
|
||||||
"desc": "View all currently available Minecraft Servers and their mods"
|
"desc": "View all currently available Minecraft Servers and their mods"
|
||||||
},
|
},
|
||||||
"ready_or_not": {
|
"ready_or_not": {
|
||||||
"name": "Ready or Not",
|
"name": "Ready or Not",
|
||||||
"icon": "/assets/icons/ron-logo.avif",
|
"icon": "/assets/icons/ron-logo.avif",
|
||||||
"href": "https://readyornot.neshweb.net/",
|
"href": "https://readyornot.neshweb.net/",
|
||||||
"desc": "Collection of Floor Plans for the Game 'Ready or Not'"
|
"desc": "Collection of Floor Plans for the Game 'Ready or Not'"
|
||||||
},
|
},
|
||||||
"zomboid": {
|
"zomboid": {
|
||||||
"name": "Zomboid",
|
"name": "Zomboid",
|
||||||
"icon": "/assets/icons/zomboid-logo.avif",
|
"icon": "/assets/icons/zomboid-logo.avif",
|
||||||
"ip": "91.13.248.30",
|
"ip": "91.13.248.30",
|
||||||
"status": "Online"
|
"status": "Online"
|
||||||
},
|
},
|
||||||
"factorio": {
|
"factorio": {
|
||||||
"name": "Factorio",
|
"name": "Factorio",
|
||||||
"status": "Online"
|
"status": "Online"
|
||||||
},
|
},
|
||||||
"space_engineers": {
|
"space_engineers": {
|
||||||
"name": "Space Engineers",
|
"name": "Space Engineers",
|
||||||
"status": "Online"
|
"status": "Online"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button as ButtonPrimitive } from "bits-ui";
|
import { Button as ButtonPrimitive } from 'bits-ui';
|
||||||
import { cn } from "$lib/utils";
|
import { cn } from '$lib/utils';
|
||||||
import { buttonVariants, type Props, type Events } from ".";
|
import { buttonVariants, type Props, type Events } from '.';
|
||||||
|
|
||||||
type $$Props = Props;
|
type $$Props = Props;
|
||||||
type $$Events = Events;
|
type $$Events = Events;
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export let variant: $$Props["variant"] = "default";
|
export let variant: $$Props['variant'] = 'default';
|
||||||
export let size: $$Props["size"] = "default";
|
export let size: $$Props['size'] = 'default';
|
||||||
export let builders: $$Props["builders"] = [];
|
export let builders: $$Props['builders'] = [];
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,34 @@
|
||||||
import type { Button as ButtonPrimitive } from "bits-ui";
|
import type { Button as ButtonPrimitive } from 'bits-ui';
|
||||||
import { tv, type VariantProps } from "tailwind-variants";
|
import { tv, type VariantProps } from 'tailwind-variants';
|
||||||
import Root from "./button.svelte";
|
import Root from './button.svelte';
|
||||||
|
|
||||||
const buttonVariants = tv({
|
const buttonVariants = tv({
|
||||||
base: "inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
base: 'inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||||
destructive:
|
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
'border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary:
|
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
link: 'text-primary underline-offset-4 hover:underline'
|
||||||
link: "text-primary underline-offset-4 hover:underline"
|
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: 'h-9 px-4 py-2',
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: 'h-10 rounded-md px-8',
|
||||||
icon: "h-9 w-9"
|
icon: 'h-9 w-9'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default"
|
size: 'default'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type Variant = VariantProps<typeof buttonVariants>["variant"];
|
type Variant = VariantProps<typeof buttonVariants>['variant'];
|
||||||
type Size = VariantProps<typeof buttonVariants>["size"];
|
type Size = VariantProps<typeof buttonVariants>['size'];
|
||||||
|
|
||||||
type Props = ButtonPrimitive.Props & {
|
type Props = ButtonPrimitive.Props & {
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
|
|
|
@ -1,62 +1,56 @@
|
||||||
import { type ClassValue, clsx } from "clsx";
|
import { type ClassValue, clsx } from 'clsx';
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from 'tailwind-merge';
|
||||||
import { cubicOut } from "svelte/easing";
|
import { cubicOut } from 'svelte/easing';
|
||||||
import type { TransitionConfig } from "svelte/transition";
|
import type { TransitionConfig } from 'svelte/transition';
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlyAndScaleParams = {
|
type FlyAndScaleParams = {
|
||||||
y?: number;
|
y?: number;
|
||||||
x?: number;
|
x?: number;
|
||||||
start?: number;
|
start?: number;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const flyAndScale = (
|
export const flyAndScale = (
|
||||||
node: Element,
|
node: Element,
|
||||||
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
|
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
|
||||||
): TransitionConfig => {
|
): TransitionConfig => {
|
||||||
const style = getComputedStyle(node);
|
const style = getComputedStyle(node);
|
||||||
const transform = style.transform === "none" ? "" : style.transform;
|
const transform = style.transform === 'none' ? '' : style.transform;
|
||||||
|
|
||||||
const scaleConversion = (
|
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
|
||||||
valueA: number,
|
const [minA, maxA] = scaleA;
|
||||||
scaleA: [number, number],
|
const [minB, maxB] = scaleB;
|
||||||
scaleB: [number, number]
|
|
||||||
) => {
|
|
||||||
const [minA, maxA] = scaleA;
|
|
||||||
const [minB, maxB] = scaleB;
|
|
||||||
|
|
||||||
const percentage = (valueA - minA) / (maxA - minA);
|
const percentage = (valueA - minA) / (maxA - minA);
|
||||||
const valueB = percentage * (maxB - minB) + minB;
|
const valueB = percentage * (maxB - minB) + minB;
|
||||||
|
|
||||||
return valueB;
|
return valueB;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styleToString = (
|
const styleToString = (style: Record<string, number | string | undefined>): string => {
|
||||||
style: Record<string, number | string | undefined>
|
return Object.keys(style).reduce((str, key) => {
|
||||||
): string => {
|
if (style[key] === undefined) return str;
|
||||||
return Object.keys(style).reduce((str, key) => {
|
return str + `${key}:${style[key]};`;
|
||||||
if (style[key] === undefined) return str;
|
}, '');
|
||||||
return str + `${key}:${style[key]};`;
|
};
|
||||||
}, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
duration: params.duration ?? 200,
|
duration: params.duration ?? 200,
|
||||||
delay: 0,
|
delay: 0,
|
||||||
css: (t) => {
|
css: (t) => {
|
||||||
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
|
||||||
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
|
||||||
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
|
||||||
|
|
||||||
return styleToString({
|
return styleToString({
|
||||||
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
|
||||||
opacity: t
|
opacity: t
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
easing: cubicOut
|
easing: cubicOut
|
||||||
};
|
};
|
||||||
};
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import '../app.pcss';
|
import '../app.pcss';
|
||||||
import Header from "./Header.svelte";
|
import Header from './Header.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
|
|
|
@ -1,86 +1,86 @@
|
||||||
<div class="overflow-auto h-full">
|
<div class="h-full overflow-auto">
|
||||||
<h1>Welcome to SvelteKit</h1>
|
<h1>Welcome to SvelteKit</h1>
|
||||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
<p>gap</p>
|
<p>gap</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,47 +1,53 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {page} from "$app/stores";
|
import { page } from '$app/stores';
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
|
||||||
const button = "border-t-2 bg-black/55 hover:bg-black/70 hover:border-primary w-28";
|
|
||||||
|
|
||||||
|
const button = 'border-t-2 bg-black/55 hover:bg-black/70 hover:border-primary w-28';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<ul
|
||||||
<ul class="w-full h-16 border-b sticky flex flex-row items-center justify-center gap-3 backdrop-blur-sm bg-black/40">
|
class="sticky flex h-16 w-full flex-row items-center justify-center gap-3 border-b bg-black/40 backdrop-blur-sm"
|
||||||
<li>
|
>
|
||||||
<Button
|
<li>
|
||||||
variant="ghost"
|
<Button
|
||||||
href="/"
|
variant="ghost"
|
||||||
class="{button} + {$page.url.pathname == '/' ? 'border-primary text-primary' : 'text-accent'}"
|
href="/"
|
||||||
>
|
class="{button} + {$page.url.pathname === '/' ? 'border-primary text-primary' : 'text-accent'}"
|
||||||
Home
|
>
|
||||||
</Button>
|
Home
|
||||||
</li>
|
</Button>
|
||||||
<li>
|
</li>
|
||||||
<Button
|
<li>
|
||||||
variant="ghost"
|
<Button
|
||||||
href="/servers"
|
variant="ghost"
|
||||||
class="{button} + {$page.url.pathname.startsWith('/servers') ? 'border-primary text-primary' : 'text-accent'}"
|
href="/servers"
|
||||||
>
|
class="{button} + {$page.url.pathname.startsWith('/servers')
|
||||||
Servers
|
? 'border-primary text-primary'
|
||||||
</Button>
|
: 'text-accent'}"
|
||||||
</li>
|
>
|
||||||
<li>
|
Servers
|
||||||
<Button
|
</Button>
|
||||||
variant="ghost"
|
</li>
|
||||||
href="/services"
|
<li>
|
||||||
class="{button} + {$page.url.pathname.startsWith('/services') ? 'border-primary text-primary' : 'text-accent'}"
|
<Button
|
||||||
>
|
variant="ghost"
|
||||||
Services
|
href="/services"
|
||||||
</Button>
|
class="{button} + {$page.url.pathname.startsWith('/services')
|
||||||
</li>
|
? 'border-primary text-primary'
|
||||||
<li>
|
: 'text-accent'}"
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
Services
|
||||||
href="/about"
|
</Button>
|
||||||
class="{button} + {$page.url.pathname.startsWith('/about') ? 'border-primary text-primary' : 'text-accent'}"
|
</li>
|
||||||
>
|
<li>
|
||||||
About
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</li>
|
href="/about"
|
||||||
|
class="{button} + {$page.url.pathname.startsWith('/about')
|
||||||
|
? 'border-primary text-primary'
|
||||||
|
: 'text-accent'}"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
|
@ -1,20 +1,19 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import pages from '$lib/components/pages.json';
|
import pages from '$lib/components/pages.json';
|
||||||
import Card from "$lib/components/Card.svelte";
|
import Card from '$lib/components/Card.svelte';
|
||||||
|
|
||||||
const services = $state(pages.services);
|
const services = $state(pages.services);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Services</title>
|
<title>Services</title>
|
||||||
<meta name="description" content="Overview of Services running on neshweb.net">
|
<meta name="description" content="Overview of Services running on neshweb.net" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="flex h-full flex-row flex-wrap justify-center gap-10 overflow-auto p-8">
|
||||||
<div class="flex flex-row flex-wrap justify-center gap-10 p-8 overflow-auto h-full">
|
{#each services as service}
|
||||||
{#each services as service}
|
<Card {service} />
|
||||||
<Card {service} />
|
{/each}
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
Loading…
Reference in a new issue