openvibers/openvibe.shoes Commits

Shoes' motif: a sneaker in one stroke, the stores' tags arriving (#1)

OP OpenVibers committed 13 days ago · 08fd1a3c13 · 2 files, +33 −0

Changes

ddiff --git a/public/css/shoes.css b/public/css/shoes.css
iindex 7d3da14..53f1d91 100644
---- a/public/css/shoes.css
++++ b/public/css/shoes.css
@@@ -84,3 +84,23 @@
.crumbs a { color: inherit; }
@media (max-width: 900px) { .sh-search { grid-template-columns: 1fr 1fr; } }
@media (max-width: 640px) { .sh-search { grid-template-columns: 1fr; } .sh-wrap { width: min(100% - 24px, 1180px); } }
++
++/* The room's motif: one stroke draws the shoe, then three stores' tags slide in and the lowest lights up. Motion
++ follows the network's tokens (Calm stills it); reduced motion shows the finished picture. */
++.sh-motif { position: relative; width: min(100%, 400px); aspect-ratio: 16 / 11; margin: 0 auto; color: var(--room-accent); }
++.sh-shoe { position: absolute; inset: 10% 6% auto; width: 84%; }
++.sh-shoe path { fill: none; stroke: currentColor; stroke-width: 3.2; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 100;
++ animation: shDraw calc(var(--ov-dur-4, .52s) * 3.5) var(--ov-ease-in-out, ease-in-out) both; }
++@keyframes shDraw { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
++.sh-mt { position: absolute; display: grid; gap: 1px; padding: 7px 12px; border-radius: 10px; background: var(--ovk-panel, #121826); border: 1px solid var(--ovk-line, rgba(255,255,255,.1));
++ box-shadow: 0 14px 30px -18px rgba(0, 0, 0, .7); color: var(--ovk-text, #e8ecf3); font-variant-numeric: tabular-nums;
++ animation: shTag var(--ov-dur-4, .52s) var(--ov-ease-spring, cubic-bezier(.34, 1.56, .64, 1)) both; }
++.sh-mt small { font-size: 11px; color: var(--ovk-muted, #93a0b4); }
++.sh-mt b { font-size: 18px; }
++.sh-mt-1 { right: 0; top: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3); }
++.sh-mt-2 { right: 18%; bottom: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3.4); }
++.sh-mt-3 { left: 4%; bottom: 0; animation-delay: calc(var(--ov-dur-4, .52s) * 3.8); }
++.sh-mt.is-best { border-color: var(--room-accent); box-shadow: 0 0 0 1px var(--room-accent), 0 14px 30px -18px rgba(0, 0, 0, .7); }
++.sh-mt.is-best b { color: var(--room-strong, var(--room-accent)); }
++@keyframes shTag { from { opacity: 0; transform: translateX(24px); } }
++@media (prefers-reduced-motion: reduce) { .sh-shoe path, .sh-mt { animation: none; } }
ddiff --git a/server/views/index.js b/server/views/index.js
iindex 95053e3..6c33844 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -175,6 +175,18 @@ function searchForm({ q = '', facets = {}, action = '/shoes', values = {} }) {
}
// ── Home ────────────────────────────────────────────────────
++/**
++ * The room's motif (rooms.js: "a shoe drawn as one line, price tags from several stores sliding in"): a sneaker in a
++ * single stroke that draws itself, then three stores' tags arrive and the lowest one lights up. Drawn, not a photo.
++ */
++function shoeMotif() {
++ const tag = (store, price, n, best) => `<span class="sh-mt sh-mt-${n}${best ? ' is-best' : ''}"><small>${store}</small><b>${price}</b></span>`;
++ return `<div class="sh-motif" aria-hidden="true">
++<svg class="sh-shoe" viewBox="0 0 320 170"><path pathLength="100" d="M30 140 C 26 120, 28 100, 36 90 C 46 82, 64 84, 84 92 C 96 96, 104 88, 110 74 C 114 64, 122 58, 132 60 L 140 63 C 162 78, 200 96, 240 104 C 272 110, 298 116, 304 130 C 306 138, 302 142, 294 142 L 36 142 C 32 142, 30 141, 30 140 Z M 26 142 L 26 151 C 26 156, 30 158, 36 158 L 296 158 C 304 158, 310 152, 308 142 M 150 76 L 160 67 M 168 84 L 178 75 M 186 92 L 196 83 M 70 124 C 120 120, 180 113, 244 120 M 36 96 L 28 98"/></svg>
++${tag('Store A', '$129', 1, false)}${tag('Store B', '$112', 2, false)}${tag('Store C', '$98', 3, true)}
++</div>`;
++}
++
function home({ facets = {}, dropping = [], dropsToday = [], popular = [], curveModel = null, curve = null, sceneDrop = null, jobs = [], now = Date.now() } = {}) {
const brands = (facets.brands || []).slice(0, 5);
const priceDrops = dropsToday.filter((m) => m.drop_amount).slice(0, 8);
@@@ -217,6 +229,7 @@ function home({ facets = {}, dropping = [], dropsToday = [], popular = [], curve
lede: 'One page per shoe, every store\'s offer in one all-in price, the price history from Shoes\' own daily snapshots, and a watch that tells you when your size drops below your price.',
search: { action: '/shoes', name: 'q', placeholder: 'Search a shoe, a brand or a model', label: 'Search shoes', button: 'Search', hints: ['Air Max 90', 'Samba', 'Pegasus', 'New Balance 990'] },
chips: brands.map((b) => ({ label: b.value, href: `/shoes?brand=${encodeURIComponent(b.value)}` })),
++ motif: shoeMotif(),
},
proof: [
{ value: facets.models, label: facets.models === 1 ? 'model tracked' : 'models tracked' },