From 643c356cb6f6203512f84698b6d76546e6c0dbd6 Mon Sep 17 00:00:00 2001 From: m Date: Tue, 5 May 2026 18:51:38 +0200 Subject: [PATCH] admin: per-row actions, English rewrite, segmented tabs, /admin/feedback/new MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-row action bar on the list page: [Edit] [Copy link] [Open] [Close|Reopen] [Delete] — Delete confirms then DELETE + invalidateAll(); Close/Reopen PATCHes status, no confirm; per-row error banner. Full English rewrite of admin chrome (list + detail + builder), login, landing. Drop dev jargon — "instance" / "slug" / "schema" / docs/plans references gone. Sample SAMPLE_FORM content also translated to a session-feedback example. Participant /f/ stays untouched (author- supplied content). Results.svelte stays as-is too — shared with the participant page where the surrounding chrome is German. Tab strip on /admin/feedback/ restyled as a segmented pill bar (.fb-tabs / .fb-tab / .fb-tab--active). Active tab gets the green primary-light background + bolder text + radius-md, hover lifts to white. Earlier tabs were nearly invisible. Split create form to its own route /admin/feedback/new (page + auth-only +page.server.ts mirroring the list loader). List page now shows just the form list with a "+ New form" CTA in the header. --- src/lib/components/FormBuilder.svelte | 34 +-- src/lib/styles/feedback.css | 43 ++++ src/routes/+page.svelte | 9 +- src/routes/admin/feedback/+page.svelte | 243 +++++++----------- src/routes/admin/feedback/[id]/+page.svelte | 113 ++++---- src/routes/admin/feedback/new/+page.server.ts | 7 + src/routes/admin/feedback/new/+page.svelte | 148 +++++++++++ src/routes/login/+page.svelte | 16 +- 8 files changed, 383 insertions(+), 230 deletions(-) create mode 100644 src/routes/admin/feedback/new/+page.server.ts create mode 100644 src/routes/admin/feedback/new/+page.svelte diff --git a/src/lib/components/FormBuilder.svelte b/src/lib/components/FormBuilder.svelte index 9a0856f..45c046b 100644 --- a/src/lib/components/FormBuilder.svelte +++ b/src/lib/components/FormBuilder.svelte @@ -4,12 +4,12 @@ let { value = $bindable() }: { value: FeedbackFormDefinition } = $props(); const TYPE_LABELS: Record = { - short_text: 'Kurztext', - long_text: 'Langtext', - single_choice: 'Single-Choice', - multi_choice: 'Multi-Choice', - scale: 'Skala', - boolean: 'Ja/Nein', + short_text: 'Short text', + long_text: 'Long text', + single_choice: 'Single choice', + multi_choice: 'Multiple choice', + scale: 'Scale', + boolean: 'Yes / No', }; const TYPES: FeedbackQuestion['type'][] = [ @@ -28,7 +28,7 @@ } function defaultQuestion(type: FeedbackQuestion['type']): FeedbackQuestion { - const base = { id: uid(), label: 'Neue Frage', required: false } as const; + const base = { id: uid(), label: 'New question', required: false } as const; switch (type) { case 'short_text': case 'long_text': @@ -126,9 +126,9 @@ {/each}
- - - + + +
@@ -148,12 +148,12 @@ checked={q.required === true} onchange={(e) => update(i, { required: (e.target as HTMLInputElement).checked })} /> - Pflichtfeld + Required {#if q.type === 'short_text' || q.type === 'long_text'}
- + {:else if q.type === 'single_choice' || q.type === 'multi_choice'}
- +
{#each q.options as opt, optIdx (optIdx)}
@@ -178,7 +178,7 @@ class="fb-builder__icon-btn fb-builder__icon-btn--danger" disabled={q.options.length <= 2} onclick={() => removeOption(i, optIdx)} - aria-label="Option entfernen" + aria-label="Remove option" >✕
{/each} @@ -210,7 +210,7 @@ />
- +
- + - + - fdbck — Feedback per Link + fdbck — feedback by link @@ -7,15 +7,14 @@

fdbck

- Per-Link Feedback-Forms und Live-Chat-Masken. - Anonym, ohne Anmeldung, nur mit langem Link. + Private feedback forms and live chat — share a link, get answers.

-

Diese Seite ist nur über persönlich geteilte Links erreichbar.

+

This page is only reachable through a private link shared with you.

- Admin-Login + Admin sign-in

diff --git a/src/routes/admin/feedback/+page.svelte b/src/routes/admin/feedback/+page.svelte index f94d3ea..8dedf90 100644 --- a/src/routes/admin/feedback/+page.svelte +++ b/src/routes/admin/feedback/+page.svelte @@ -1,36 +1,12 @@ @@ -100,103 +90,68 @@
-

Feedback Instances

-

Forms und Live-Chat-Masken — per langem Slug zugänglich.

+
+
+

Feedback forms

+

Collect feedback through forms or live chat. Share a private link with your audience.

+
+ + New form +
-

Neue Instance

-
-
- - -
- -
- - -
- -
- -
- -
-
- - -
- -
- Leer lassen für Chat-only. Schema-Doku: docs/plans/feedback-feature.md §5. -
-
- - {#if createError} -
{createError}
- {/if} - - -
-
- -
-

Bestehende Instances ({data.instances.length})

+

Your forms ({data.instances.length})

{#if data.instances.length === 0} -

Noch keine Instance erstellt.

+

No forms yet.

{:else}
{#each data.instances as i (i.id)} -
+
- {i.title} - + {i.title} + {modeLabel(i)} {#if i.status === 'closed'} closed {:else} - open + open {/if}
-
- {i.counts.submissions} Submissions · {i.counts.posts} Posts · seit {new Date(i.created_at).toLocaleDateString()} +
+ {i.counts.submissions} responses · {i.counts.posts} messages · created {new Date(i.created_at).toLocaleDateString()}
-
+
/f/{i.slug}
-
- - Öffnen +
+ Edit + + Open + +
+ {#if rowError[i.id]} +
{rowError[i.id]}
+ {/if}
{/each}
diff --git a/src/routes/admin/feedback/[id]/+page.svelte b/src/routes/admin/feedback/[id]/+page.svelte index e77fbe1..7a7c9b4 100644 --- a/src/routes/admin/feedback/[id]/+page.svelte +++ b/src/routes/admin/feedback/[id]/+page.svelte @@ -59,7 +59,7 @@ } function ensureBuilderForm(): void { - if (!editForm) editForm = { questions: [{ id: 'q1', label: 'Frage 1', type: 'short_text', required: false }] }; + if (!editForm) editForm = { questions: [{ id: 'q1', label: 'Question 1', type: 'short_text', required: false }] }; } let pollHandle: ReturnType | null = null; @@ -97,12 +97,12 @@ }); if (!res.ok) { const j = (await res.json().catch(() => ({}))) as { error?: string }; - actionError = j.error ?? `Fehler ${res.status}`; + actionError = j.error ?? `Error ${res.status}`; return; } await refresh(); } catch (e) { - actionError = e instanceof Error ? e.message : 'Netzwerkfehler'; + actionError = e instanceof Error ? e.message : 'Network error'; } } @@ -117,12 +117,12 @@ }); if (!res.ok) { const j = (await res.json().catch(() => ({}))) as { error?: string }; - actionError = j.error ?? `Fehler ${res.status}`; + actionError = j.error ?? `Error ${res.status}`; return; } await refresh(); } catch (e) { - actionError = e instanceof Error ? e.message : 'Netzwerkfehler'; + actionError = e instanceof Error ? e.message : 'Network error'; } finally { actionInFlight = false; } @@ -139,7 +139,7 @@ try { parsedForm = FeedbackFormDefinitionSchema.parse(JSON.parse(trimmed)); } catch (err) { - actionError = `JSON ungültig: ${err instanceof Error ? err.message : 'parse error'}`; + actionError = `Invalid JSON: ${err instanceof Error ? err.message : 'parse error'}`; actionInFlight = false; return; } @@ -150,7 +150,7 @@ try { parsedForm = FeedbackFormDefinitionSchema.parse(parsedForm); } catch (err) { - actionError = `Form-Definition ungültig: ${err instanceof Error ? err.message : 'parse error'}`; + actionError = `Invalid questions: ${err instanceof Error ? err.message : 'parse error'}`; actionInFlight = false; return; } @@ -171,7 +171,7 @@ }); if (!res.ok) { const j = (await res.json().catch(() => ({}))) as { error?: string; details?: unknown }; - actionError = j.error ?? `Fehler ${res.status}`; + actionError = j.error ?? `Error ${res.status}`; if (j.details) actionError += ' — ' + JSON.stringify(j.details); return; } @@ -180,26 +180,26 @@ syncJsonFromVisual(); activeTab = inst.form_definition ? 'results' : (inst.chat_enabled ? 'chat' : 'submissions'); } catch (e) { - actionError = e instanceof Error ? e.message : 'Netzwerkfehler'; + actionError = e instanceof Error ? e.message : 'Network error'; } finally { actionInFlight = false; } } async function destroy(): Promise { - const ok = confirm(`Wirklich Instance "${inst.title}" inkl. aller Submissions/Posts löschen?`); + const ok = confirm(`Delete "${inst.title}" and all its responses and messages? This cannot be undone.`); if (!ok) return; actionInFlight = true; try { const res = await fetch(`/api/admin/feedback/${inst.id}`, { method: 'DELETE' }); if (!res.ok) { const j = (await res.json().catch(() => ({}))) as { error?: string }; - actionError = j.error ?? `Fehler ${res.status}`; + actionError = j.error ?? `Error ${res.status}`; return; } await goto('/admin/feedback'); } catch (e) { - actionError = e instanceof Error ? e.message : 'Netzwerkfehler'; + actionError = e instanceof Error ? e.message : 'Network error'; } finally { actionInFlight = false; } @@ -219,7 +219,7 @@ function summarizeAnswer(v: unknown): string { if (v === null || v === undefined) return '—'; - if (typeof v === 'boolean') return v ? 'Ja' : 'Nein'; + if (typeof v === 'boolean') return v ? 'Yes' : 'No'; if (Array.isArray(v)) return v.join(', '); return String(v); } @@ -250,37 +250,37 @@ - {inst.title} — Feedback Admin + {inst.title} — Feedback admin

{inst.title}

{#if inst.description}

{inst.description}

{/if}
- + {inst.status} - /f/{inst.slug} - - Vorschau + /f/{inst.slug} + + Preview {#if inst.status === 'open'} {:else} {/if} - - - + + +
@@ -288,18 +288,19 @@
{actionError}
{/if} -
+
{#each [ { key: 'chat', label: `Chat (${posts.length})`, show: inst.chat_enabled }, - { key: 'results', label: `Ergebnisse${results ? ` (${results.total_submissions})` : ''}`, show: !!inst.form_definition }, - { key: 'submissions', label: `Submissions (${submissions.length})`, show: !!inst.form_definition }, - { key: 'edit', label: 'Bearbeiten', show: true }, + { key: 'results', label: `Results${results ? ` (${results.total_submissions})` : ''}`, show: !!inst.form_definition }, + { key: 'submissions', label: `Responses (${submissions.length})`, show: !!inst.form_definition }, + { key: 'edit', label: 'Edit', show: true }, ] as t (t.key)} {#if t.show} @@ -309,23 +310,23 @@ {#if activeTab === 'chat'}
-

Live-Chat

+

Live chat

{#if posts.length === 0} -

Noch keine Posts.

+

No messages yet.

{:else}
{#each posts as p (p.id)}
- {p.display_name ?? 'anonym'} + {p.display_name ?? 'anonymous'} {fmtDateTime(p.created_at)} session: {p.client_session_id.slice(0, 8)}…
@@ -338,24 +339,24 @@
{:else if activeTab === 'results'}
-

Ergebnisse{formVersion ? ` · v${formVersion}` : ''}

+

Results{formVersion ? ` · v${formVersion}` : ''}

{#if results} {:else} -

Keine Form-Definition.

+

No questions configured.

{/if}
{:else if activeTab === 'submissions'}
-

Form Submissions

+

Responses

{#if submissions.length === 0} -

Noch keine Submissions.

+

No responses yet.

{:else}
- - + + {#each questions as q (q.id)} @@ -364,9 +365,9 @@ {#each submissions as s (s.id)} - - - + + + {#each questions as q (q.id)}
Wann
When Name{q.label}
{fmtDateTime(s.created_at)}{s.display_name ?? 'anonym'}
{fmtDateTime(s.created_at)}{s.display_name ?? 'anonymous'} {answerCellFor(q.id, s)} @@ -381,47 +382,47 @@ {:else if activeTab === 'edit'}
-

Bearbeiten{formVersion ? ` · v${formVersion}` : ''}

+

Edit{formVersion ? ` · v${formVersion}` : ''}

{#if submissions.length > 0}
- {submissions.length} Antworten bereits eingegangen. Beim Speichern wird die Version automatisch hochgezählt — alte Antworten behalten ihren Snapshot. + {submissions.length} responses already received. Saving will automatically bump the version — earlier responses keep their original snapshot.
{/if}
- +
- +
- Form-Definition + Questions
+ >Visual
@@ -431,8 +432,8 @@ {#if editForm} {:else} -

Kein Formular angelegt.

- +

No questions configured.

+ {/if} {:else} +
+ +
+ +
+ +
+
+ + +
+ +
+ Leave empty for chat-only feedback. You can edit questions visually after the form is created. +
+
+ + {#if createError} +
{createError}
+ {/if} + + + +
+ diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index d242419..b51f541 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -21,12 +21,12 @@ }); if (!res.ok) { const j = (await res.json().catch(() => ({}))) as { error?: string }; - error = j.error ?? `Fehler ${res.status}`; + error = j.error ?? `Error ${res.status}`; return; } await goto(data.next); } catch (e) { - error = e instanceof Error ? e.message : 'Netzwerkfehler'; + error = e instanceof Error ? e.message : 'Network error'; } finally { inFlight = false; } @@ -34,20 +34,20 @@ - Login — fdbck + Sign in — fdbck
-

Admin-Login

-

Nur für m.

+

Sign in

+

Admin access only.

- +
- + - {inFlight ? 'Logging in …' : 'Einloggen'} + {inFlight ? 'Signing in…' : 'Sign in'}