/* 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 (
);
}
/* ---- 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 (
{/* 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 (
{!large && }
);
}
if (src) {
return
;
}
// fallback placeholder
return (
{media.type === 'video' && }
);
}
Object.assign(window, { hashCode, usePersisted, Icon, MediaScene, VideoBadge, MediaThumb });