// data.jsx — loader, тянет контент из PocketBase API. // Структура итогового объекта: // { DIRECTIONS, TRAINERS, SCHEDULE: {days, times, sample}, FAQ, PRICES, // GALLERY, HERO_MEDIA, HERO_VIDEO, REVIEWS } const API_BASE = "/api"; const DAYS_ORDER = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"]; const DAYS_GRID = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ"]; // воскресенье студия закрыта const TIMES_GRID = ["10:30", "12:00", "18:00", "19:30", "21:00"]; async function fetchAll(collection, sort = "order") { const url = `${API_BASE}/collections/${collection}/records?perPage=500&sort=${encodeURIComponent(sort)}`; const r = await fetch(url, { credentials: "omit" }); if (!r.ok) throw new Error(`${collection}: ${r.status}`); const j = await r.json(); return j.items || []; } // Soft fetch — если коллекции ещё нет (404) или ошибка сети, вернёт [] вместо throw. // Нужно для опциональных секций (gallery/reviews), чтоб сайт не падал. async function fetchAllSoft(collection, sort = "order") { try { return await fetchAll(collection, sort); } catch (e) { console.warn(`[data.jsx] soft-fetch ${collection} failed:`, e.message); return []; } } const GALLERY_PLACEHOLDERS = [ { id: "ph-1", caption: "Зал · ночь", aspect: "3/4", placeholder: true }, { id: "ph-2", caption: "Pole · экзотик", aspect: "3/4", placeholder: true }, { id: "ph-3", caption: "Strip-пластика", aspect: "3/4", placeholder: true }, { id: "ph-4", caption: "Heel · урок", aspect: "3/4", placeholder: true }, { id: "ph-5", caption: "Раздевалка", aspect: "3/4", placeholder: true }, { id: "ph-6", caption: "После занятия", aspect: "3/4", placeholder: true }, ]; function fileUrl(collectionId, recordId, filename, thumb) { if (!filename) return null; const base = `${API_BASE}/files/${collectionId}/${recordId}/${filename}`; return thumb ? `${base}?thumb=${thumb}` : base; } window.STRIP_LOAD = async function loadAll() { const [directions, trainersRaw, scheduleRaw, prices, faq, galleryRaw, reviewsRaw, heroVideoRaw] = await Promise.all([ fetchAll("directions"), fetchAll("trainers"), fetchAll("schedule", "time"), fetchAll("prices"), fetchAll("faq"), fetchAllSoft("gallery"), fetchAllSoft("reviews"), fetchAllSoft("hero_video"), ]); // schedule grid: { "10:30-ПН": {name, who, type} } const sample = {}; for (const s of scheduleRaw) { if (s.active === false) continue; sample[`${s.time}-${s.day}`] = { name: s.name, who: s.who_label, type: s.type }; } // trainers: подмешать stats + персональное расписание из общей schedule const trainers = trainersRaw .filter((t) => t.active !== false) .map((t) => { const personal = scheduleRaw .filter((s) => s.active !== false && s.who_label === t.first) .sort((a, b) => DAYS_ORDER.indexOf(a.day) - DAYS_ORDER.indexOf(b.day) || a.time.localeCompare(b.time) ) .map((s) => ({ d: s.day, t: s.time, k: s.name })); return { id: t.code || t.id, first: t.first, last: t.last, role: t.role, chip: t.chip, bio: t.bio, tags: Array.isArray(t.tags) ? t.tags : [], stats: { exp: t.stats_exp, students: t.stats_students, style: t.stats_style, }, photo_url: fileUrl(t.collectionId, t.id, t.photo, "600x800"), photo_url_lg: fileUrl(t.collectionId, t.id, t.photo, "900x1200"), photo_label: `ФОТО · ${t.first.toUpperCase()} — ${(t.role || "").toUpperCase()}`, schedule: personal, }; }); // directions: маппим code → id, чтоб старый ключ продолжал работать const DIRECTIONS = directions.map((d) => ({ id: d.code || d.id, no: d.no, name: d.name, accent: d.accent || "", desc: d.desc, levels: d.levels, sessions: d.sessions, })); const FAQ = faq.map((f) => ({ q: f.question, a: f.answer })); const PRICES = prices.map((p) => ({ id: p.id, name: p.name, accent: p.accent || "", price: p.price, per: p.per, items: Array.isArray(p.items) ? p.items : [], cta: p.cta, featured: !!p.featured, badge: p.badge || null, kind: p.kind || "pack", // trial | pack | unlimited duration: Number(p.duration) || 0, // минуты: 60 / 90, 0 = неприменимо sessions: Number(p.sessions) || 0, // занятий в абонементе })); // gallery: если PB-коллекция пустая — отдаём 6 плейсхолдеров, чтоб секция была видна const galleryReal = galleryRaw .filter((g) => g.active !== false) .map((g) => ({ id: g.id, caption: g.caption || "", aspect: g.aspect || "3/4", featured: !!g.featured, placeholder: !g.image, url: fileUrl(g.collectionId, g.id, g.image, "800x1200"), url_lg: fileUrl(g.collectionId, g.id, g.image, "1600x2000"), })); const GALLERY = galleryReal.length > 0 ? galleryReal : GALLERY_PLACEHOLDERS; // hero media: фотки, отмеченные featured=true (легаси, на будущее — если решим показывать carousel) const HERO_MEDIA = galleryReal.filter((g) => g.featured && !g.placeholder); // hero video: активное видео с минимальным order. null если ничего не загружено. const heroVideoActive = heroVideoRaw .filter((v) => v.active !== false && v.file) .sort((a, b) => (a.order || 0) - (b.order || 0)); const HERO_VIDEO = heroVideoActive.length > 0 ? { id: heroVideoActive[0].id, url: fileUrl(heroVideoActive[0].collectionId, heroVideoActive[0].id, heroVideoActive[0].file), label: heroVideoActive[0].label || "", } : null; // reviews: отзывы для социального proof const REVIEWS = reviewsRaw .filter((r) => r.active !== false) .map((r) => ({ id: r.id, name: r.name, text: r.text, source: r.source || "other", source_url: r.source_url || "", rating: r.rating || null, date: r.date_label || "", })); return { DIRECTIONS, TRAINERS: trainers, SCHEDULE: { days: DAYS_GRID, times: TIMES_GRID, sample }, FAQ, PRICES, GALLERY, HERO_MEDIA, HERO_VIDEO, REVIEWS, }; };