/* Running Club — shared kit: icons, media placeholders, helpers */ function hashCode(str) { let h = 0; for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) | 0; return Math.abs(h); } function usePersisted(key, initial) { const [v, setV] = React.useState(() => { try { const raw = localStorage.getItem(key); return raw != null ? JSON.parse(raw) : initial; } catch (e) { return initial; } }); React.useEffect(() => { try { localStorage.setItem(key, JSON.stringify(v)); } catch (e) {} }, [key, v]); return [v, setV]; } /* ---- Icons (simple line/solid) ---- */ function Icon({ name, size = 20, color = 'currentColor', stroke = 2 }) { const p = { fill: 'none', stroke: color, strokeWidth: stroke, strokeLinecap: 'round', strokeLinejoin: 'round' }; const paths = { play: , share: , download:, close: , plus: , left: , right: , up: , pin: , calendar:, ruler: , watch: , runner: , grid: , heart: , chat: , flame: , camera: , check: , dots: , }; return ( {paths[name]} ); } /* ---- Media placeholder: duotone "scene" built from simple shapes ---- */ function MediaScene({ scene, seed = 0, style }) { const pal = (window.CLUB.SCENES[scene]) || ['#2b2b2f', '#9a9aa2']; const [deep, light] = pal; const r = (n) => ((seed * 9301 + 49297 + n * 233) % 100) / 100; // deterministic 0..1 let shapes = null; if (scene === 'lac' || scene === 'montagne' || scene === 'sunset') { const sunX = 20 + r(1) * 60, sunY = 26 + r(2) * 12; shapes = ( ); } else if (scene === 'foret') { shapes = ( {[12, 30, 50, 68, 86].map((x, i) => ( ))} ); } else if (scene === 'route') { shapes = ( {[10, 35, 62, 90].map((y, i) => ( ))} ); } else if (scene === 'groupe') { shapes = ( {[24, 44, 64, 80].map((x, i) => ( ))} ); } else if (scene === 'gourde') { shapes = ( {[18, 30, 70, 82, 26, 74].map((x, i) => ( ))} ); } else if (scene === 'medaille') { shapes = ( ); } else if (scene === 'stade') { shapes = ( ); } else if (scene === 'ville') { shapes = ( {[6, 22, 36, 52, 68, 84].map((x, i) => ( ))} ); } return (
{shapes} {/* subtle film grain / vignette */}
); } /* play badge for videos, sits inside a positioned tile */ function VideoBadge({ dur }) { return (
{dur && (
{dur}
)}
); } /* ---- Media thumb: real file (media/…) OR a styled placeholder scene ---- */ function MediaThumb({ media, large }) { const src = media.fichier ? ('media/' + media.fichier) : null; const fill = { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }; if (src && media.type === 'video') { return ( ); } if (src) { return {media.note; } // fallback placeholder return ( {media.type === 'video' && } ); } Object.assign(window, { hashCode, usePersisted, Icon, MediaScene, VideoBadge, MediaThumb });