// iterations.jsx — design iterations for A/B testing
// · FormBlockMetroA — flat metro line, every field is a stop
// · FormBlockMetroB — grouped "stations", line connects sections
// · FAQMetro — accordion on a metro rail
// · FAQCategorized — open two-column grid grouped by topic
//
// Globals consumed: React, I (icons), BlockMeta (from blocks.jsx).
// All forms use REAL controlled inputs so the metro line lights up
// vertically as the visitor fills fields / opens questions.
const { useState, useRef, useLayoutEffect, useEffect } = React;
// ─────────────────────────────────────────────────────────────
// Shared rail hook — measures dot centers and returns the
// geometry needed to draw the track + scroll-style fill line.
// `activeIndex` = deepest lit node; fill runs from node 0 to it.
// ─────────────────────────────────────────────────────────────
function useRail(activeIndex, deps) {
const railRef = useRef(null);
const dotRefs = useRef([]);
const [centers, setCenters] = useState([]);
useLayoutEffect(() => {
const measure = () => {
const base = railRef.current ? railRef.current.getBoundingClientRect().top : 0;
const cs = dotRefs.current.map(d =>
d ? (d.getBoundingClientRect().top - base + d.offsetHeight / 2) : null);
setCenters(cs);
};
measure();
const r = () => measure();
window.addEventListener('resize', r);
const t = setTimeout(measure, 60); // after fonts settle
return () => { window.removeEventListener('resize', r); clearTimeout(t); };
}, deps); // eslint-disable-line
const valid = centers.filter(c => c != null);
const top = valid.length ? valid[0] : 0;
const bottom = valid.length ? valid[valid.length - 1] : 0;
const fill = (activeIndex >= 0 && centers[activeIndex] != null)
? centers[activeIndex] - top : 0;
return { railRef, dotRefs, top, height: bottom - top, fill };
}
function deepestActive(flags) {
let idx = -1;
flags.forEach((f, i) => { if (f) idx = i; });
return idx;
}
// ─────────────────────────────────────────────────────────────
// Small field atoms (controlled)
// ─────────────────────────────────────────────────────────────
function Req() { return * ; }
function TextField({ label, type = 'text', placeholder, value, onChange, onFocus, onBlur, required }) {
return (
{label}{required && }
onChange(e.target.value)}
onFocus={onFocus} onBlur={onBlur} />
);
}
function SelectField({ label, value, onChange, onFocus, onBlur, required, options }) {
return (
{label}{required && }
onChange(e.target.value)}
onFocus={onFocus} onBlur={onBlur}
style={{ color: value ? 'var(--gray-900)' : 'var(--gray-500)' }}>
Bitte auswählen
{options.map(o => {o} )}
);
}
function TextareaField({ label, placeholder, value, onChange, onFocus, onBlur, required }) {
return (
{label}{required && }
);
}
const ANLASS_OPTS = ['Schulausflug', 'Vereinsreise', 'Firmenanlass', 'Hochzeit', 'Andere'];
// Shared intro column (matches the original FormBlock left rail)
function FormIntro({ eyebrow, title, body }) {
const contact = [
{ icon: , label: 'Telefon', value: '+41 41 874 78 78' },
{ icon: , label: 'E-Mail', value: 'gruppen@aagu.ch' },
{ icon: , label: 'Adresse', value: 'Ried 1, 6467 Schattdorf' },
];
return (
{eyebrow}
{title}
{body}
{contact.map((c, i) => (
))}
);
}
// ═════════════════════════════════════════════════════════════
// ITERATION A — flat metro line, every field is its own stop.
// The orange line draws down from stop 01 to the deepest field
// you've completed or are editing, exactly like the Stops block.
// ═════════════════════════════════════════════════════════════
function FormBlockMetroA({
eyebrow = "Kontakt",
title = "Gruppenreise anfragen",
body = "Jedes Feld ist eine Station. Die Linie zeichnet sich nach, während Sie das Formular Halt für Halt ausfüllen.",
showMeta = true,
}) {
// pre-fill the first two stops so the rail shows a partial fill at rest
const [v, setV] = useState({
vorname: 'Maria', nachname: 'Gisler', email: 'maria.gisler@email.ch',
tel: '', anlass: '', personen: '', datum: '', bemerkungen: '',
});
const [consent, setConsent] = useState(false);
const [focus, setFocus] = useState(null);
const set = k => val => setV(s => ({ ...s, [k]: val }));
const requiredOk = v.vorname && v.nachname && v.email && v.anlass && v.personen && v.datum && consent;
// node completeness (focus also lights a node)
const nodes = [
{ key: 'name', done: !!(v.vorname && v.nachname) },
{ key: 'email', done: !!v.email },
{ key: 'tel', done: !!v.tel },
{ key: 'anlass', done: !!v.anlass },
{ key: 'personen', done: !!v.personen },
{ key: 'datum', done: !!v.datum },
{ key: 'bemerkungen', done: !!v.bemerkungen },
{ key: 'send', done: !!requiredOk, terminal: true },
];
const flags = nodes.map(n => n.done || focus === n.key);
const activeIndex = deepestActive(flags);
const { railRef, dotRefs, top, height, fill } = useRail(activeIndex, [v, focus, consent]);
const GUT = 30, CX = 15; // gutter width / line x
const Dot = ({ i, n }) => {
const lit = n.done || focus === n.key;
return (
dotRefs.current[i] = el}
style={{
position: 'relative', zIndex: 1, width: n.terminal ? 30 : 26, height: n.terminal ? 30 : 26,
borderRadius: '50%', background: '#fff',
border: `${n.terminal ? 4 : 4}px solid ${lit ? 'var(--brand-orange)' : '#C9CFD7'}`,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
color: lit ? 'var(--brand-orange)' : 'var(--gray-400)',
boxShadow: focus === n.key ? '0 0 0 4px rgba(241,142,0,0.16)' : 'none',
transition: 'border-color .25s ease, box-shadow .2s ease', fontSize: 11, fontWeight: 800,
}}>
{n.terminal
?
: (n.done ?
: {String(i + 1).padStart(2, '0')} )}
);
};
const Row = ({ i, children }) => (
);
const fp = k => ({
value: v[k], onChange: set(k),
onFocus: () => setFocus(k), onBlur: () => setFocus(null),
});
return (
);
}
// ═════════════════════════════════════════════════════════════
// ITERATION B — grouped "stations". Three big numbered stops on
// the line, each gathering a set of fields. A stop turns to a
// check (and the segment to it fills) once its required fields
// are complete — a guided multi-step feel on a single page.
// ═════════════════════════════════════════════════════════════
function FormBlockMetroB({
eyebrow = "Kontakt",
title = "Gruppenreise anfragen",
body = "Drei Halte bis zur fertigen Anfrage. Die Linie rückt vor, sobald ein Abschnitt vollständig ist.",
showMeta = true,
}) {
const [v, setV] = useState({
vorname: 'Maria', nachname: 'Gisler', email: 'maria.gisler@email.ch', tel: '+41 79 123 45 67',
anlass: '', personen: '', datum: '', bemerkungen: '',
});
const [consent, setConsent] = useState(false);
const [focusGroup, setFocusGroup] = useState(null);
const set = k => val => setV(s => ({ ...s, [k]: val }));
const fp = (k, g) => ({
value: v[k], onChange: set(k),
onFocus: () => setFocusGroup(g), onBlur: () => setFocusGroup(null),
});
const gDone = [
!!(v.vorname && v.nachname && v.email),
!!(v.anlass && v.personen && v.datum),
!!consent,
];
const sendDone = gDone.every(Boolean);
const groups = [
{ key: 0, n: 'Kontakt', sub: 'Wie wir Sie erreichen' },
{ key: 1, n: 'Anlass', sub: 'Worum geht die Reise' },
{ key: 2, n: 'Ihre Wünsche', sub: 'Details & Einverständnis' },
];
const flags = [...gDone.map((d, i) => d || focusGroup === i), sendDone];
const activeIndex = deepestActive(flags);
const { railRef, dotRefs, top, height, fill } = useRail(activeIndex, [v, focusGroup, consent]);
const CX = 24; // disc center
const Disc = ({ i, label, terminal }) => {
const lit = flags[i];
const done = i < 3 ? gDone[i] : sendDone;
return (
dotRefs.current[i] = el}
style={{
position: 'relative', zIndex: 1, width: 48, height: 48, borderRadius: '50%',
background: lit ? 'var(--brand-orange)' : '#fff',
border: `3px solid ${lit ? 'var(--brand-orange)' : '#C9CFD7'}`,
color: lit ? '#fff' : 'var(--gray-400)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-display)', fontWeight: 800, fontSize: 17,
boxShadow: focusGroup === i ? '0 0 0 5px rgba(241,142,0,0.16)' : 'none',
transition: 'background .25s ease, border-color .25s ease, box-shadow .2s ease',
}}>
{terminal ? : (done ? : label)}
);
};
const Section = ({ i, terminal, children }) => (
);
const card = { background: '#fff', border: '1px solid var(--line)', borderRadius: 16, padding: 22, display: 'flex', flexDirection: 'column', gap: 14 };
const head = (g) => (
);
return (
);
}
// ═════════════════════════════════════════════════════════════
// FAQ DEFAULT CONTENT (shared by both iterations)
// ═════════════════════════════════════════════════════════════
const FAQ_ITEMS = [
{ cat: 'Tickets & Tarife', q: 'Wo kann ich Billette kaufen?', a: 'Billette gibt es online, in unserer App, an den Verkaufsstellen sowie direkt im Bus beim Chauffeur. Wir akzeptieren bar, Karte und TWINT.' },
{ cat: 'Tickets & Tarife', q: 'Wie funktioniert das Tarifsystem?', a: 'Der Kanton Uri ist Teil des Tarifverbunds. Tickets sind zonenbasiert und gelten auf allen Linien innerhalb der gewählten Zonen.' },
{ cat: 'An Bord', q: 'Sind Hunde im Bus erlaubt?', a: 'Ja, Hunde sind in unseren Bussen herzlich willkommen. Kleine Hunde reisen kostenlos, für grössere Hunde gilt der halbe Tarif.' },
{ cat: 'An Bord', q: 'Kann ich mein Velo mitnehmen?', a: 'Auf ausgewählten Linien transportieren wir Velos nach Platzangebot. Faltvelos sind immer als Handgepäck dabei.' },
{ cat: 'Gruppen', q: 'Gibt es Rabatte für Schulen oder Gruppen?', a: 'Für Schulklassen und Gruppen ab 10 Personen bieten wir attraktive Konditionen. Kontaktieren Sie uns unter info@aagu.ch.' },
{ cat: 'Gruppen', q: 'Wie weit im Voraus muss ich buchen?', a: 'Gruppenanfragen nehmen wir bis spätestens zwei Werktage vor der Reise entgegen — je früher, desto besser planbar.' },
];
const CAT_COLORS = { 'Tickets & Tarife': '#00427D', 'An Bord': '#7BB028', 'Gruppen': '#F18E00' };
// ═════════════════════════════════════════════════════════════
// FAQ ITERATION A — accordion on a metro rail.
// The line fills down to the currently open question.
// ═════════════════════════════════════════════════════════════
function FAQMetro({ title = "Häufige Fragen", eyebrow = "FAQ", showMeta = true, items = FAQ_ITEMS }) {
const [open, setOpen] = useState(1);
const { railRef, dotRefs, top, height, fill } = useRail(open, [open]);
const CX = 19;
return (
{showMeta &&
}
{items.map((it, i) => {
const isOpen = open === i;
const lit = i <= open;
return (
dotRefs.current[i] = el}
style={{
position: 'relative', zIndex: 1, width: 30, height: 30, borderRadius: '50%', background: '#fff',
border: `4px solid ${lit ? 'var(--brand-orange)' : '#C9CFD7'}`,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
color: lit ? 'var(--brand-orange)' : 'var(--gray-400)', fontSize: 12, fontWeight: 800,
transition: 'border-color .25s ease',
}}>{String(i + 1).padStart(2, '0')}
setOpen(isOpen ? -1 : i)}
style={{
width: '100%', textAlign: 'left', border: 0, cursor: 'pointer',
padding: '18px 22px', display: 'flex', alignItems: 'center', gap: 16,
background: isOpen ? 'var(--brand-orange-tint)' : '#fff',
}}>
{it.q}
{isOpen ? : }
{isOpen &&
{it.a}
}
);
})}
);
}
// ═════════════════════════════════════════════════════════════
// FAQ ITERATION B — open two-column grid grouped by topic.
// No accordion: every answer visible, scannable, SEO-friendly.
// Category chips use the official line colors.
// ═════════════════════════════════════════════════════════════
function FAQCategorized({ title = "Häufige Fragen", eyebrow = "FAQ", showMeta = true, items = FAQ_ITEMS }) {
const cats = [];
items.forEach(it => {
let g = cats.find(c => c.name === it.cat);
if (!g) { g = { name: it.cat, items: [] }; cats.push(g); }
g.items.push(it);
});
return (
{showMeta &&
}
{cats.map(cat => {
const color = CAT_COLORS[cat.name] || 'var(--brand-orange)';
return (
{cat.name}
{cat.items.map((it, i) => (
· {it.q}
{it.a}
))}
);
})}
);
}
Object.assign(window, {
FormBlockMetroA, FormBlockMetroB, FAQMetro, FAQCategorized,
});