Files
fdbck/src/routes/api/public/feedback/[slug]/+server.ts
mAi 778df213da mAi: #4 - single-submission follow-up fixes
Four bugs from m's smoke pass on the just-shipped single-submission feature:

1. Reload showed the legacy "you can submit again" branch instead of the
   read-only summary, because the client never refetched its previous
   submission. Fix: page server load now does an IP+UA backstop lookup so
   first paint is correct; client onMount supplements with a session_id
   lookup against the new GET ?session_id= variant for the
   cleared-cookies-but-same-browser case. Renamed JSON field
   previous_submission to keep server/client shape symmetric. Same parametised
   .eq() pattern as the submit handler — no PostgREST .or() with a
   user-controlled session id.

2. Trailing colon in "Du hast schon abgesendet. Du kannst trotzdem nochmal
   antworten:" reads like an unfinished sentence. Rewrote as a question:
   "Du hast bereits abgesendet. Möchtest du eine weitere Antwort senden?"
   The branch is now also gated on !singleSubmission — when the toggle is
   on it never fires (the previous_submission branch wins).

3. The .fb-already card looked like a form replica (boxes around values).
   Replaced with a confirmation summary: ✓-icon header ("Antwort gesendet"
   + timestamp), then a definition list with muted labels above plain
   values, no input outlines. On ≥560px the rows become a two-column grid
   with light dividers.

4. The "Noch eine Antwort senden" ghost button on the success card was
   misleading when single_submission is on (clicking it 409s on next
   submit). Hidden when singleSubmission is true; the success banner
   alone now stands.

bun check 0 errors, bun test 25 pass, bun build OK.
2026-05-06 15:44:36 +02:00

88 lines
2.9 KiB
TypeScript

/**
* GET /api/public/feedback/<slug> — public-facing instance config (slug = access token).
* GET /api/public/feedback/<slug>?session_id= — also resolves the caller's previous submission
* when single_submission is on, so the participant
* page can render the read-only summary on first
* paint instead of falling through to the legacy
* "submit again" branch on reload.
*/
import type { RequestHandler } from './$types';
import { json } from '$lib/server/response';
import { handleApiError, notFound } from '$lib/server/errors';
import { getInstanceBySlug, clampUserAgent } from '$lib/server/feedback';
import { fdb } from '$lib/server/fdb';
export const GET: RequestHandler = async ({ params, url, request, getClientAddress }) => {
try {
const inst = await getInstanceBySlug(params.slug);
if (!inst) return notFound('Feedback instance not found');
let previousSubmission:
| { submitted_at: string; display_name: string | null; answers: unknown }
| null = null;
if (inst.single_submission) {
const cols = 'answers, display_name, created_at';
const sessionId = url.searchParams.get('session_id');
if (sessionId && sessionId.length > 0 && sessionId.length <= 100) {
const r = await fdb()
.from('feedback_submissions')
.select(cols)
.eq('instance_id', inst.id)
.eq('client_session_id', sessionId)
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle();
if (r.error) throw r.error;
if (r.data) {
previousSubmission = {
submitted_at: r.data.created_at,
display_name: r.data.display_name,
answers: r.data.answers,
};
}
}
// Back-stop: same IP + UA. Catches the "cleared LocalStorage but same browser" case.
if (!previousSubmission) {
const ip = getClientAddress();
const ua = clampUserAgent(request.headers.get('user-agent'));
if (ua) {
const r = await fdb()
.from('feedback_submissions')
.select(cols)
.eq('instance_id', inst.id)
.eq('client_ip', ip)
.eq('user_agent', ua)
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle();
if (r.error) throw r.error;
if (r.data) {
previousSubmission = {
submitted_at: r.data.created_at,
display_name: r.data.display_name,
answers: r.data.answers,
};
}
}
}
}
return json({
title: inst.title,
description: inst.description,
form_definition: inst.form_definition,
chat_enabled: inst.chat_enabled,
live_results_enabled: inst.live_results_enabled,
single_submission: inst.single_submission,
status: inst.status,
closed_at: inst.closed_at,
previous_submission: previousSubmission,
});
} catch (e) {
return handleApiError(e, 'feedback GET');
}
};