From 696b796383228eda7ca446868f4738e7fca02535 Mon Sep 17 00:00:00 2001 From: mAi Date: Tue, 5 May 2026 23:13:13 +0200 Subject: [PATCH] mAi: #2 - admin Share section + env-var docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-contained "Share" section on the admin detail page. When no short URL exists yet: shows an optional custom-slug input + "Create short link" button. When one exists: shows the URL with Copy + Open buttons and a collapsed "Replace" form for picking a new slug. Append-only — does not touch existing buttons, the icon system, or feedback.css; uses inline styles + existing fb-* classes only, so it stays out of dokploy's parallel button-system refactor. .env.example documents SHLINK_URL + SHLINK_API_KEY (must be copied from the flexsiebels.de Dokploy app config to fdbck.msbls.de before this works in prod). Refs m/fdbck#2. --- .env.example | 5 + src/routes/admin/feedback/[id]/+page.svelte | 130 ++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/.env.example b/.env.example index fa7c8c6..5fbef6d 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,8 @@ PUBLIC_SITE_URL=https://fdbck.msbls.de # Optional cookie scope (leave unset for host-only) # COOKIE_DOMAIN=.msbls.de + +# Shlink short-URL service (admin "Share" feature) +# SHLINK_URL defaults to https://msbls.de when unset. +SHLINK_URL=https://msbls.de +SHLINK_API_KEY= diff --git a/src/routes/admin/feedback/[id]/+page.svelte b/src/routes/admin/feedback/[id]/+page.svelte index 7a7c9b4..87ac39d 100644 --- a/src/routes/admin/feedback/[id]/+page.svelte +++ b/src/routes/admin/feedback/[id]/+page.svelte @@ -24,6 +24,11 @@ let editChatEnabled = $state(inst.chat_enabled); let editLiveResults = $state(inst.live_results_enabled); + let shareSlugInput = $state(''); + let shareInFlight = $state(false); + let shareError = $state(null); + let shareCopied = $state(false); + let editMode = $state<'visual' | 'json'>('visual'); let editForm = $state( inst.form_definition ? (inst.form_definition as FeedbackFormDefinition) : null, @@ -213,6 +218,46 @@ } } + async function copyShortUrl(): Promise { + if (!inst.short_url) return; + try { + await navigator.clipboard.writeText(inst.short_url); + shareCopied = true; + setTimeout(() => (shareCopied = false), 1500); + } catch { + // silent + } + } + + async function createShareLink(): Promise { + shareError = null; + shareInFlight = true; + try { + const slug = shareSlugInput.trim(); + const body: { customSlug?: string } = {}; + if (slug) body.customSlug = slug; + + const res = await fetch(`/api/admin/feedback/${inst.id}/share`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const j = (await res.json().catch(() => ({}))) as { error?: string; details?: unknown }; + shareError = j.error ?? `Error ${res.status}`; + if (j.details) shareError += ' — ' + JSON.stringify(j.details); + return; + } + const j = (await res.json()) as { instance: typeof inst }; + inst = j.instance; + shareSlugInput = ''; + } catch (e) { + shareError = e instanceof Error ? e.message : 'Network error'; + } finally { + shareInFlight = false; + } + } + function fmtDateTime(iso: string): string { return new Date(iso).toLocaleString(); } @@ -288,6 +333,91 @@
{actionError}
{/if} +
+

Share

+ {#if inst.short_url} +

+ Memorable short link — resolves to /f/{inst.slug}. +

+
+ + {inst.short_url} + + + + Open ↗ + +
+
+ + Replace with a different short link + +
+ +
+ + +
+

+ Leave blank for a random short code, or pick a memorable slug like vote or session-feedback. +

+
+
+ {:else} +

+ Create a memorable short link (e.g. https://msbls.de/vote) that redirects to this form. +

+
+ +
+ + +
+

+ Leave blank for a random short code, or pick a memorable slug like vote or session-feedback. +

+
+ {/if} + {#if shareError} +
{shareError}
+ {/if} +
+
{#each [ { key: 'chat', label: `Chat (${posts.length})`, show: inst.chat_enabled },