430 lines
26 KiB
HTML
430 lines
26 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<meta name="robots" content="noindex">
|
|
<title>Bike-Tinder · Mockup A — Pure</title>
|
|
<style>
|
|
:root{
|
|
--bg:#0b0b0e; --card:#141419; --text:#fff; --mut:rgba(255,255,255,.62);
|
|
--accent1:#ff7854; --accent2:#fd267d; --like:#46d369; --nope:#fd4d5c; --maybe:#ffb43a;
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
|
|
html,body{height:100%}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
background:var(--bg);color:var(--text);overflow:hidden;-webkit-font-smoothing:antialiased}
|
|
.app{position:relative;display:flex;flex-direction:column;height:100%}
|
|
/* header */
|
|
header{height:50px;display:flex;align-items:center;justify-content:space-between;padding:0 16px}
|
|
.brand{display:flex;align-items:center;gap:8px;font-weight:800;font-size:16px;letter-spacing:.2px}
|
|
.flame{width:18px;height:18px;border-radius:6px;background:linear-gradient(135deg,var(--accent1),var(--accent2))}
|
|
.pill{font-size:12px;color:var(--mut);background:#1b1b21;border:1px solid #26262e;border-radius:999px;padding:5px 10px}
|
|
.pill b{color:#fff}
|
|
/* stage + cards */
|
|
.stage{position:relative;flex:1;padding:6px 14px 2px;min-height:0}
|
|
.card{position:absolute;left:14px;right:14px;top:6px;bottom:34px;border-radius:18px;overflow:hidden;background:var(--card);
|
|
box-shadow:0 0 0 1px rgba(255,255,255,.07), 0 18px 40px rgba(0,0,0,.5);will-change:transform;touch-action:none}
|
|
.nophoto{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:repeating-linear-gradient(45deg,#141419 0 14px,#18181f 14px 28px);color:#55555f;font-size:13px;text-align:center;padding:24px;line-height:1.6}
|
|
.card .photo{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
.card .grad{position:absolute;left:0;right:0;bottom:0;height:52%;background:linear-gradient(180deg,rgba(0,0,0,0) 0%,rgba(0,0,0,.55) 55%,rgba(0,0,0,.88) 100%)}
|
|
.card .info{position:absolute;left:16px;right:16px;bottom:14px;z-index:3}
|
|
.chips{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap}
|
|
.chip{font-size:11px;font-weight:600;letter-spacing:.4px;padding:4px 9px;border-radius:999px;background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.22)}
|
|
.title{font-size:23px;font-weight:800;line-height:1.15;text-shadow:0 1px 8px rgba(0,0,0,.5);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
.price{font-size:20px;font-weight:600;margin-top:2px}
|
|
.sub{font-size:13px;color:var(--mut);margin-top:3px}
|
|
.stamp{position:absolute;top:26px;z-index:4;font-weight:800;letter-spacing:2px;font-size:26px;padding:6px 12px;border:3px solid;border-radius:10px;opacity:0;pointer-events:none}
|
|
.stamp.like{left:20px;transform:rotate(-14deg);color:var(--like);border-color:var(--like);background:rgba(70,211,105,.08)}
|
|
.stamp.nope{right:20px;transform:rotate(14deg);color:var(--nope);border-color:var(--nope);background:rgba(253,77,92,.08)}
|
|
.stamp.maybe{left:50%;top:auto;bottom:130px;transform:translateX(-50%);color:var(--maybe);border-color:var(--maybe);background:rgba(255,180,58,.08)}
|
|
/* actions */
|
|
.actions{height:86px;display:flex;align-items:center;justify-content:center;gap:16px}
|
|
.act{border:0;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .12s}
|
|
.act:active{transform:scale(.92)}
|
|
.undo{width:42px;height:42px;background:#1d1d24;color:#f5c451;font-size:18px}
|
|
.x{width:58px;height:58px;background:#fff;color:#fd5068;font-size:26px;font-weight:700;box-shadow:0 6px 18px rgba(0,0,0,.4)}
|
|
.s{width:58px;height:58px;background:#1d1d24;color:var(--maybe);font-size:24px;border:1px solid #2a2a33}
|
|
.h{width:62px;height:62px;background:linear-gradient(135deg,var(--accent1),var(--accent2));color:#fff;font-size:26px;box-shadow:0 8px 22px rgba(253,38,125,.35)}
|
|
/* tabs */
|
|
nav{height:68px;display:flex;align-items:stretch;border-top:1px solid #1d1d24;background:#0d0d10;padding-bottom:6px;position:relative;z-index:60}
|
|
.tab{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;font-size:10px;letter-spacing:.3px;color:#6f6f7a;cursor:pointer}
|
|
.tab.on{color:#fff}
|
|
.tab svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
/* fullscreen detail dossier — iOS-style sheet: top gap + grabber, drag down to close */
|
|
.dback{position:absolute;inset:0;z-index:69;background:rgba(0,0,0,.38);opacity:0;pointer-events:none;transition:opacity .28s}
|
|
.dback.on{opacity:1}
|
|
.detail{position:absolute;top:26px;left:0;right:0;bottom:0;z-index:70;background:#0f0f13;border-radius:18px 18px 0 0;box-shadow:0 -14px 40px rgba(0,0,0,.45);transform:translateY(103%);transition:transform .3s cubic-bezier(.2,.8,.3,1);display:flex;flex-direction:column;overflow:hidden}
|
|
.detail.on{transform:translateY(0)}
|
|
.dgrab{flex:none;height:26px;display:flex;align-items:center;justify-content:center;background:#0f0f13;cursor:grab;touch-action:none}
|
|
.dgrab i{width:42px;height:5px;border-radius:3px;background:rgba(255,255,255,.3);display:block}
|
|
.dclose{position:absolute;top:38px;left:14px;width:36px;height:36px;border-radius:50%;background:rgba(0,0,0,.55);color:#fff;border:0;font-size:17px;z-index:5;cursor:pointer}
|
|
.dhero{position:relative;height:34%;min-height:190px;flex:none;background:#17171c;touch-action:none}
|
|
.dphoto{width:100%;height:100%;object-fit:cover;display:block}
|
|
.dphoto.ph{opacity:.25}
|
|
.dherograd{position:absolute;left:0;right:0;bottom:0;height:70px;background:linear-gradient(transparent,rgba(15,15,19,.95))}
|
|
.dbody{flex:1;overflow-y:auto;padding:14px 18px 20px}
|
|
.dtitle{font-size:20px;font-weight:800;line-height:1.2}
|
|
.drow{display:flex;align-items:baseline;gap:10px;margin-top:5px}
|
|
.dprice{font-size:19px;font-weight:700}
|
|
.dsub{font-size:12.5px;color:var(--mut)}
|
|
.dchips{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
|
.dsec{font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:#7b7b88;margin:18px 0 8px}
|
|
.specs{border-top:1px solid #1f1f27}
|
|
.spec{display:flex;justify-content:space-between;gap:14px;padding:9px 2px;border-bottom:1px solid #1f1f27;font-size:13.5px}
|
|
.spec .k{color:#8f8f9c}
|
|
.spec .v{font-weight:600;text-align:right}
|
|
.spec .v.z{color:#5a5a66;font-weight:500}
|
|
.dnotes{font-size:13.5px;line-height:1.5;color:#b9b9c2}
|
|
.dopen{display:inline-block;margin-top:16px;color:#ff9a76;font-weight:600;font-size:14px;text-decoration:none}
|
|
.dacts{flex:none;display:flex;gap:10px;padding:12px 16px calc(14px + env(safe-area-inset-bottom, 0px));border-top:1px solid #1d1d24;background:#0d0d10}
|
|
.dact{flex:1;height:48px;border-radius:14px;border:1px solid #2a2a33;background:#16161b;color:#fff;font-size:14px;font-weight:700;cursor:pointer}
|
|
.dact:active{transform:scale(.97)}
|
|
.dact.no{color:#fd7b89}
|
|
.dact.mb{color:var(--maybe)}
|
|
.dact.lk{background:linear-gradient(135deg,var(--accent1),var(--accent2));border:0;box-shadow:0 8px 22px rgba(253,38,125,.3)}
|
|
.ibtn{position:absolute;top:14px;right:14px;width:32px;height:32px;border-radius:50%;background:rgba(0,0,0,.45);border:1px solid rgba(255,255,255,.25);color:#fff;font-size:14px;z-index:4;cursor:pointer}
|
|
/* empty */
|
|
.empty{position:absolute;inset:6px 14px 2px;display:none;flex-direction:column;align-items:center;justify-content:center;gap:8px;border:1px dashed #2a2a33;border-radius:18px;color:var(--mut);text-align:center;padding:24px;font-size:13.5px}
|
|
.empty.on{display:flex}
|
|
.empty b{color:#fff;font-size:16px}
|
|
/* list screen */
|
|
.list{position:absolute;inset:0;display:none;flex-direction:column;background:var(--bg);z-index:50;padding-bottom:68px}
|
|
.list.on{display:flex}
|
|
.lhead{padding:14px 16px 8px}
|
|
.lhead h1{font-size:20px;font-weight:800}
|
|
.lhead .sub{margin-top:2px}
|
|
.filters{display:flex;gap:7px;padding:4px 16px 10px;overflow-x:auto}
|
|
.fchip{font-size:12px;padding:6px 11px;border-radius:999px;background:#1b1b21;border:1px solid #26262e;color:var(--mut);white-space:nowrap;cursor:pointer}
|
|
.fchip.on{background:#fff;color:#111;border-color:#fff;font-weight:600}
|
|
.lrows{flex:1;overflow-y:auto;padding:0 10px 12px}
|
|
.row{display:flex;gap:11px;align-items:center;padding:9px 8px;border-radius:14px}
|
|
.row + .row{border-top:1px solid #1a1a1f}
|
|
.thumb{width:56px;height:56px;border-radius:12px;object-fit:cover;background:#1a1a20;flex:none}
|
|
.thumb.ph{display:flex;align-items:center;justify-content:center;color:#4a4a55;font-size:20px}
|
|
.rmain{flex:1;min-width:0}
|
|
.rtitle{font-size:14.5px;font-weight:650;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.rsub{font-size:12px;color:var(--mut);margin-top:2px}
|
|
.rright{display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex:none}
|
|
.rprice{font-size:14px;font-weight:700}
|
|
.rc{display:flex;gap:5px}
|
|
.rbtn{width:26px;height:26px;border-radius:50%;border:1px solid #2a2a33;background:#16161b;color:#8a8a95;font-size:12px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
|
|
.rbtn.on.like{background:rgba(70,211,105,.15);border-color:var(--like);color:var(--like)}
|
|
.rbtn.on.maybe{background:rgba(255,180,58,.15);border-color:var(--maybe);color:var(--maybe)}
|
|
.rbtn.on.nope{background:rgba(253,77,92,.15);border-color:var(--nope);color:var(--nope)}
|
|
/* stub screens */
|
|
.stub{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;gap:10px;background:var(--bg);z-index:50;color:var(--mut);text-align:center;padding:40px;font-size:13.5px}
|
|
.stub.on{display:flex}
|
|
.stub b{color:#fff;font-size:17px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app">
|
|
<header>
|
|
<div class="brand"><span class="flame"></span>Bike-Tinder</div>
|
|
<div class="pill"><b>560</b> live · <b>3</b> new</div>
|
|
</header>
|
|
|
|
<div class="stage" id="stage">
|
|
<div class="empty" id="empty"><b>You're all caught up.</b>New bikes arrive nightly — next import 08:30.<br>↺ brings the last card back.</div>
|
|
</div>
|
|
|
|
<div class="actions" id="actions">
|
|
<button class="act undo" id="btnUndo" title="Undo">↺</button>
|
|
<button class="act x" id="btnNope" title="Nope">✕</button>
|
|
<button class="act s" id="btnMaybe" title="Maybe">★</button>
|
|
<button class="act h" id="btnLike" title="Like">♥</button>
|
|
</div>
|
|
|
|
<nav>
|
|
<div class="tab on" data-tab="deck"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="13" height="16" rx="2.5"/><path d="M8.5 2.5h9A2.5 2.5 0 0 1 20 5v12"/></svg>Deck</div>
|
|
<div class="tab" data-tab="list"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>List</div>
|
|
<div class="tab" data-tab="matches"><svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>Matches</div>
|
|
<div class="tab" data-tab="trends"><svg viewBox="0 0 24 24"><path d="M5 20V10M12 20V4M19 20v-7"/></svg>Trends</div>
|
|
</nav>
|
|
|
|
<div class="dback" id="dback"></div>
|
|
<div class="detail" id="detail">
|
|
<div class="dgrab" id="dgrab"><i></i></div>
|
|
<button class="dclose" id="dclose">⌄</button>
|
|
<div class="dhero"><img class="dphoto" id="dphoto" alt=""><div class="dherograd"></div></div>
|
|
<div class="dbody" id="dbody">
|
|
<div class="dtitle" id="dtitle"></div>
|
|
<div class="drow"><div class="dprice" id="dprice"></div><div class="dsub" id="dsub"></div></div>
|
|
<div class="dchips" id="dchips"></div>
|
|
<div class="dsec">The machine</div>
|
|
<div class="specs" id="dspecs"></div>
|
|
<div class="dsec">Seller's notes</div>
|
|
<div class="dnotes" id="dnotes"></div>
|
|
<a class="dopen" id="dopen" href="#" target="_blank" rel="noopener">Open on Kijiji ↗</a>
|
|
</div>
|
|
<div class="dacts">
|
|
<button class="dact no" id="dNo">✕ Nope</button>
|
|
<button class="dact mb" id="dMb">★ Maybe</button>
|
|
<button class="dact lk" id="dLk">♥ Like</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="list" id="listScreen">
|
|
<div class="lhead"><h1>All bikes</h1><div class="sub">560 active · <b style="color:#fff">3 new</b> since your last visit</div></div>
|
|
<div class="filters">
|
|
<div class="fchip on">Score</div><div class="fchip">Price ↕</div><div class="fchip">Newest</div><div class="fchip">km</div><div class="fchip">Filters ⚙</div>
|
|
</div>
|
|
<div class="lrows" id="lrows"></div>
|
|
</div>
|
|
|
|
<div class="stub" id="stubMatches"><b>Your shortlist lives here</b>Liked and Maybe bikes — once the build lands.</div>
|
|
<div class="stub" id="stubTrends"><b>Trends — Phase 3</b>The deck learns your taste as you swipe.</div>
|
|
</div>
|
|
|
|
<script>
|
|
const CARDS=[
|
|
{title:"2025 Indian Scout Classic Limited",price:"$21,099",sub:"Moncton, NB · dealer",chips:["Dealer","Cruise control"],
|
|
year:"2025",km:"—",cc:"1,250 cc",family:"Indian Scout",style:"Street cruiser",loc:"Moncton, NB",seller:"Dealer",seen:"Today",
|
|
img:"https://media.kijiji.ca/api/v1/ca-prod-dealer-ads/images/b0/b01e9e9b-63d6-4cf6-bd58-ef7f70ef8df2?rule=kijijica-960-webp",
|
|
desc:"Welcome to Rallye Motoplex & Marine. Save $1,102 today with rates as low as 3.99%. Call today and ask about our in-store incentives.",
|
|
url:"https://www.kijiji.ca/v-street-cruisers-choppers/moncton/2025-indian-scout-classic-limited-tech-ghost-white-metallic/1729602020"},
|
|
{title:"2013 Kawasaki Vulcan 900 Classic",price:"$4,800",sub:"Fredericton, NB · dealer",chips:[],
|
|
year:"2013",km:"24,800 km",cc:"903 cc",family:"Vulcan 900",style:"Street cruiser",loc:"Fredericton, NB",seller:"Dealer",seen:"6 days ago",
|
|
img:"https://media.kijiji.ca/api/v1/ca-prod-dealer-ads/images/88/882c012d-8682-4616-882b-03bfc4c880eb?rule=kijijica-960-webp",
|
|
desc:"MULTI-POINT INSPECTED & RECONDITIONED. Financing + extended warranty options available. No freight or PDI on used bikes.",
|
|
url:"https://www.kijiji.ca/v-street-cruisers-choppers/fredericton/2013-kawasaki-vulcan-900-classic-used/1743203823"},
|
|
{title:"2026 Kawasaki Vulcan 900 Classic LT",price:"$11,949",sub:"Fredericton, NB · dealer",chips:["Demo"],
|
|
year:"2026",km:"—",cc:"903 cc",family:"Vulcan 900",style:"Street cruiser",loc:"Fredericton, NB",seller:"Dealer",seen:"Today",
|
|
img:"https://media.kijiji.ca/api/v1/ca-prod-dealer-ads/images/2e/2eb55a55-1648-4e1f-8ca0-ba3fcdeb78d7?rule=kijijica-960-webp",
|
|
desc:"Plus 1 free year Kawasaki Protection Plus (KPP). Limited time offer.",
|
|
url:"https://www.kijiji.ca/v-street-cruisers-choppers/fredericton/2026-kawasaki-vulcan-900-classic-lt-demo-save-800-00-plus-1/1736278655"},
|
|
{title:"2019 Honda Rebel 500",price:"$5,700",sub:"Yarmouth, NS · private",chips:["Windshield"],
|
|
year:"2019",km:"8,900 km",cc:"471 cc",family:"Rebel 500",style:"Street cruiser",loc:"Yarmouth, NS",seller:"Private",seen:"2 days ago",
|
|
img:"https://media.kijiji.ca/api/v1/ca-prod-fsbo-ads/images/a3/a3c41571-2a33-4102-843a-149a76d0c3b5?rule=kijijica-960-webp",
|
|
desc:"Great bike with a professionally painted tank. Comes with gel insert in seat, Kappa back rack, windshield and Givi back box. Light and nimble with a low seat.",
|
|
url:"https://www.kijiji.ca/v-street-cruisers-choppers/yarmouth/2019-honda-rebel-500/1745749820"},
|
|
{title:"2005 Yamaha Vstar 650 Custom Bobber",price:"$5,400",sub:"Bedford, NS · private",chips:["MVI"],
|
|
year:"2005",km:"46,500 km",cc:"649 cc",family:"V-Star 650",style:"Street cruiser",loc:"Bedford, NS",seller:"Private",seen:"12 days ago",
|
|
img:"https://media.kijiji.ca/api/v1/ca-prod-fsbo-ads/images/a3/a349dd47-f2cb-472b-a469-330bad7fa061?rule=kijijica-960-webp",
|
|
desc:"(Bike is ready to ride) Kept inside and in heated storage. MVI: inspection is good for another year. Horn, lights and gauges all work.",
|
|
url:"https://www.kijiji.ca/v-street-cruisers-choppers/bedford/2005-yamaha-vstar-650-custom-bobber/1745767295"},
|
|
{title:"2026 Indian Scout Bobber",price:"$17,199",sub:"Halifax, NS · dealer",chips:["Dealer","Bobber"],
|
|
year:"2026",km:"—",cc:"1,250 cc",family:"Indian Scout",style:"Street cruiser",loc:"Halifax, NS",seller:"Dealer",seen:"Today",img:null,
|
|
desc:"Maroon metallic finish. ABS, LED lighting, 1250 cc V-twin. The long-keep one — saddlebags and extended passenger seat planned.",
|
|
url:"https://www.kijiji.ca/"},
|
|
{title:"2024 Royal Enfield Super Meteor 650",price:"$5,993",sub:"Halifax, NS · dealer",chips:["Dealer","Warranty"],
|
|
year:"2024",km:"3,900 km",cc:"648 cc",family:"Super Meteor 650",style:"Street cruiser",loc:"Halifax, NS",seller:"Dealer",seen:"Today",img:null,
|
|
desc:"2024 Super Meteor in Celestial Red. Dealer maintained, one season ridden.",
|
|
url:"https://www.kijiji.ca/"},
|
|
{title:"2020 Honda 750 Shadow",price:"$2,250",sub:"Bedford, NS · private",chips:["Project"],
|
|
year:"2020",km:"38,000 km",cc:"745 cc",family:"Shadow 750",style:"Street cruiser",loc:"Bedford, NS",seller:"Private",seen:"3 weeks ago",img:null,
|
|
desc:"Reliable commuter Shadow. Honest bike, needs a carb clean and new rear tire.",
|
|
url:"https://www.kijiji.ca/"}
|
|
];
|
|
const ROWS=CARDS.map(c=>({...c,st:0}));
|
|
|
|
const $=id=>document.getElementById(id);
|
|
const stage=$('stage');
|
|
const DEPTH=5; // a 5-card live stack at all times
|
|
const SC=.026, TY=16, DIM=.08; // per-depth step: scale, y-offset, dimming
|
|
let pile=[]; // top-first [{i,d,el}] — every card its own swipable object
|
|
let nextIdx=0, hist=[];
|
|
let dragEl=null, sx=0, sy=0, dx=0, dy=0, moved=false;
|
|
|
|
function isTop(el){ return pile[0]&&pile[0].el===el; }
|
|
function setStamps(el,a,b,c){
|
|
el.querySelector('.stamp.like').style.opacity=a;
|
|
el.querySelector('.stamp.nope').style.opacity=b;
|
|
el.querySelector('.stamp.maybe').style.opacity=c;
|
|
}
|
|
function cardNode(item,i){
|
|
const el=document.createElement('div');
|
|
el.className='card';
|
|
el.innerHTML=`${item.img?`<img class="photo" src="${item.img}" alt="" draggable="false">`:`<div class="nophoto">Photo pending — thumbnail backfill queued</div>`}
|
|
<div class="grad"></div>
|
|
<div class="stamp like">LIKE</div>
|
|
<div class="stamp nope">NOPE</div>
|
|
<div class="stamp maybe">MAYBE</div>
|
|
<button class="ibtn">ⓘ</button>
|
|
<div class="info"><div class="chips">${(item.chips||[]).map(x=>`<span class="chip">${x}</span>`).join('')}</div><div class="title">${item.title}</div><div class="price">${item.price}</div><div class="sub">${item.sub}</div></div>`;
|
|
el.addEventListener('pointerdown',down);
|
|
el.addEventListener('pointermove',move);
|
|
el.addEventListener('pointerup',upH);
|
|
el.addEventListener('pointercancel',upH);
|
|
const ib=el.querySelector('.ibtn');
|
|
ib.addEventListener('pointerdown',e=>e.stopPropagation());
|
|
ib.addEventListener('click',e=>{ e.stopPropagation(); if(isTop(el)) openDetail(item,{type:'deck'}); });
|
|
return {i,d:item,el};
|
|
}
|
|
function layer(el,d,anim){
|
|
el.classList.toggle('topcard',d===0);
|
|
el.style.transition=anim?'transform .22s cubic-bezier(.2,.8,.3,1), filter .22s ease, opacity .22s ease':'none';
|
|
el.style.transform=`translateY(${TY*d}px) scale(${1-SC*d})`;
|
|
el.style.filter=`brightness(${1-DIM*d})`;
|
|
el.style.opacity='1';
|
|
el.style.zIndex=10-d;
|
|
el.style.pointerEvents=d===0?'auto':'none';
|
|
}
|
|
function relayer(){ pile.forEach((c,d)=>layer(c.el,d,true)); }
|
|
function fillPile(){
|
|
while(pile.length<DEPTH&&nextIdx<CARDS.length){
|
|
const entry=cardNode(CARDS[nextIdx],nextIdx), d=pile.length;
|
|
entry.el.style.transition='none';
|
|
entry.el.style.transform=`translateY(${TY*(d+2)}px) scale(${1-SC*(d+2)})`;
|
|
entry.el.style.opacity='0';
|
|
stage.appendChild(entry.el);
|
|
pile.push(entry); nextIdx++;
|
|
void entry.el.offsetWidth;
|
|
layer(entry.el,d,true);
|
|
}
|
|
relayer();
|
|
}
|
|
|
|
/* --- list rows --- */
|
|
function renderList(){
|
|
$('lrows').innerHTML=ROWS.map((r,i)=>`
|
|
<div class="row">
|
|
${r.img?`<img class="thumb" src="${r.img}" alt="">`:`<div class="thumb ph">◍</div>`}
|
|
<div class="rmain"><div class="rtitle">${r.title}</div><div class="rsub">${r.sub}</div></div>
|
|
<div class="rright"><div class="rprice">${r.price}</div>
|
|
<div class="rc" data-i="${i}">
|
|
<button class="rbtn like" data-s="1">♥</button>
|
|
<button class="rbtn maybe" data-s="2">★</button>
|
|
<button class="rbtn nope" data-s="3">✕</button>
|
|
</div>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
$('lrows').addEventListener('click',e=>{
|
|
const b=e.target.closest('.rbtn');
|
|
if(!b){
|
|
const row=e.target.closest('.row'); if(!row) return;
|
|
const i=+row.querySelector('.rc').dataset.i;
|
|
openDetail(ROWS[i],{type:'row',i}); return;
|
|
}
|
|
const i=+b.closest('.rc').dataset.i, s=+b.dataset.s;
|
|
ROWS[i].st = (ROWS[i].st===s)?0:s;
|
|
const ctrl=b.closest('.rc');
|
|
ctrl.querySelectorAll('.rbtn').forEach(btn=>btn.classList.toggle('on',+btn.dataset.s===ROWS[i].st));
|
|
});
|
|
|
|
/* --- fullscreen dossier --- */
|
|
let dCtx=null;
|
|
function openDetail(data,ctx){
|
|
dCtx=ctx||null;
|
|
const ph=$('dphoto');
|
|
if(data.img){ ph.src=data.img; ph.classList.remove('ph'); } else { ph.removeAttribute('src'); ph.classList.add('ph'); }
|
|
$('dtitle').textContent=data.title; $('dprice').textContent=data.price; $('dsub').textContent=data.sub;
|
|
$('dchips').innerHTML=(data.chips||[]).map(x=>`<span class="chip">${x}</span>`).join('');
|
|
const rows=[["Year",data.year],["Odometer",data.km],["Engine",data.cc],["Family",data.family],["Category",data.style],["Location",data.loc],["Seller",data.seller],["Source","Kijiji"],["First seen",data.seen]];
|
|
$('dspecs').innerHTML=rows.map(r=>`<div class="spec"><span class="k">${r[0]}</span><span class="v${(!r[1]||r[1]==='—')?' z':''}">${r[1]||'—'}</span></div>`).join('');
|
|
$('dnotes').textContent=data.desc||'';
|
|
$('dopen').href=data.url||'#';
|
|
$('detail').classList.add('on'); $('dback').classList.add('on');
|
|
$('dbody').scrollTop=0;
|
|
}
|
|
function closeDetail(){ const d=$('detail'); d.style.transition=''; d.style.transform=''; d.classList.remove('on'); $('dback').classList.remove('on'); dCtx=null; }
|
|
$('dclose').onclick=closeDetail;
|
|
$('dclose').addEventListener('pointerdown',e=>e.stopPropagation());
|
|
/* pull-down-to-close (grabber bar + photo) */
|
|
let dDragY=null;
|
|
function dStart(e){ dDragY=e.clientY; $('detail').style.transition='none'; try{e.currentTarget.setPointerCapture(e.pointerId)}catch(_){} }
|
|
function dMoveH(e){ if(dDragY===null) return; const dy=Math.max(0,e.clientY-dDragY); $('detail').style.transform=`translateY(${dy}px)`; }
|
|
function dEndH(e){ if(dDragY===null) return; const dy=Math.max(0,e.clientY-dDragY); dDragY=null;
|
|
if(dy>110){ closeDetail(); }
|
|
else { const d=$('detail'); d.style.transition='transform .25s cubic-bezier(.2,.8,.3,1)'; d.style.transform=''; } }
|
|
[document.querySelector('.dgrab'),document.querySelector('.dhero')].forEach(el=>{ if(!el) return; el.addEventListener('pointerdown',dStart); el.addEventListener('pointermove',dMoveH); el.addEventListener('pointerup',dEndH); el.addEventListener('pointercancel',dEndH); });
|
|
function dAction(a){
|
|
const ctx=dCtx; closeDetail();
|
|
if(!ctx) return;
|
|
if(ctx.type==='deck'){
|
|
if(pile[0]) commit(pile[0].el, a==='like'?'like':(a==='nope'?'nope':'maybe'));
|
|
} else {
|
|
const i=ctx.i, s= a==='like'?1:(a==='maybe'?2:3);
|
|
ROWS[i].st=(ROWS[i].st===s)?0:s;
|
|
const ctrl=document.querySelector(`.rc[data-i="${i}"]`);
|
|
if(ctrl) ctrl.querySelectorAll('.rbtn').forEach(btn=>btn.classList.toggle('on',+btn.dataset.s===ROWS[i].st));
|
|
}
|
|
}
|
|
$('dNo').onclick=()=>dAction('nope'); $('dMb').onclick=()=>dAction('maybe'); $('dLk').onclick=()=>dAction('like');
|
|
/* ⓘ buttons are wired per-card inside cardNode() */
|
|
|
|
/* --- swipe engine: commit + fly are per-card; nothing locks the deck --- */
|
|
function commit(el,dir){
|
|
if(!isTop(el)) return;
|
|
const t=pile.shift();
|
|
hist.push(t.i);
|
|
flyOut(el,dir);
|
|
fillPile(); relayer();
|
|
if(!pile.length) $('empty').classList.add('on');
|
|
}
|
|
function flyOut(el,dir){
|
|
const x= dir==='maybe'?0:(dir==='like'?620:-620);
|
|
const y= dir==='maybe'?-780:40;
|
|
const rot= dir==='maybe'?0:(dir==='like'?26:-26);
|
|
setStamps(el, dir==='like'?1:0, dir==='nope'?1:0, dir==='maybe'?1:0);
|
|
el.classList.remove('topcard');
|
|
el.style.zIndex=12; el.style.pointerEvents='none';
|
|
el.style.transition='transform .3s ease-in, opacity .3s';
|
|
el.style.transform=`translate(${x}px,${y}px) rotate(${rot}deg)`;
|
|
el.style.opacity=.15;
|
|
setTimeout(()=>{ el.remove(); },420);
|
|
}
|
|
function undo(){
|
|
if(!hist.length) return;
|
|
const i=hist.pop();
|
|
const entry=cardNode(CARDS[i],i);
|
|
entry.el.style.transition='none';
|
|
entry.el.style.transform='translateY(-46px) scale(1.02)';
|
|
entry.el.style.opacity='0';
|
|
stage.appendChild(entry.el);
|
|
pile.unshift(entry);
|
|
if(pile.length>DEPTH){ const drop=pile.pop(); drop.el.remove(); nextIdx=drop.i; }
|
|
void entry.el.offsetWidth;
|
|
relayer();
|
|
$('empty').classList.remove('on');
|
|
}
|
|
$('btnLike').onclick=()=>{ if(pile[0]) commit(pile[0].el,'like'); };
|
|
$('btnNope').onclick=()=>{ if(pile[0]) commit(pile[0].el,'nope'); };
|
|
$('btnMaybe').onclick=()=>{ if(pile[0]) commit(pile[0].el,'maybe'); };
|
|
$('btnUndo').onclick=undo;
|
|
|
|
/* --- drag: only the top card is grabbable; every other card stays live --- */
|
|
function down(e){
|
|
const el=e.currentTarget;
|
|
if(!isTop(el)) return;
|
|
dragEl=el; moved=false; sx=e.clientX; sy=e.clientY; dx=0; dy=0;
|
|
try{ el.setPointerCapture(e.pointerId) }catch(_){}
|
|
el.style.transition='none';
|
|
}
|
|
function move(e){
|
|
const el=e.currentTarget;
|
|
if(dragEl!==el) return;
|
|
dx=e.clientX-sx; dy=e.clientY-sy;
|
|
if(Math.abs(dx)>6||Math.abs(dy)>6) moved=true;
|
|
const rot=Math.max(-12,Math.min(12,dx/12));
|
|
el.style.transform=`translate(${dx}px,${dy}px) rotate(${rot}deg)`;
|
|
setStamps(el, Math.max(0,Math.min(1,dx/120)), Math.max(0,Math.min(1,-dx/120)), dy<-20?Math.max(0,Math.min(1,-dy/130)):0);
|
|
}
|
|
function upH(e){
|
|
const el=e.currentTarget;
|
|
if(dragEl!==el) return;
|
|
dragEl=null;
|
|
const w=stage.clientWidth||360;
|
|
el.style.transition='transform .3s cubic-bezier(.2,.8,.3,1)';
|
|
if(dx>w*0.35) commit(el,'like');
|
|
else if(dx<-w*0.35) commit(el,'nope');
|
|
else if(dy<-140) commit(el,'maybe');
|
|
else { el.style.transform=''; setStamps(el,0,0,0); if(!moved && e.type==='pointerup' && isTop(el)) openDetail(pile[0].d,{type:'deck'}); }
|
|
}
|
|
|
|
/* --- tabs --- */
|
|
document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>{
|
|
const name=t.dataset.tab;
|
|
document.querySelectorAll('.tab').forEach(x=>x.classList.toggle('on',x===t));
|
|
$('listScreen').classList.toggle('on',name==='list');
|
|
$('stubMatches').classList.toggle('on',name==='matches');
|
|
$('stubTrends').classList.toggle('on',name==='trends');
|
|
});
|
|
|
|
renderList(); fillPile();
|
|
</script>
|
|
</body>
|
|
</html>
|