moved sites to postgres database, use button to add more

This commit is contained in:
2025-09-03 00:15:33 +02:00
parent a2a4fdd770
commit a5b9823b10
16 changed files with 242 additions and 120 deletions

View File

@@ -1,13 +1,7 @@
<script lang="ts">
import External from '$lib/icons/external.svelte';
import type { Site } from '$lib/interfaces/site.ts';
interface Site {
title: string;
image: string;
link: string;
background?: string;
color?: string;
}
let { title, image, background, color, link }: Site = $props();
@@ -60,7 +54,7 @@
h2,
.link,
.title {
transition: all 0.2s ease-in-out;
transition: all 0.18s ease-in-out;
}
.title {
@@ -90,7 +84,8 @@
.image {
height: 8rem;
width: 100%;
margin: 1.2rem 0;
width: 8rem;
margin: 1.2rem auto;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
@@ -105,6 +100,7 @@
left: calc(100% - 2rem);
top: 0;
opacity: 0;
fill: var(--color);
}
@media screen and (max-width: 750px) {
@@ -130,7 +126,6 @@
opacity: 1;
left: calc(100% - 1rem);
top: 2px;
fill: var(--color);
}
}
}

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Input from '$lib/components/Input.svelte';
import ColorInput from '$lib/components/ColorInput.svelte';
import Id from '$lib/icons/id.svelte';
import TextColor from '$lib/icons/text-color.svelte';
import TextSize from '$lib/icons/text-size.svelte';
import Window from '$lib/icons/window.svelte';
import Quill from '$lib/icons/quill.svelte';
import Tag from '$lib/icons/tag.svelte';
import Link from '$lib/icons/link.svelte';
import PaintRoller from '$lib/icons/paint-roller.svelte';
import Bucket from '$lib/icons/bucket.svelte';
import Picture from '$lib/icons/picture.svelte';
const dispatch = createEventDispatcher();
const close = () => dispatch('close');
</script>
<form method="POST">
<div class="wrapper">
<Input label="Name" icon={TextSize} placeholder="Website name" required />
<Input label="Link" icon={Link} placeholder="https://site.tld" required />
<Input label="Image" icon={Picture} placeholder="/images/site.png" required />
<ColorInput label="Color" icon={PaintRoller} placeholder="#21ADF6" required />
<ColorInput label="Background" icon={Bucket} />
</div>
<footer>
<button on:click={close} aria-disabled="false" type="button" tabindex="0"
><span tabindex="-1">Cancel</span></button
>
<button class="affirmative" type="submit" tabindex="-1">
<span tabindex="-1">Add connection</span>
</button>
</footer>
</form>
<style lang="scss">
form {
.wrapper {
display: flex;
flex-direction: column;
}
footer {
padding: 0.75rem 1.5rem;
max-width: 100%;
height: 2.5rem;
width: auto;
display: flex;
justify-content: flex-end;
flex: 0 0 auto;
gap: 1rem;
flex-wrap: wrap;
button {
flex: unset;
span {
font-size: 0.8rem;
}
}
}
}
:global(form .wrapper div) {
margin-bottom: 0.5rem;
}
</style>

View File

@@ -0,0 +1,9 @@
export interface Site {
name: string;
link: string;
image: string;
color: string;
background: string;
created?: number;
updated?: number;
}

View File

@@ -0,0 +1,21 @@
import { getDb } from '../database';
import type { Site } from '$lib/interfaces/site';
export async function allSites(): Promise<Array<Site>> {
const pool = await getDb();
const query = 'SELECT * FROM site';
const result = await pool.query(query);
return result.rows || [];
}
export async function addSite(site: Site) {
const timestamp = Math.floor(new Date().getTime() / 1000);
const query = `INSERT INTO site (name, link, image, color, background, updated)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id`;
const { name, link, image, color, background } = site;
const pool = await getDb();
const result = await pool.query(query, [name, link, image, color, background, timestamp]);
return { id: result.rows[0].id };
}

View File

@@ -0,0 +1,49 @@
import { allSites, addSite } from '$lib/server/database/sites';
import type { Site } from '$lib/interfaces/site';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async (): Promise<{ sites: Array<Site> }> => {
let sites: Site[] = [];
try {
sites = await allSites();
console.log('got sites:', sites);
} catch (error) {
console.error('error while fetching sites server props, likely db issue');
console.error(error);
}
return { sites };
};
export const actions = {
default: async ({ request }) => {
try {
const formData = await request.formData();
// Extract values by input `name` attributes
const name = formData.get('Name')?.toString().trim();
const link = formData.get('Link')?.toString().trim();
const image = formData.get('Image')?.toString().trim();
const color = formData.get('Color')?.toString().trim();
const background = formData.get('Background')?.toString().trim();
if (!name || !link || !image || !color || !background) {
return { error: 'All fields are required!', success: false, statusCode: 400 };
}
if (!name || !link) {
return { error: 'name & link are required', success: false, statusCode: 400 };
}
const site: Site = { name, link, image, color, background };
await addSite(site);
return { success: true };
} catch (err: unknown) {
console.log(err);
console.error('Failed to add site:', err.message);
return { error: 'internal server error', success: false, statusCode: 500 };
}
}
} satisfies Actions;

View File

@@ -1,103 +1,26 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte';
import Dialog from '$lib/components/Dialog.svelte';
import Section from '$lib/components/Section.svelte';
import FormSite from '$lib/components/forms/FormSite.svelte';
import ThumbnailButton from '$lib/components/ThumbnailButton.svelte';
import type { Site } from '$lib/interfaces/site.ts';
interface Site {
title: string;
image: string;
link: string;
background?: string;
color?: string;
}
let { data }: { data: { site: Site } } = $props();
let open = $state(false);
const sites: Array<Site> = [
{
title: 'Grafana',
image: '/images/grafana.png',
link: 'https://grafana.schleppe.cloud',
background: '#F5E3DC',
color: '#F05A24'
},
{
title: 'Prometheus',
image: '/images/prometheus.svg',
link: 'http://prome.schleppe:9090',
background: '#262221',
color: '#F3BFA2'
},
{
title: 'Traefik',
image: '/images/traefik.png',
link: 'https://grafana.schleppe.cloud',
background: '#30A4C2',
color: 'white'
},
{
title: 'Kibana',
image: '/images/kibana.svg',
link: 'https://kibana.schleppe.cloud',
background: '#f6cfdd',
color: '#401C26'
},
{
title: 'HASS',
image: '/images/hass.png',
link: 'http://homeassistant.schleppe:8123',
background: '#1ABCF2',
color: 'white'
},
{
title: 'Vault',
image: '/images/vault.svg',
link: 'http://vault.schleppe:8200',
background: 'white',
color: 'black'
},
{
title: 'Drone',
image: '/images/drone.png',
link: 'https://drone.schleppe.cloud',
background: '#D8E2F0',
color: '#1E375A'
},
{
title: 'Immich',
image: '/images/immich.png',
link: 'http://immich.schleppe:2283',
background: 'white',
color: 'black'
},
{
title: 'Wiki',
image: '/images/xwiki.png',
link: 'https://wiki.schleppe.cloud',
background: 'white',
color: 'black'
},
{
title: 'Gitea',
image: '/images/gitea.png',
link: 'https://git.schleppe.cloud',
background: '#E6E7D7',
color: '#609925'
},
{
title: 'PBS',
image: '/images/proxmox.png',
link: 'https://clio.schleppe:8007',
background: '#EDE1D2',
color: '#E66B00'
}
];
const { sites } = data;
</script>
<PageHeader>Sites</PageHeader>
<PageHeader>Sites
<button class="add-site-btn affirmative" on:click={() => (open = true)}><span>Add new site</span></button>
</PageHeader>
<div class="section-wrapper">
{#each sites as site}
{#each sites as site (site)}
<ThumbnailButton
title={site.title}
title={site.name}
image={site.image}
background={site.background}
color={site.color}
@@ -106,27 +29,15 @@
{/each}
</div>
<div class="section-wrapper full-width">
<Section
title="Expose HTTP traffic"
description="You can reach your Application on a specific Port you configure, redirecting all your domains to it. You can make it Private by disabling HTTP traffic."
/>
<Section
title="IP restrictions"
description="Restrict or block access to your application based on specific IP addresses or CIDR blocks."
/>
<Section
title="Expose HTTP traffic"
description="You can reach your Application on a specific Port you configure, redirecting all your domains to it. You can make it Private by disabling HTTP traffic."
/>
<Section
title="Connected services"
description="Connected services can communicate with your application over the private network."
/>
</div>
{#if open}
<Dialog
on:close={() => (open = false)}
title="Add new site"
description="You can select anything deployed in <b>Belgium (europe-west1) datacenter</b> and create an internal connection with your service."
>
<FormSite on:close={() => (open = false)} />
</Dialog>
{/if}
<style lang="scss">
.section-wrapper {
@@ -149,4 +60,10 @@
margin-top: 4rem;
}
}
:global(button.add-site-btn) {
font-size: 1.2rem;
float: right;
height: 2.5rem;
}
</style>