feat: touch swipe for dating mode with live drag feedback

- swipe left/right to reject/keep, swipe up for favorite
- card tilts and overlays fade in proportionally during drag
- snap-back animation when swipe distance below threshold
- touchcancel handled identically to touchend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 12:44:15 +00:00
parent 1a2dd00bed
commit 433fd93a36
+76 -3
View File
@@ -131,7 +131,7 @@
.drop-zone { border: 2px dashed var(--border-mid); border-radius: 12px; padding: 2rem 1rem; text-align: center; transition: border-color 0.15s, background 0.15s; margin-bottom: 1rem; cursor: default; } .drop-zone { border: 2px dashed var(--border-mid); border-radius: 12px; padding: 2rem 1rem; text-align: center; transition: border-color 0.15s, background 0.15s; margin-bottom: 1rem; cursor: default; }
.drop-zone.drag-over { border-color: var(--blue); background: var(--blue-dim); } .drop-zone.drag-over { border-color: var(--blue); background: var(--blue-dim); }
.drop-zone.done { border-color: var(--green); background: var(--green-dim); } .drop-zone.done { border-color: var(--green); background: var(--green-dim); }
.drop-icon { font-size: 2.2rem; margin-bottom: 0.5rem; } .drop-icon { margin-bottom: 0.5rem; line-height: 1; }
.drop-text { font-size: 1rem; font-weight: 500; color: var(--text); margin-bottom: 0.25rem; } .drop-text { font-size: 1rem; font-weight: 500; color: var(--text); margin-bottom: 0.25rem; }
.drop-sub { font-size: 0.8rem; color: var(--faint); } .drop-sub { font-size: 0.8rem; color: var(--faint); }
.upload-status { margin-bottom: 1rem; } .upload-status { margin-bottom: 1rem; }
@@ -163,7 +163,8 @@
.tinder-counter { font-size: 0.92rem; color: var(--muted); font-weight: 500; } .tinder-counter { font-size: 0.92rem; color: var(--muted); font-weight: 500; }
.tinder-done-btn { padding: 0.3rem 0.8rem; border-radius: 6px; border: 1.5px solid var(--border); background: #fff; color: var(--muted); cursor: pointer; font-size: 0.82rem; transition: border-color 0.15s, color 0.15s; } .tinder-done-btn { padding: 0.3rem 0.8rem; border-radius: 6px; border: 1.5px solid var(--border); background: #fff; color: var(--muted); cursor: pointer; font-size: 0.82rem; transition: border-color 0.15s, color 0.15s; }
.tinder-done-btn:hover { border-color: var(--blue); color: var(--blue); } .tinder-done-btn:hover { border-color: var(--blue); color: var(--blue); }
.tinder-card { position: relative; border-radius: 16px; overflow: hidden; background: #fff; box-shadow: 0 4px 24px rgba(0,0,0,0.12); transform-origin: bottom center; } .tinder-card { position: relative; border-radius: 16px; overflow: hidden; background: #fff; box-shadow: 0 4px 24px rgba(0,0,0,0.12); transform-origin: bottom center; touch-action: none; user-select: none; }
.tinder-card.snapping { transition: transform 0.25s cubic-bezier(0.2,0,0.4,1); }
.tinder-card img { width: 100%; max-height: 62vh; object-fit: contain; background: #F8F9FB; display: block; } .tinder-card img { width: 100%; max-height: 62vh; object-fit: contain; background: #F8F9FB; display: block; }
.tinder-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 3rem; font-weight: 900; letter-spacing: 0.08em; opacity: 0; pointer-events: none; transition: opacity 0.12s; } .tinder-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 3rem; font-weight: 900; letter-spacing: 0.08em; opacity: 0; pointer-events: none; transition: opacity 0.12s; }
.tinder-overlay-ok { background: rgba(34,197,94,0.25); color: var(--green); } .tinder-overlay-ok { background: rgba(34,197,94,0.25); color: var(--green); }
@@ -277,7 +278,17 @@
<div id="view-start" class="view active card"> <div id="view-start" class="view active card">
<input type="file" id="folder-picker" webkitdirectory multiple accept="image/*" style="display:none"> <input type="file" id="folder-picker" webkitdirectory multiple accept="image/*" style="display:none">
<div id="drop-zone" class="drop-zone"> <div id="drop-zone" class="drop-zone">
<div class="drop-icon">&#128193;</div> <div class="drop-icon">
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4" width="44" height="44" rx="10" fill="var(--blue-dim)" stroke="var(--blue)" stroke-width="2"/>
<rect x="13" y="16" width="26" height="20" rx="4" fill="none" stroke="var(--blue)" stroke-width="2"/>
<line x1="19" y1="16" x2="19" y2="12" stroke="var(--blue)" stroke-width="2" stroke-linecap="round"/>
<line x1="19" y1="12" x2="28" y2="12" stroke="var(--blue)" stroke-width="2" stroke-linecap="round"/>
<line x1="28" y1="12" x2="28" y2="16" stroke="var(--blue)" stroke-width="2" stroke-linecap="round"/>
<line x1="26" y1="22" x2="26" y2="30" stroke="var(--blue)" stroke-width="2" stroke-linecap="round"/>
<line x1="22" y1="26" x2="30" y2="26" stroke="var(--blue)" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<p class="drop-text">Drag &amp; Drop</p> <p class="drop-text">Drag &amp; Drop</p>
<p style="font-size:0.8rem; color:var(--faint); margin:0.4rem 0;">oder</p> <p style="font-size:0.8rem; color:var(--faint); margin:0.4rem 0;">oder</p>
<button id="folder-picker-btn" style="padding:0.45rem 1.1rem; border-radius:8px; border:1.5px solid var(--border); background:#fff; color:var(--muted); font-size:0.85rem; font-weight:500; cursor:pointer; transition:border-color 0.15s, color 0.15s;">Ordner auswählen</button> <button id="folder-picker-btn" style="padding:0.45rem 1.1rem; border-radius:8px; border:1.5px solid var(--border); background:#fff; color:var(--muted); font-size:0.85rem; font-weight:500; cursor:pointer; transition:border-color 0.15s, color 0.15s;">Ordner auswählen</button>
@@ -1340,6 +1351,68 @@
el("tinder-btn-nok").addEventListener("click", () => tinderSwipe("left")); el("tinder-btn-nok").addEventListener("click", () => tinderSwipe("left"));
el("tinder-btn-ok").addEventListener("click", () => tinderSwipe("right")); el("tinder-btn-ok").addEventListener("click", () => tinderSwipe("right"));
// Touch swipe
const SWIPE_H = 80; // px horizontal threshold
const SWIPE_UP = 75; // px upward threshold (favorite)
let _tx0 = 0, _ty0 = 0, _touching = false;
function _resetOverlays() {
el("tinder-overlay-ok").style.opacity = "0";
el("tinder-overlay-nok").style.opacity = "0";
el("tinder-overlay-fav").style.opacity = "0";
}
el("tinder-card").addEventListener("touchstart", e => {
if (tinderSwiping) return;
const t = e.touches[0];
_tx0 = t.clientX; _ty0 = t.clientY;
_touching = true;
el("tinder-card").classList.remove("snapping");
}, { passive: true });
el("tinder-card").addEventListener("touchmove", e => {
if (!_touching || tinderSwiping) return;
e.preventDefault();
const t = e.touches[0];
const dx = t.clientX - _tx0;
const dy = t.clientY - _ty0;
const isUp = dy < -SWIPE_UP * 0.5 && Math.abs(dx) < 55;
const rot = isUp ? 0 : (dx / el("tinder-card").offsetWidth) * 18;
el("tinder-card").style.transform =
`translateX(${isUp ? 0 : dx}px) translateY(${isUp ? dy * 0.6 : 0}px) rotate(${rot}deg)`;
el("tinder-overlay-ok").style.opacity = (!isUp && dx > 30) ? Math.min(1, (dx - 30) / 70) + "" : "0";
el("tinder-overlay-nok").style.opacity = (!isUp && dx < -30) ? Math.min(1, (-dx - 30) / 70) + "" : "0";
el("tinder-overlay-fav").style.opacity = isUp ? Math.min(1, (-dy - 37) / 50) + "" : "0";
}, { passive: false });
function _onTouchEnd(e) {
if (!_touching) return;
_touching = false;
const t = e.changedTouches[0];
const dx = t.clientX - _tx0;
const dy = t.clientY - _ty0;
el("tinder-card").style.transform = "";
_resetOverlays();
if (dy < -SWIPE_UP && Math.abs(dx) < 55) {
tinderFavorite();
} else if (dx > SWIPE_H) {
tinderSwipe("right");
} else if (dx < -SWIPE_H) {
tinderSwipe("left");
} else {
const card = el("tinder-card");
card.classList.add("snapping");
card.addEventListener("transitionend", () => card.classList.remove("snapping"), { once: true });
}
}
el("tinder-card").addEventListener("touchend", _onTouchEnd);
el("tinder-card").addEventListener("touchcancel", _onTouchEnd);
el("tinder-fav-star").addEventListener("click", () => { el("tinder-fav-star").addEventListener("click", () => {
const photo = tinderQueue[tinderIndex]; const photo = tinderQueue[tinderIndex];
if (!photo) return; if (!photo) return;