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.
88 lines
2.9 KiB
TypeScript
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');
|
|
}
|
|
};
|