Replace desktop card drag with a mobile-only swipe hint in the coverflow/strip

The mouse drag-to-scroll on the card row felt off in combination with the
coverflow's 3D transform (client feedback), so it's removed - desktop users
now navigate via the custom scrollbar underneath, which was already there.

Touch devices keep native scroll-snap swiping, but on first load it wasn't
obvious the row was swipeable. Adds a one-time hint: the row glides right by
~40% of a card width and back, 500ms after mount. Touch-only (pointer:
coarse), skipped under prefers-reduced-motion, plays once per page load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Maaxxs 2026-07-18 18:48:31 +02:00
parent ad9d99a458
commit 9de1059042

View file

@ -268,6 +268,7 @@ export default function ModelPage() {
const scrollerRef = useRef(null); const scrollerRef = useRef(null);
const trackRef = useRef(null); const trackRef = useRef(null);
const thumbRef = useRef(null); const thumbRef = useRef(null);
const hintPlayedRef = useRef(false);
const formatPrice = (value) => const formatPrice = (value) =>
new Intl.NumberFormat(language === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }).format(value); new Intl.NumberFormat(language === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: 'EUR' }).format(value);
@ -312,92 +313,28 @@ export default function ModelPage() {
}; };
}, [layout, data]); }, [layout, data]);
// Coverflow-Kacheln mit der Maus greifen und ziehen. Nur fuer echte Maus-Pointer - // Kein Karten-Drag per Maus mehr (Kundenfeedback: fuehlte sich in Kombination mit der
// Touch behaelt das native Wisch-Scrolling mit Momentum. Waehrend des Ziehens wird // 3D-Transformation im Coverflow nicht gut an). Auf dem Desktop navigiert man
// das Scroll-Snapping ausgesetzt (es wuerde gegen scrollLeft ankaempfen); beim // stattdessen ueber die eigene Scroll-Leiste unten; Touch-Geraete behalten ohnehin
// Loslassen faehrt die Reihe weich zur naechstgelegenen Karte. Wer gezogen hat, // das native, fluessige Wisch-Scrolling der scroll-snap-Kartenreihe.
// soll beim Loslassen ueber einem Button nichts ausloesen - der Capture-Click- //
// Handler schluckt genau diesen einen Klick. // Stattdessen: einmaliger kurzer Wisch-Hinweis beim ersten Laden auf Touch-Geraeten
// (Kundenwunsch) - die Reihe faehrt kurz ein Stueck nach rechts und wieder zurueck,
// damit sofort klar ist, dass sich die Kacheln wischen lassen. Nur einmal pro
// Seitenaufruf, nur auf echten Touch-Geraeten, nicht bei reduzierter Bewegung.
useEffect(() => { useEffect(() => {
const el = scrollerRef.current; const el = scrollerRef.current;
if (layout !== 'coverflow' || !el) return undefined; if (layout === 'grid' || !el || hintPlayedRef.current) return undefined;
let dragging = false; const isTouch = window.matchMedia('(pointer: coarse)').matches;
let moved = 0; const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let startX = 0; if (!isTouch || reducedMotion) return undefined;
let startScroll = 0; hintPlayedRef.current = true;
const peek = Math.min(el.clientWidth * 0.4, 160);
const snapToNearest = () => { const t1 = setTimeout(() => el.scrollTo({ left: peek, behavior: 'smooth' }), 500);
const mid = el.scrollLeft + el.clientWidth / 2; const t2 = setTimeout(() => el.scrollTo({ left: 0, behavior: 'smooth' }), 1250);
let best = null;
let bestDist = Infinity;
for (const card of el.children) {
const center = card.offsetLeft + card.offsetWidth / 2;
const dist = Math.abs(center - mid);
if (dist < bestDist) {
bestDist = dist;
best = center;
}
}
if (best !== null) el.scrollTo({ left: best - el.clientWidth / 2, behavior: 'smooth' });
};
// WICHTIG: Pointer-Capture erst ab echter Bewegung (>4px) starten - sofortiges
// Capture beim Druecken wuerde auch das Click-Event auf den Container umlenken,
// womit normale Klicks auf Buttons/Links in den Karten nie ankaemen.
let pending = false;
const onDown = (e) => {
if (e.pointerType !== 'mouse' || e.button !== 0) return;
pending = true;
dragging = false;
moved = 0;
startX = e.clientX;
startScroll = el.scrollLeft;
};
const onMove = (e) => {
if (!pending && !dragging) return;
const dx = e.clientX - startX;
moved = Math.max(moved, Math.abs(dx));
if (!dragging) {
if (moved <= 4) return;
dragging = true;
pending = false;
el.setPointerCapture(e.pointerId);
el.style.scrollSnapType = 'none';
el.dataset.dragging = 'true';
}
el.scrollLeft = startScroll - dx;
};
const onUp = (e) => {
pending = false;
if (!dragging) return;
dragging = false;
el.dataset.dragging = 'false';
if (el.hasPointerCapture?.(e.pointerId)) el.releasePointerCapture(e.pointerId);
snapToNearest();
// Snap erst nach der weichen Fahrt wieder aktivieren, sonst springt der Browser hart.
setTimeout(() => {
el.style.scrollSnapType = '';
}, 450);
};
const onClickCapture = (e) => {
if (moved > 6) {
e.preventDefault();
e.stopPropagation();
moved = 0;
}
};
el.addEventListener('pointerdown', onDown);
el.addEventListener('pointermove', onMove);
el.addEventListener('pointerup', onUp);
el.addEventListener('pointercancel', onUp);
el.addEventListener('click', onClickCapture, true);
return () => { return () => {
el.removeEventListener('pointerdown', onDown); clearTimeout(t1);
el.removeEventListener('pointermove', onMove); clearTimeout(t2);
el.removeEventListener('pointerup', onUp);
el.removeEventListener('pointercancel', onUp);
el.removeEventListener('click', onClickCapture, true);
}; };
}, [layout, data]); }, [layout, data]);