);
+ }
@@ -229,6 +324,95 @@
/>
+ {:else if q.type === 'date_ranked_choice'}
+
+
+
+ {#each q.options as opt, optIdx (opt.id)}
+
+ {/each}
+
+
+
+
+
+
+
{/if}
diff --git a/src/lib/components/Results.svelte b/src/lib/components/Results.svelte
index 9a01aa4..52aaf56 100644
--- a/src/lib/components/Results.svelte
+++ b/src/lib/components/Results.svelte
@@ -26,6 +26,33 @@
}
}
+ const dateOptionFmt = new Intl.DateTimeFormat([], {
+ weekday: 'short',
+ day: '2-digit',
+ month: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ const dateOptionTimeFmt = new Intl.DateTimeFormat([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+
+ function fmtDateOption(start: string, end: string | null | undefined): string {
+ try {
+ const startStr = dateOptionFmt.format(new Date(start));
+ if (!end) return startStr;
+ const sd = new Date(start);
+ const ed = new Date(end);
+ if (sd.toDateString() === ed.toDateString()) {
+ return `${startStr}–${dateOptionTimeFmt.format(ed)}`;
+ }
+ return `${startStr} – ${dateOptionFmt.format(ed)}`;
+ } catch {
+ return start;
+ }
+ }
+
function questionDenominator(q: QuestionResult): number {
// For multi_choice, the count is the number of submissions that ticked
// at least one option; we render bars relative to that so percentages
@@ -105,6 +132,37 @@
{q.stats.no} · {pct(q.stats.no, denom)}%
+ {:else if q.stats.type === 'date_ranked_choice'}
+ {q.stats.count} {q.stats.count === 1 ? 'Antwort' : 'Antworten'}
+
+ {#each q.stats.options as opt, optIdx (opt.id)}
+ {@const denom = maxOf(opt.histogram.map((b) => b.count))}
+
+
+
#{optIdx + 1}
+
+
{fmtDateOption(opt.start, opt.end)}
+ {#if opt.label}
{opt.label}
{/if}
+
+
+ Schnitt: {fmtMean(opt.mean)}
+ · {opt.count} {opt.count === 1 ? 'Bewertung' : 'Bewertungen'}
+
+
+
+ {#each opt.histogram as bucket (bucket.value)}
+
+
{bucket.value}
+
+
{bucket.count}
+
+ {/each}
+
+
+ {/each}
+
{:else if q.stats.type === 'short_text' || q.stats.type === 'long_text'}
{q.stats.count} Antworten
{#if q.stats.answers.length > 0}
diff --git a/src/lib/styles/feedback.css b/src/lib/styles/feedback.css
index 8f424f5..60c1911 100644
--- a/src/lib/styles/feedback.css
+++ b/src/lib/styles/feedback.css
@@ -325,6 +325,80 @@ body { min-height: 100vh; }
margin-top: 0.4rem;
}
+.fb-date-ranked {
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+}
+
+.fb-date-ranked__row {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-md);
+ background: var(--color-bg-primary);
+ flex-wrap: wrap;
+}
+
+.fb-date-ranked__opt {
+ flex: 1 1 220px;
+ min-width: 0;
+}
+
+.fb-date-ranked__when {
+ font-weight: 500;
+ color: var(--color-text-primary);
+}
+
+.fb-date-ranked__label {
+ font-size: 0.85rem;
+ color: var(--fb-muted);
+ margin-top: 0.15rem;
+}
+
+.fb-date-ranked__scale {
+ flex: 0 1 auto;
+}
+
+.fb-date-ranked__skip {
+ min-width: 2.5rem;
+ min-height: 2.5rem;
+ padding: 0.5rem 0.75rem;
+ border: 1px dashed var(--color-border-primary);
+ border-radius: var(--radius-md);
+ background: transparent;
+ color: var(--fb-muted);
+ font-size: 1rem;
+ cursor: pointer;
+ transition: border-color 150ms ease, background 150ms ease;
+}
+
+.fb-date-ranked__skip:hover {
+ border-color: var(--color-primary);
+}
+
+.fb-date-ranked__skip--active {
+ background: var(--color-bg-secondary);
+ border-style: solid;
+ color: var(--color-text-primary);
+}
+
+@media (max-width: 480px) {
+ .fb-date-ranked__row {
+ align-items: stretch;
+ }
+ .fb-date-ranked__opt {
+ flex-basis: 100%;
+ }
+ .fb-date-ranked__scale {
+ width: 100%;
+ justify-content: space-between;
+ }
+}
+
/* Button system — primary base + secondary/ghost/danger variants + sm/lg/icon sizes. */
.fb-btn {
@@ -678,6 +752,43 @@ body { min-height: 100vh; }
margin-top: 0.25rem;
}
+.fb-builder__date-ranked {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.fb-builder__date-row {
+ display: flex;
+ gap: 0.5rem;
+ align-items: flex-start;
+ padding: 0.5rem;
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-md);
+ background: var(--color-bg-primary);
+}
+
+.fb-builder__date-fields {
+ display: grid;
+ grid-template-columns: max-content 1fr;
+ gap: 0.4rem 0.6rem;
+ align-items: center;
+ flex: 1;
+ min-width: 0;
+}
+
+.fb-builder__date-fields .fb-question__label {
+ margin: 0;
+ white-space: nowrap;
+ font-size: 0.85rem;
+}
+
+@media (max-width: 480px) {
+ .fb-builder__date-fields {
+ grid-template-columns: 1fr;
+ }
+}
+
.fb-builder__add {
display: flex;
flex-wrap: wrap;
@@ -814,6 +925,57 @@ body { min-height: 100vh; }
}
}
+.fb-results__date-ranked {
+ display: flex;
+ flex-direction: column;
+ gap: 0.9rem;
+}
+
+.fb-results__date-opt {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ padding: 0.6rem 0.75rem;
+ border: 1px solid var(--color-border-primary);
+ border-radius: var(--radius-md);
+ background: var(--color-bg-primary);
+}
+
+.fb-results__date-head {
+ display: flex;
+ gap: 0.6rem;
+ align-items: baseline;
+ flex-wrap: wrap;
+}
+
+.fb-results__date-rank {
+ font-weight: 600;
+ color: var(--fb-muted);
+ font-variant-numeric: tabular-nums;
+ min-width: 1.75rem;
+}
+
+.fb-results__date-when {
+ flex: 1 1 200px;
+ min-width: 0;
+}
+
+.fb-results__date-label {
+ font-size: 0.8rem;
+ color: var(--fb-muted);
+ margin-top: 0.1rem;
+}
+
+.fb-results__date-mean {
+ font-size: 0.85rem;
+ color: var(--color-text-primary);
+}
+
+.fb-results__date-count {
+ color: var(--fb-muted);
+ font-weight: 400;
+}
+
/* ─────────────────────────────────────────────────────────────────
Minimalist redesign — utility classes added 2026-05-06.
See docs/plans/ui-redesign.md for the rationale.
diff --git a/src/routes/f/[slug]/+page.svelte b/src/routes/f/[slug]/+page.svelte
index 16dc881..9275ec8 100644
--- a/src/routes/f/[slug]/+page.svelte
+++ b/src/routes/f/[slug]/+page.svelte
@@ -190,6 +190,21 @@
if (formDef) {
for (const q of formDef.questions) {
+ if (q.type === 'date_ranked_choice') {
+ const map = (answers[q.id] as Record | undefined) ?? {};
+ const rated = q.options.filter((opt) => typeof map[opt.id] === 'number');
+ if (q.required && rated.length === 0) {
+ submitError = `Bitte bewerte mindestens eine Option: ${q.label}`;
+ submitInFlight = false;
+ return;
+ }
+ if (q.allow_partial === false && rated.length < q.options.length) {
+ submitError = `Bitte bewerte alle Optionen: ${q.label}`;
+ submitInFlight = false;
+ return;
+ }
+ continue;
+ }
if (q.required) {
const v = answers[q.id];
const empty =
@@ -268,6 +283,45 @@
answers = { ...answers, [qid]: value };
}
+ function setDateRankedRating(qid: string, optId: string, rating: number | null): void {
+ const cur = (answers[qid] as Record | undefined) ?? {};
+ answers = { ...answers, [qid]: { ...cur, [optId]: rating } };
+ }
+
+ function dateRankedRating(qid: string, optId: string): number | null {
+ const cur = answers[qid] as Record | undefined;
+ if (!cur) return null;
+ const v = cur[optId];
+ return typeof v === 'number' ? v : null;
+ }
+
+ const dateOptionFmt = new Intl.DateTimeFormat([], {
+ weekday: 'short',
+ day: '2-digit',
+ month: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ const dateOptionTimeFmt = new Intl.DateTimeFormat([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+
+ function fmtDateOption(start: string, end?: string): string {
+ try {
+ const startStr = dateOptionFmt.format(new Date(start));
+ if (!end) return startStr;
+ const sd = new Date(start);
+ const ed = new Date(end);
+ if (sd.toDateString() === ed.toDateString()) {
+ return `${startStr}–${dateOptionTimeFmt.format(ed)}`;
+ }
+ return `${startStr} – ${dateOptionFmt.format(ed)}`;
+ } catch {
+ return start;
+ }
+ }
+
onMount(() => {
displayName = loadName();
const s = loadSession();
@@ -516,6 +570,43 @@
Nein
+ {:else if q.type === 'date_ranked_choice'}
+
+ {#if q.scale?.min_label || q.scale?.max_label}
+
+ 1 — {q.scale.min_label ?? 'passt nicht'}
+ 5 — {q.scale.max_label ?? 'passt super'}
+
+ {/if}
+ {#each q.options as opt (opt.id)}
+
+
+
{fmtDateOption(opt.start, opt.end)}
+ {#if opt.label}
{opt.label}
{/if}
+
+
+ {#each [1, 2, 3, 4, 5] as v (v)}
+
+ {/each}
+
+
+
+ {/each}
+
{/if}
{#if q.help}