mAi: #1 - UI: builder + participant renderer + results display
- FormBuilder.svelte: new `Date ranked choice` type in the type picker. Per-option editor uses datetime-local inputs (start required, end optional) plus an optional free-text label. Below the option list, two optional rating-1 / rating-5 label fields and an "Allow participants to skip individual options" toggle (default on). Local↔UTC conversion helpers keep storage in UTC ISO 8601 while the input element shows the author's local time. - /f/[slug]: participant rows of `(date · optional label)` + 1-5 button group + a "—" skip button. Required check enforces "at least one rated" for required questions and "all rated" when allow_partial=false. - Results.svelte: ranked list of options with mean rating, count, and a per-option distribution histogram. Heading shows the option's local-time date range. Sort order comes from the aggregator (mean desc + tiebreaks). - feedback.css: layout for the new builder rows, participant rating rows (mobile-stacks), and the ranked results list. Refs m/fdbck#1.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
multi_choice: 'Multiple choice',
|
||||
scale: 'Scale',
|
||||
boolean: 'Yes / No',
|
||||
date_ranked_choice: 'Date ranked choice',
|
||||
};
|
||||
|
||||
const TYPES: FeedbackQuestion['type'][] = [
|
||||
@@ -20,6 +21,7 @@
|
||||
'multi_choice',
|
||||
'scale',
|
||||
'boolean',
|
||||
'date_ranked_choice',
|
||||
];
|
||||
|
||||
function uid(): string {
|
||||
@@ -28,6 +30,37 @@
|
||||
return 'q_' + Array.from(buf, (b) => b.toString(36)).join('').slice(0, 8);
|
||||
}
|
||||
|
||||
function optUid(): string {
|
||||
const buf = new Uint8Array(4);
|
||||
(globalThis.crypto ?? (window as unknown as { crypto: Crypto }).crypto).getRandomValues(buf);
|
||||
return 'opt_' + Array.from(buf, (b) => b.toString(36)).join('').slice(0, 6);
|
||||
}
|
||||
|
||||
/** Round `now` to the next full hour and return as ISO 8601 UTC. */
|
||||
function defaultStartIso(offsetHours = 0): string {
|
||||
const d = new Date();
|
||||
d.setMinutes(0, 0, 0);
|
||||
d.setHours(d.getHours() + 1 + offsetHours);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
/** Convert a stored UTC ISO string into the `YYYY-MM-DDTHH:MM` shape that `<input type="datetime-local">` expects, in the viewer's local time. */
|
||||
function isoToLocalInput(iso: string | undefined | null): string {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
/** Convert a `<input type="datetime-local">` value (local time, no offset) into a UTC ISO string. */
|
||||
function localInputToIso(local: string): string | null {
|
||||
if (!local) return null;
|
||||
const d = new Date(local);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
function defaultQuestion(type: FeedbackQuestion['type']): FeedbackQuestion {
|
||||
const base = { id: uid(), label: 'New question', required: false } as const;
|
||||
switch (type) {
|
||||
@@ -41,6 +74,16 @@
|
||||
return { ...base, type, min: 1, max: 5 };
|
||||
case 'boolean':
|
||||
return { ...base, type };
|
||||
case 'date_ranked_choice':
|
||||
return {
|
||||
...base,
|
||||
type,
|
||||
options: [
|
||||
{ id: optUid(), start: defaultStartIso(0) },
|
||||
{ id: optUid(), start: defaultStartIso(24) },
|
||||
],
|
||||
allow_partial: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +138,58 @@
|
||||
if (q.options.length <= 2) return;
|
||||
update(idx, { options: q.options.filter((_, i) => i !== optIdx) } as Partial<FeedbackQuestion>);
|
||||
}
|
||||
|
||||
function setDateOption(idx: number, optIdx: number, patch: { start?: string; end?: string | null; label?: string | null }): void {
|
||||
const q = value.questions[idx];
|
||||
if (q.type !== 'date_ranked_choice') return;
|
||||
const options = q.options.map((opt, i) => {
|
||||
if (i !== optIdx) return opt;
|
||||
const next = { ...opt };
|
||||
if (patch.start !== undefined) next.start = patch.start;
|
||||
if (patch.end !== undefined) {
|
||||
if (patch.end === null || patch.end === '') delete next.end;
|
||||
else next.end = patch.end;
|
||||
}
|
||||
if (patch.label !== undefined) {
|
||||
if (patch.label === null || patch.label === '') delete next.label;
|
||||
else next.label = patch.label;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
update(idx, { options } as Partial<FeedbackQuestion>);
|
||||
}
|
||||
|
||||
function addDateOption(idx: number): void {
|
||||
const q = value.questions[idx];
|
||||
if (q.type !== 'date_ranked_choice') return;
|
||||
if (q.options.length >= 50) return;
|
||||
update(idx, {
|
||||
options: [...q.options, { id: optUid(), start: defaultStartIso(24 * (q.options.length + 1)) }],
|
||||
} as Partial<FeedbackQuestion>);
|
||||
}
|
||||
|
||||
function removeDateOption(idx: number, optIdx: number): void {
|
||||
const q = value.questions[idx];
|
||||
if (q.type !== 'date_ranked_choice') return;
|
||||
if (q.options.length <= 2) return;
|
||||
update(idx, { options: q.options.filter((_, i) => i !== optIdx) } as Partial<FeedbackQuestion>);
|
||||
}
|
||||
|
||||
function setScaleLabel(idx: number, which: 'min_label' | 'max_label', val: string): void {
|
||||
const q = value.questions[idx];
|
||||
if (q.type !== 'date_ranked_choice') return;
|
||||
const scale = { ...(q.scale ?? {}) };
|
||||
if (val === '') delete scale[which];
|
||||
else scale[which] = val;
|
||||
const empty = !scale.min_label && !scale.max_label;
|
||||
update(idx, { scale: empty ? undefined : scale } as Partial<FeedbackQuestion>);
|
||||
}
|
||||
|
||||
function setAllowPartial(idx: number, allow: boolean): void {
|
||||
const q = value.questions[idx];
|
||||
if (q.type !== 'date_ranked_choice') return;
|
||||
update(idx, { allow_partial: allow } as Partial<FeedbackQuestion>);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="fb-builder">
|
||||
@@ -229,6 +324,95 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if q.type === 'date_ranked_choice'}
|
||||
<div class="fb-question">
|
||||
<label class="fb-question__label">Date / time options</label>
|
||||
<div class="fb-builder__date-ranked">
|
||||
{#each q.options as opt, optIdx (opt.id)}
|
||||
<div class="fb-builder__date-row">
|
||||
<div class="fb-builder__date-fields">
|
||||
<label class="fb-question__label">Start</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="fb-input"
|
||||
value={isoToLocalInput(opt.start)}
|
||||
oninput={(e) => {
|
||||
const iso = localInputToIso((e.target as HTMLInputElement).value);
|
||||
if (iso) setDateOption(i, optIdx, { start: iso });
|
||||
}}
|
||||
/>
|
||||
<label class="fb-question__label">End (optional)</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="fb-input"
|
||||
value={isoToLocalInput(opt.end)}
|
||||
oninput={(e) => {
|
||||
const raw = (e.target as HTMLInputElement).value;
|
||||
if (!raw) setDateOption(i, optIdx, { end: null });
|
||||
else {
|
||||
const iso = localInputToIso(raw);
|
||||
if (iso) setDateOption(i, optIdx, { end: iso });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label class="fb-question__label">Label (optional)</label>
|
||||
<input
|
||||
class="fb-input"
|
||||
maxlength="200"
|
||||
placeholder="e.g. Office, 09:00 sharp"
|
||||
value={opt.label ?? ''}
|
||||
oninput={(e) => setDateOption(i, optIdx, { label: (e.target as HTMLInputElement).value })}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="fb-builder__icon-btn fb-builder__icon-btn--danger"
|
||||
disabled={q.options.length <= 2}
|
||||
onclick={() => removeDateOption(i, optIdx)}
|
||||
aria-label="Remove option"
|
||||
><Icon name="x" size={14} /></button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="fb-btn fb-btn--secondary fb-btn--sm fb-builder__add-option"
|
||||
onclick={() => addDateOption(i)}
|
||||
disabled={q.options.length >= 50}
|
||||
><Icon name="plus" /> Date option</button>
|
||||
</div>
|
||||
|
||||
<div class="fb-builder__scale">
|
||||
<div class="fb-question">
|
||||
<label class="fb-question__label">Rating-1 label (optional)</label>
|
||||
<input
|
||||
class="fb-input"
|
||||
maxlength="50"
|
||||
placeholder="e.g. doesn't work"
|
||||
value={q.scale?.min_label ?? ''}
|
||||
oninput={(e) => setScaleLabel(i, 'min_label', (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="fb-question">
|
||||
<label class="fb-question__label">Rating-5 label (optional)</label>
|
||||
<input
|
||||
class="fb-input"
|
||||
maxlength="50"
|
||||
placeholder="e.g. works great"
|
||||
value={q.scale?.max_label ?? ''}
|
||||
oninput={(e) => setScaleLabel(i, 'max_label', (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="fb-option-row" style="display:inline-flex;">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={q.allow_partial !== false}
|
||||
onchange={(e) => setAllowPartial(i, (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span>Allow participants to skip individual options</span>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<div class="fb-question">
|
||||
|
||||
@@ -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 @@
|
||||
<span class="fb-results__row-count">{q.stats.no} · {pct(q.stats.no, denom)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else if q.stats.type === 'date_ranked_choice'}
|
||||
<div class="fb-results__meta">{q.stats.count} {q.stats.count === 1 ? 'Antwort' : 'Antworten'}</div>
|
||||
<div class="fb-results__date-ranked">
|
||||
{#each q.stats.options as opt, optIdx (opt.id)}
|
||||
{@const denom = maxOf(opt.histogram.map((b) => b.count))}
|
||||
<div class="fb-results__date-opt">
|
||||
<div class="fb-results__date-head">
|
||||
<span class="fb-results__date-rank">#{optIdx + 1}</span>
|
||||
<div class="fb-results__date-when">
|
||||
<div>{fmtDateOption(opt.start, opt.end)}</div>
|
||||
{#if opt.label}<div class="fb-results__date-label">{opt.label}</div>{/if}
|
||||
</div>
|
||||
<div class="fb-results__date-mean">
|
||||
Schnitt: <strong>{fmtMean(opt.mean)}</strong>
|
||||
<span class="fb-results__date-count">· {opt.count} {opt.count === 1 ? 'Bewertung' : 'Bewertungen'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fb-results__bars">
|
||||
{#each opt.histogram as bucket (bucket.value)}
|
||||
<div class="fb-results__row">
|
||||
<span class="fb-results__row-label">{bucket.value}</span>
|
||||
<div class="fb-results__bar-track">
|
||||
<div class="fb-results__bar-fill" style="width: {pct(bucket.count, denom)}%"></div>
|
||||
</div>
|
||||
<span class="fb-results__row-count">{bucket.count}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if q.stats.type === 'short_text' || q.stats.type === 'long_text'}
|
||||
<div class="fb-results__meta">{q.stats.count} Antworten</div>
|
||||
{#if q.stats.answers.length > 0}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string, number | null> | 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<string, number | null> | undefined) ?? {};
|
||||
answers = { ...answers, [qid]: { ...cur, [optId]: rating } };
|
||||
}
|
||||
|
||||
function dateRankedRating(qid: string, optId: string): number | null {
|
||||
const cur = answers[qid] as Record<string, number | null> | 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 @@
|
||||
<span>Nein</span>
|
||||
</label>
|
||||
</div>
|
||||
{:else if q.type === 'date_ranked_choice'}
|
||||
<div class="fb-date-ranked">
|
||||
{#if q.scale?.min_label || q.scale?.max_label}
|
||||
<div class="fb-scale__labels" style="margin-bottom: 0.5rem;">
|
||||
<span>1 — {q.scale.min_label ?? 'passt nicht'}</span>
|
||||
<span>5 — {q.scale.max_label ?? 'passt super'}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each q.options as opt (opt.id)}
|
||||
<div class="fb-date-ranked__row">
|
||||
<div class="fb-date-ranked__opt">
|
||||
<div class="fb-date-ranked__when">{fmtDateOption(opt.start, opt.end)}</div>
|
||||
{#if opt.label}<div class="fb-date-ranked__label">{opt.label}</div>{/if}
|
||||
</div>
|
||||
<div class="fb-scale fb-date-ranked__scale" role="radiogroup" aria-label={opt.label ?? fmtDateOption(opt.start, opt.end)}>
|
||||
{#each [1, 2, 3, 4, 5] as v (v)}
|
||||
<button
|
||||
type="button"
|
||||
class="fb-scale__btn {dateRankedRating(q.id, opt.id) === v ? 'fb-scale__btn--active' : ''}"
|
||||
aria-pressed={dateRankedRating(q.id, opt.id) === v}
|
||||
onclick={() => setDateRankedRating(q.id, opt.id, v)}
|
||||
>
|
||||
{v}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
type="button"
|
||||
class="fb-date-ranked__skip {dateRankedRating(q.id, opt.id) === null ? 'fb-date-ranked__skip--active' : ''}"
|
||||
onclick={() => setDateRankedRating(q.id, opt.id, null)}
|
||||
aria-label="Skip option"
|
||||
>
|
||||
—
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if q.help}
|
||||
|
||||
Reference in New Issue
Block a user