/* archive.jsx — past events archive section + interactive primitives */
const { useState: useStateA, useEffect: useEffectA, useRef: useRefA } = React;
/* --- IntersectionObserver reveal hook (local copy; scopes are separate) --- */
function useReveal() {
const ref = useRefA(null);
useEffectA(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } });
}, { threshold: 0.18 });
io.observe(el);
return () => io.disconnect();
}, []);
return ref;
}
/* --- Count-up number animation --- */
function CountUp({ end, duration = 1800, suffix = '', prefix = '', decimals = 0 }) {
const [val, setVal] = useStateA(0);
const ref = useRefA(null);
const started = useRefA(false);
useEffectA(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting && !started.current) {
started.current = true;
const start = performance.now();
const tick = (t) => {
const p = Math.min(1, (t - start) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(end * eased);
if (p < 1) requestAnimationFrame(tick);
else setVal(end);
};
requestAnimationFrame(tick);
io.disconnect();
}
});
}, { threshold: 0.4 });
io.observe(el);
return () => io.disconnect();
}, [end, duration]);
const formatted = decimals > 0 ? val.toFixed(decimals) : Math.round(val).toLocaleString('ru-RU');
return {prefix}{formatted}{suffix};
}
/* --- Tilt wrapper for cards --- */
function Tilt({ children, max = 6, className = '', style = {} }) {
const ref = useRefA(null);
const onMove = (e) => {
const el = ref.current; if (!el) return;
const r = el.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width;
const y = (e.clientY - r.top) / r.height;
const rx = (0.5 - y) * max;
const ry = (x - 0.5) * max;
el.style.transform = `perspective(900px) rotateX(${rx}deg) rotateY(${ry}deg)`;
};
const onLeave = () => {
if (ref.current) ref.current.style.transform = '';
};
return (
{children}
);
}
/* --- Magnetic button hook --- */
function useMagnetic(strength = 18) {
const ref = useRefA(null);
useEffectA(() => {
const el = ref.current; if (!el) return;
let raf;
const onMove = (e) => {
const r = el.getBoundingClientRect();
const x = e.clientX - (r.left + r.width / 2);
const y = e.clientY - (r.top + r.height / 2);
cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
el.style.transform = `translate(${(x/r.width) * strength}px, ${(y/r.height) * strength}px)`;
});
};
const onLeave = () => {
cancelAnimationFrame(raf);
el.style.transform = '';
};
el.addEventListener('mousemove', onMove);
el.addEventListener('mouseleave', onLeave);
return () => {
el.removeEventListener('mousemove', onMove);
el.removeEventListener('mouseleave', onLeave);
};
}, [strength]);
return ref;
}
/* --- Archive data --- */
const ARCHIVE = [
{ id:'AR-024', when:'Май 2026', title:'Шаббат-ужин · Резиденция Альфаси', loc:'Кейсария', guests:24, photos:38, scene:'scene-dinner', size:'size-lg', cat:'Ужины', desc:'Закрытый вечер в частной резиденции у моря. Шеф Эял Шани, разговор о поколениях капитала и о том, как дети наследуют не деньги, а этику.' },
{ id:'AR-023', when:'Апр 2026', title:'Регата 24h · Хайфский залив', loc:'Хайфа', guests:18, photos:124, scene:'scene-sea', size:'size-md', cat:'Спорт', desc:'Три яхты, два дня, ночёвка под звёздами. Команды формируются жеребьёвкой — лучший способ узнать партнёра до подписания term sheet.' },
{ id:'AR-022', when:'Мар 2026', title:'Винный салон · Долина Эла', loc:'Иудейские горы', guests:32, photos:62, scene:'scene-vineyard', size:'size-md', cat:'Гастрономия', desc:'Резервные коллекции трёх виноделен. Авирам Кац открывает разговор о терруаре севера и инвестициях в винодельческий бизнес.' },
{ id:'AR-021', when:'Фев 2026', title:'Открытие коллекции Шапиро', loc:'Тель‑Авив', guests:41, photos:89, scene:'scene-gallery', size:'size-sm', cat:'Культура', desc:'Приватный preview новой галереи Эстер Шапиро. Современное израильское искусство, разговор с куратором Тейт Модерн.' },
{ id:'AR-020', when:'Янв 2026', title:'Пустынный завтрак · Махтеш Рамон', loc:'Негев', guests:16, photos:47, scene:'scene-desert', size:'size-sm', cat:'Путешествия', desc:'Восход в кратере, бедуинский завтрак, разговор о Негеве как следующей экономической границе Израиля.' },
{ id:'AR-019', when:'Дек 2025', title:'Хануккальный приём · Старый город', loc:'Иерусалим', guests:52, photos:96, scene:'scene-jerusalem', size:'size-sm', cat:'Праздники', desc:'Восемь свечей, восемь спикеров, восемь историй о том, как свет переживает темноту. Закрытое мероприятие в Еврейском квартале.' },
{ id:'AR-018', when:'Ноя 2025', title:'Tikkun Olam · вечер благотворения', loc:'Герцлия', guests:74, photos:148, scene:'scene-shabbat', size:'size-wide', cat:'Филантропия', desc:'Резиденты клуба собрали ₪4.2 млн на программу образования для детей репатриантов. Спикер — президент Israel Education Fund.' },
];
const CATS = ['Все', 'Ужины', 'Гастрономия', 'Спорт', 'Культура', 'Путешествия', 'Праздники', 'Филантропия'];
/* --- Archive section --- */
function Archive() {
const [filter, setFilter] = useStateA('Все');
const [active, setActive] = useStateA(null); // selected event for lightbox
const r = useReveal();
useEffectA(() => {
const onKey = (e) => { if (e.key === 'Escape') setActive(null); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
useEffectA(() => {
document.body.style.overflow = active ? 'hidden' : '';
}, [active]);
const items = filter === 'Все' ? ARCHIVE : ARCHIVE.filter(a => a.cat === filter);
return (
§ 04 — Архив
Что уже было
в этом году.
Мы не публикуем фото в соцсетях — но архив открыт для тех, кто думает о вступлении. Семь моментов из двадцати четырёх событий 2025—2026.
{CATS.map(c => (
))}
{items.map(a => (
setActive(a)} />
))}
setActive(null)} />
);
}
function ArchiveCard({ a, onOpen }) {
return (
FRAME {a.id}
{a.photos} КАДРОВ
{a.when}
{a.loc}
{a.guests} гостей
);
}
function Lightbox({ event, onClose }) {
if (!event) {
return ;
}
return (
e.stopPropagation()}>
FRAME {event.id}
{event.photos} КАДРОВ
{event.when} · {event.loc}
{event.title.replace(/ /g, ' ')}
{event.guests}Гостей
{event.photos}Фото в архиве
{event.cat}Категория
);
}
Object.assign(window, { Archive, CountUp, Tilt, useMagnetic });