MVP сайта детского лагеря Сокол

- index.html: 9 секций (header, hero, about, shifts, schedule, gallery, signup, payment, contacts, footer)
- style.css: оранжево-синяя палитра, шрифт Nunito, мобильная адаптация
- script.js: рендер карточек смен из JSON, галерея с лайтбоксом, форма с валидацией
- data/shifts.json: 4 mock-смены

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aiganym 2026-05-22 11:52:16 +05:00
parent 62621304fa
commit ca40ebeeb0
6 changed files with 1084 additions and 0 deletions

114
PRD.md
View File

@ -0,0 +1,114 @@
# PRD — Сайт детского лагеря «Сокол»
> Документ написан простым языком — без программистских терминов.
> Цель: объяснить, каким должен быть сайт, чтобы передать его разработчику.
---
## 1. Что мы хотим создать
Сайт для детского оздоровительного лагеря **«Сокол»**.
Сайт должен быть понятным, красивым и удобным — чтобы родители могли легко записать ребёнка в лагерь, а дети могли узнать, что их ждёт.
**Инстаграм лагеря:** [https://www.instagram.com/doc.sokol_](https://www.instagram.com/doc.sokol_/)
---
## 2. Для кого этот сайт
### Родители
Хотят быстро и без лишних звонков:
- записать ребёнка в лагерь
- узнать расписание смен и программу
- оплатить путёвку онлайн
- связаться с администрацией, если есть вопросы
### Дети
Хотят увидеть, что их ждёт:
- яркие фото и видео из жизни лагеря
- расписание мероприятий и кружков
---
## 3. Что должно быть на сайте
### Главная страница
- Красивое фото или видео с территории лагеря
- Короткий слоган — одна фраза о том, что такое «Сокол»
- Большая кнопка **«Записаться»** и **«Узнать цены»**
### Запись в лагерь
- Простая форма: имя ребёнка, возраст, телефон родителя, выбор смены
- После отправки — подтверждение, что заявка принята
### Онлайн-оплата путёвки
- Возможность оплатить картой прямо на сайте
- Информация о стоимости каждой смены и что в неё входит
### Расписание и программа
- Даты всех смен на текущий сезон
- Описание дня в лагере: подъём, занятия, мероприятия, отбой
- Список кружков, секций и конкурсов
### Фото и видео
- Галерея из Инстаграма и дополнительные материалы
- Короткие видео — чтобы дети и родители почувствовали атмосферу
### Контакты и связь с администрацией
- Телефон, WhatsApp / Telegram
- Адрес лагеря и как добраться
- Форма обратной связи для вопросов
### О лагере
- Коротко: история, ценности, команда вожатых
- Фото персонала (вызывает доверие у родителей)
---
## 4. Что уже есть и можно использовать
| Материал | Статус |
|---|---|
| Инстаграм [@doc.sokol_](https://www.instagram.com/doc.sokol_/) | Готов, использовать как основу |
| Фото и видео из лагеря | Есть — передать разработчику |
| Брошюры / презентации | Есть — передать разработчику |
---
## 5. Как должен выглядеть сайт
- **Яркий и летний** — солнечные цвета, живые фото, радостная атмосфера
- **Простой** — родитель любого возраста разбирается за 1–2 минуты
- **Удобный на телефоне** — большинство заходят с мобильного
- **Без лишнего текста** — больше фото, меньше слов
---
## 6. Что НЕ нужно на первом этапе
- Личный кабинет для родителей
- Форум или чат внутри сайта
- Сложные фильтры и поиск
---
## 7. Главный результат
Родитель заходит на сайт → за 2–3 минуты понимает, что лагерь классный → записывает ребёнка и оплачивает путёвку онлайн, не выходя из дома.
---
## 8. Сроки
**Приоритет: запустить сайт до начала лета.**
Рекомендуемый порядок работ:
1. Главная страница + форма записи — *в первую очередь*
2. Расписание смен и цены — *в первую очередь*
3. Галерея фото и видео — *в первую очередь*
4. Онлайн-оплата — *после основного запуска*
5. Блок «О лагере» и контакты — *вместе с основным запуском*
---
*Документ подготовлен сотрудником Казахтелеком для передачи разработчику сайта.*

211
TRD.md
View File

@ -0,0 +1,211 @@
# TRD — Техническое задание. Сайт детского лагеря «Сокол» (MVP)
> Цель этого документа — дать разработчику чёткие инструкции.
> Задача: рабочий MVP за ~1 час. Не идеальный код, а работающий сайт.
---
## 1. Выбор технологии
**Стек: чистый HTML + CSS + JavaScript (без фреймворков)**
Почему именно это:
- Не нужно ничего устанавливать — открыл файл в браузере и готово
- Разработчик любого уровня разберётся за минуты
- Легко выложить на бесплатный хостинг (GitHub Pages, Netlify, Vercel)
- Онлайн-оплата подключается через готовый виджет от Kaspi / CloudPayments — просто вставить код
---
## 2. Структура папок проекта
```
sokol-camp/
│
├── index.html ← единственная страница сайта
├── style.css ← все стили (цвета, шрифты, отступы)
├── script.js ← логика: форма записи, галерея
│
├── images/ ← папка с фотографиями
│ ├── hero.jpg ← главное фото на обложке
│ ├── photo-1.jpg
│ ├── photo-2.jpg
│ └── ...
│
├── data/
│ └── shifts.json ← данные о сменах (даты, цены, описание)
│
└── README.md ← как запустить проект (2 строки)
```
---
## 3. Какие данные нужны
### Смены лагеря — файл `data/shifts.json`
Разработчик заполняет этот файл реальными данными от заказчика.
```json
[
{
"id": 1,
"name": "Смена 1 — «Старт лета»",
"dates": "1 июня — 21 июня 2025",
"price": 120000,
"currency": "тенге",
"spots_left": 12,
"description": "Спорт, творчество, командные игры. Возраст: 7–12 лет.",
"includes": ["3-разовое питание", "проживание в корпусе", "все мероприятия"]
},
{
"id": 2,
"name": "Смена 2 — «Приключения»",
"dates": "25 июня — 15 июля 2025",
"price": 135000,
"currency": "тенге",
"spots_left": 8,
"description": "Походы, квесты, природа. Возраст: 10–15 лет.",
"includes": ["3-разовое питание", "проживание в корпусе", "все мероприятия", "поход выходного дня"]
}
]
```
### Форма заявки — данные, которые вводит родитель
| Поле | Тип | Обязательное |
|---|---|---|
| Имя ребёнка | Текст | Да |
| Возраст ребёнка | Число | Да |
| Имя родителя | Текст | Да |
| Телефон | Телефон | Да |
| Выбор смены | Выпадающий список | Да |
| Комментарий | Текст | Нет |
**Куда отправляется заявка (MVP-вариант):**
Использовать бесплатный сервис [Formspree.io](https://formspree.io) — заявка приходит на email администратора. Настройка: 5 минут, регистрация бесплатна.
---
## 4. Из чего состоит `index.html` (одна страница, секции сверху вниз)
```
[1] Шапка (header)
— Логотип / название «Сокол»
— Меню: О лагере | Смены | Галерея | Контакты
— Кнопка «Записаться» (ведёт вниз на форму)
[2] Обложка (hero)
— Большое фото лагеря на весь экран
— Слоган: «Лето, которое запомнится навсегда»
— Две кнопки: «Записаться» и «Узнать цены»
[3] О лагере
— 3–4 предложения + фото команды
[4] Смены и цены
— Карточки смен (данные берутся из shifts.json)
— Кнопка «Выбрать смену» → прокрутка к форме
[5] Расписание дня
— Простая таблица: время → активность
[6] Галерея
— Сетка фото (12–16 штук)
— При клике — увеличение фото
[7] Форма записи
— Поля (см. раздел 3)
— Кнопка «Отправить заявку»
— После отправки: сообщение «Заявка принята! Мы позвоним вам в течение часа.»
[8] Контакты
— Телефон (кликабельный — сразу звонит)
— WhatsApp и Telegram (кнопки)
— Адрес
— Ссылка на Инстаграм: instagram.com/doc.sokol_
[9] Подвал (footer)
— Название лагеря, год, ссылки
```
---
## 5. Онлайн-оплата (добавить после базового запуска)
Для MVP достаточно указать реквизиты и кнопку «Оплатить через Kaspi».
Kaspi позволяет принимать оплату по номеру телефона или QR-коду — без подключения эквайринга.
Инструкция для разработчика:
1. Добавить кнопку «Оплатить» рядом с каждой сменой
2. При нажатии — показать QR-код Kaspi или номер для перевода
3. Попросить родителя прислать скриншот оплаты в WhatsApp
Полноценный эквайринг (CloudPayments, Stripe) — следующий этап, не MVP.
---
## 6. Дизайн — минимальные требования
| Параметр | Значение |
|---|---|
| Основной цвет | Ярко-оранжевый `#FF6B35` или небесно-синий `#00AAFF` |
| Фоновый цвет | Белый `#FFFFFF` |
| Шрифт | Google Fonts: «Nunito» (весёлый, читаемый) |
| Кнопки | Крупные, яркие, с закруглёнными углами |
| Мобильная версия | Обязательно — меню сворачивается в «гамбургер» |
---
## 7. Как запустить проект
### Локально (на компьютере разработчика)
```bash
# 1. Скачать папку проекта
# 2. Открыть index.html в браузере — сайт уже работает
```
Никаких установок не нужно.
### Выложить в интернет (бесплатно, за 5 минут)
Использовать [Netlify Drop](https://app.netlify.com/drop):
1. Зайти на сайт netlify.com/drop
2. Перетащить папку `sokol-camp/` в браузер
3. Готово — сайт получит адрес вида `sokol-camp.netlify.app`
4. Позже можно подключить свой домен (например, `sokol-camp.kz`)
---
## 8. Порядок работы (чтобы уложиться в 1 час)
| Время | Задача |
|---|---|
| 0–10 мин | Создать структуру папок, вставить фото |
| 10–25 мин | Сделать секции: шапка, обложка, о лагере |
| 25–40 мин | Добавить смены (из JSON), форму записи |
| 40–50 мин | Галерея, контакты, подвал |
| 50–60 мин | Проверить на телефоне, выложить на Netlify |
---
## 9. Что НЕ делать в MVP
- Не писать базу данных — заявки идут на email через Formspree
- Не делать панель администратора — смены редактируются в файле `shifts.json`
- Не подключать сложный эквайринг — только Kaspi QR
- Не делать несколько страниц — всё на одной `index.html`
---
## 10. Ссылки и ресурсы для разработчика
| Ресурс | Зачем |
|---|---|
| [formspree.io](https://formspree.io) | Приём заявок на email бесплатно |
| [fonts.google.com](https://fonts.google.com) | Шрифт Nunito |
| [netlify.com/drop](https://app.netlify.com/drop) | Быстрая публикация сайта |
| [instagram.com/doc.sokol_](https://www.instagram.com/doc.sokol_/) | Фото и видео для галереи |
| [squoosh.app](https://squoosh.app) | Сжать фото перед загрузкой |
---
*Документ составлен на основе PRD лагеря «Сокол». Версия MVP — первый запуск.*

42
data/shifts.json Normal file
View File

@ -0,0 +1,42 @@
[
{
"id": 1,
"name": "Смена 1 — «Старт лета»",
"dates": "1 июня — 21 июня 2025",
"price": 120000,
"currency": "тенге",
"spots_left": 12,
"description": "Спорт, творчество, командные игры. Возраст: 7–12 лет.",
"includes": ["3-разовое питание", "проживание в корпусе", "все мероприятия"]
},
{
"id": 2,
"name": "Смена 2 — «Приключения»",
"dates": "25 июня — 15 июля 2025",
"price": 135000,
"currency": "тенге",
"spots_left": 8,
"description": "Походы, квесты, природа. Возраст: 10–15 лет.",
"includes": ["3-разовое питание", "проживание в корпусе", "все мероприятия", "поход выходного дня"]
},
{
"id": 3,
"name": "Смена 3 — «Творческая лаборатория»",
"dates": "19 июля — 8 августа 2025",
"price": 130000,
"currency": "тенге",
"spots_left": 15,
"description": "Музыка, театр, рисование, мультипликация. Возраст: 8–14 лет.",
"includes": ["3-разовое питание", "проживание в корпусе", "мастер-классы", "итоговый концерт"]
},
{
"id": 4,
"name": "Смена 4 — «Прощание с летом»",
"dates": "12 августа — 30 августа 2025",
"price": 125000,
"currency": "тенге",
"spots_left": 20,
"description": "Спорт, дискотеки, костры, кино под открытым небом. Возраст: 9–15 лет.",
"includes": ["3-разовое питание", "проживание в корпусе", "все мероприятия", "выпускной"]
}
]

246
index.html Normal file
View File

@ -0,0 +1,246 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Детский лагерь «Сокол» — лето 2025</title>
<meta name="description" content="Детский лагерь «Сокол»: смены, цены, расписание, запись онлайн." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- [1] HEADER -->
<header class="header" id="top">
<div class="container header__inner">
<a href="#top" class="logo">
<span class="logo__mark">🦅</span>
<span class="logo__text">Сокол</span>
</a>
<nav class="nav" id="nav">
<a href="#about">О лагере</a>
<a href="#shifts">Смены</a>
<a href="#schedule">Расписание</a>
<a href="#gallery">Галерея</a>
<a href="#contacts">Контакты</a>
</nav>
<a href="#signup" class="btn btn--primary btn--small nav__cta">Записаться</a>
<button class="burger" id="burger" aria-label="Меню">
<span></span><span></span><span></span>
</button>
</div>
</header>
<!-- [2] HERO -->
<section class="hero">
<div class="hero__bg"></div>
<div class="container hero__content">
<h1 class="hero__title">Лето, которое запомнится навсегда</h1>
<p class="hero__subtitle">Детский лагерь «Сокол» — спорт, друзья, приключения и творчество для детей 7–15 лет.</p>
<div class="hero__buttons">
<a href="#signup" class="btn btn--primary">Записаться</a>
<a href="#shifts" class="btn btn--secondary">Узнать цены</a>
</div>
</div>
</section>
<!-- [3] ABOUT -->
<section class="section" id="about">
<div class="container about">
<div class="about__text">
<h2>О лагере</h2>
<p>«Сокол» — детский лагерь с 30-летней историей. Корпуса в сосновом лесу, своё озеро, спортивные площадки и опытные вожатые.</p>
<p>Каждая смена — отдельная тема: спорт, творчество, природа. Дети возвращаются домой с новыми друзьями и яркими историями.</p>
<ul class="about__points">
<li>✅ Лицензированные педагоги и медики</li>
<li>✅ Соотношение 1 вожатый на 8 детей</li>
<li>✅ Питание 3 раза в день + полдник</li>
<li>✅ Круглосуточная охрана территории</li>
</ul>
</div>
<div class="about__photo" aria-label="Команда лагеря"></div>
</div>
</section>
<!-- [4] SHIFTS -->
<section class="section section--gray" id="shifts">
<div class="container">
<h2 class="section__title">Смены и цены</h2>
<p class="section__lead">Выбирайте удобные даты. Места ограничены.</p>
<div class="shifts" id="shifts-grid">
<!-- shift cards rendered by script.js -->
</div>
</div>
</section>
<!-- [5] SCHEDULE -->
<section class="section" id="schedule">
<div class="container">
<h2 class="section__title">Расписание дня</h2>
<p class="section__lead">Чем занят ребёнок с утра до вечера.</p>
<table class="schedule">
<thead>
<tr><th>Время</th><th>Активность</th></tr>
</thead>
<tbody>
<tr><td>08:00</td><td>Подъём, зарядка на свежем воздухе</td></tr>
<tr><td>09:00</td><td>Завтрак</td></tr>
<tr><td>10:00</td><td>Кружки и мастер-классы</td></tr>
<tr><td>12:30</td><td>Спорт и игры на улице</td></tr>
<tr><td>14:00</td><td>Обед</td></tr>
<tr><td>15:00</td><td>Тихий час</td></tr>
<tr><td>16:30</td><td>Полдник</td></tr>
<tr><td>17:00</td><td>Командные игры, бассейн</td></tr>
<tr><td>19:00</td><td>Ужин</td></tr>
<tr><td>20:00</td><td>Вечернее мероприятие, дискотека</td></tr>
<tr><td>22:00</td><td>Отбой</td></tr>
</tbody>
</table>
</div>
</section>
<!-- [6] GALLERY -->
<section class="section section--gray" id="gallery">
<div class="container">
<h2 class="section__title">Галерея</h2>
<p class="section__lead">Так выглядит лето в «Соколе».</p>
<div class="gallery" id="gallery-grid">
<!-- gallery items rendered by script.js -->
</div>
</div>
</section>
<!-- [7] SIGNUP -->
<section class="section" id="signup">
<div class="container signup">
<div class="signup__info">
<h2>Запись в лагерь</h2>
<p>Оставьте заявку — администратор перезвонит в течение часа, расскажет о свободных местах и поможет с оплатой.</p>
<p class="signup__note">Заявка ни к чему не обязывает. Оплата — только после подтверждения.</p>
</div>
<form class="form" id="signup-form" novalidate>
<label class="form__field">
<span>Имя ребёнка *</span>
<input type="text" name="child_name" required />
</label>
<label class="form__field">
<span>Возраст ребёнка *</span>
<input type="number" name="child_age" min="5" max="17" required />
</label>
<label class="form__field">
<span>Имя родителя *</span>
<input type="text" name="parent_name" required />
</label>
<label class="form__field">
<span>Телефон *</span>
<input type="tel" name="phone" placeholder="+7 ___ ___ __ __" required />
</label>
<label class="form__field">
<span>Смена *</span>
<select name="shift" id="shift-select" required>
<option value="">— выберите смену —</option>
</select>
</label>
<label class="form__field form__field--wide">
<span>Комментарий</span>
<textarea name="comment" rows="3" placeholder="Особенности, аллергии, пожелания..."></textarea>
</label>
<button type="submit" class="btn btn--primary form__submit">Отправить заявку</button>
<p class="form__success" id="form-success" hidden>
🎉 Заявка принята! Мы позвоним вам в течение часа.
</p>
</form>
</div>
</section>
<!-- [7.5] PAYMENT -->
<section class="section section--gray" id="payment">
<div class="container">
<h2 class="section__title">Оплата</h2>
<p class="section__lead">После подтверждения заявки администратором.</p>
<div class="payment-simple">
<div class="pay-card">
<div class="pay-card__icon">💳</div>
<h3>Kaspi перевод</h3>
<p class="pay-card__big">+7 700 123 45 67</p>
<p>Получатель: Сокол ИП</p>
<p class="pay-card__hint">В комментарии: имя ребёнка и номер смены</p>
</div>
<div class="pay-card">
<div class="pay-card__icon">🏦</div>
<h3>Банковский счёт</h3>
<p>АО «Народный Банк»</p>
<p>ИИК: KZ123456789012345678</p>
<p>БИН: 123456789012</p>
</div>
<div class="pay-card">
<div class="pay-card__icon">📱</div>
<h3>Подтверждение</h3>
<p>Пришлите скриншот оплаты в WhatsApp:</p>
<p class="pay-card__big">
<a href="https://wa.me/77001234567" target="_blank" rel="noopener">+7 700 123 45 67</a>
</p>
</div>
</div>
</div>
</section>
<!-- [8] CONTACTS -->
<section class="section section--gray" id="contacts">
<div class="container contacts">
<h2 class="section__title">Контакты</h2>
<div class="contacts__grid">
<div class="contact-card">
<div class="contact-card__icon">📞</div>
<h3>Телефон</h3>
<a href="tel:+77001234567">+7 700 123 45 67</a>
<p class="contact-card__sub">Ежедневно с 9:00 до 21:00</p>
</div>
<div class="contact-card">
<div class="contact-card__icon">💬</div>
<h3>Мессенджеры</h3>
<a href="https://wa.me/77001234567" target="_blank" rel="noopener">WhatsApp</a>
<a href="https://t.me/sokol_camp" target="_blank" rel="noopener">Telegram</a>
</div>
<div class="contact-card">
<div class="contact-card__icon">📍</div>
<h3>Адрес</h3>
<p>Алматинская обл., Талгарский район, урочище «Сосновый бор»</p>
</div>
<div class="contact-card">
<div class="contact-card__icon">📷</div>
<h3>Инстаграм</h3>
<a href="https://www.instagram.com/doc.sokol_/" target="_blank" rel="noopener">@doc.sokol_</a>
<p class="contact-card__sub">Фото и видео со смен</p>
</div>
</div>
</div>
</section>
<!-- [9] FOOTER -->
<footer class="footer">
<div class="container footer__inner">
<div>
<strong>🦅 Лагерь «Сокол»</strong>
<p>© 2025. Все права защищены.</p>
</div>
<nav class="footer__nav">
<a href="#about">О лагере</a>
<a href="#shifts">Смены</a>
<a href="#signup">Запись</a>
<a href="#contacts">Контакты</a>
</nav>
</div>
</footer>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" hidden>
<button class="lightbox__close" id="lightbox-close" aria-label="Закрыть">×</button>
<div class="lightbox__img" id="lightbox-img"></div>
</div>
<script src="script.js"></script>
</body>
</html>

165
script.js Normal file
View File

@ -0,0 +1,165 @@
// ========== DATA ==========
// Mock shifts (also available in data/shifts.json — used when fetch works).
const MOCK_SHIFTS = [
{ id: 1, name: 'Смена 1 — «Старт лета»', dates: '1 июня — 21 июня 2025', price: 120000, currency: 'тенге', spots_left: 12,
description: 'Спорт, творчество, командные игры. Возраст: 7–12 лет.',
includes: ['3-разовое питание', 'проживание в корпусе', 'все мероприятия'] },
{ id: 2, name: 'Смена 2 — «Приключения»', dates: '25 июня — 15 июля 2025', price: 135000, currency: 'тенге', spots_left: 8,
description: 'Походы, квесты, природа. Возраст: 10–15 лет.',
includes: ['3-разовое питание', 'проживание в корпусе', 'все мероприятия', 'поход выходного дня'] },
{ id: 3, name: 'Смена 3 — «Творческая лаборатория»', dates: '19 июля — 8 августа 2025', price: 130000, currency: 'тенге', spots_left: 15,
description: 'Музыка, театр, рисование, мультипликация. Возраст: 8–14 лет.',
includes: ['3-разовое питание', 'проживание в корпусе', 'мастер-классы', 'итоговый концерт'] },
{ id: 4, name: 'Смена 4 — «Прощание с летом»', dates: '12 августа — 30 августа 2025', price: 125000, currency: 'тенге', spots_left: 20,
description: 'Спорт, дискотеки, костры, кино под открытым небом. Возраст: 9–15 лет.',
includes: ['3-разовое питание', 'проживание в корпусе', 'все мероприятия', 'выпускной'] },
];
// Gallery placeholders (emoji + gradient color pairs).
const GALLERY_ITEMS = [
{ emoji: '🏕️', from: '#FF6B35', to: '#ffa07a', alt: 'Палатки на поляне' },
{ emoji: '🏊', from: '#00AAFF', to: '#7dd3fc', alt: 'Бассейн' },
{ emoji: '⚽', from: '#16a34a', to: '#86efac', alt: 'Футбол' },
{ emoji: '🎨', from: '#a855f7', to: '#d8b4fe', alt: 'Творчество' },
{ emoji: '🔥', from: '#dc2626', to: '#fca5a5', alt: 'Костёр' },
{ emoji: '🌲', from: '#15803d', to: '#86efac', alt: 'Сосновый лес' },
{ emoji: '🥾', from: '#92400e', to: '#fed7aa', alt: 'Поход' },
{ emoji: '🎸', from: '#7c3aed', to: '#c4b5fd', alt: 'Концерт' },
{ emoji: '🏹', from: '#ea580c', to: '#fed7aa', alt: 'Стрельба из лука' },
{ emoji: '🎭', from: '#be185d', to: '#fbcfe8', alt: 'Театр' },
{ emoji: '🚴', from: '#0891b2', to: '#a5f3fc', alt: 'Велопрогулка' },
{ emoji: '⭐', from: '#facc15', to: '#fef08a', alt: 'Вечернее небо' },
{ emoji: '🏐', from: '#2563eb', to: '#bfdbfe', alt: 'Волейбол' },
{ emoji: '🎤', from: '#db2777', to: '#fbcfe8', alt: 'Дискотека' },
{ emoji: '🦅', from: '#475569', to: '#cbd5e1', alt: 'Символ лагеря' },
{ emoji: '🍉', from: '#dc2626', to: '#86efac', alt: 'Полдник' },
];
// ========== HELPERS ==========
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
const fmtPrice = (n) => n.toLocaleString('ru-RU');
async function loadShifts() {
try {
const r = await fetch('data/shifts.json');
if (!r.ok) throw new Error('fetch failed');
return await r.json();
} catch (_) {
// file:// or offline — fall back to embedded mock
return MOCK_SHIFTS;
}
}
// ========== RENDER: SHIFTS ==========
function renderShifts(shifts) {
const grid = $('#shifts-grid');
const select = $('#shift-select');
grid.innerHTML = shifts.map((s) => {
const lowSpots = s.spots_left <= 10;
const includes = s.includes.map((i) => `<li>${i}</li>`).join('');
return `
<article class="shift" data-id="${s.id}">
<h3 class="shift__name">${s.name}</h3>
<div class="shift__dates">📅 ${s.dates}</div>
<p class="shift__desc">${s.description}</p>
<ul class="shift__includes">${includes}</ul>
<div class="shift__price-row">
<div class="shift__price">${fmtPrice(s.price)} <small>${s.currency}</small></div>
<div class="shift__spots ${lowSpots ? 'shift__spots--low' : ''}">
Мест: ${s.spots_left}
</div>
</div>
<div class="shift__buttons">
<button class="btn btn--primary" data-action="signup" data-shift="${s.id}">Выбрать смену</button>
</div>
</article>
`;
}).join('');
// Populate the select for the form
select.innerHTML = '<option value="">— выберите смену —</option>' +
shifts.map((s) => `<option value="${s.id}">${s.name} (${s.dates})</option>`).join('');
// Wire card buttons
grid.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const shiftId = btn.dataset.shift;
if (btn.dataset.action === 'signup') {
select.value = shiftId;
$('#signup').scrollIntoView({ behavior: 'smooth' });
setTimeout(() => $('input[name="child_name"]').focus(), 600);
}
});
}
// ========== RENDER: GALLERY ==========
function renderGallery() {
const grid = $('#gallery-grid');
grid.innerHTML = GALLERY_ITEMS.map((g, i) => `
<div class="gallery__item" data-i="${i}"
style="background: linear-gradient(135deg, ${g.from}, ${g.to});"
aria-label="${g.alt}">
${g.emoji}
</div>
`).join('');
grid.addEventListener('click', (e) => {
const item = e.target.closest('.gallery__item');
if (!item) return;
const g = GALLERY_ITEMS[Number(item.dataset.i)];
const lb = $('#lightbox-img');
lb.style.background = `linear-gradient(135deg, ${g.from}, ${g.to})`;
lb.textContent = g.emoji;
$('#lightbox').hidden = false;
});
}
// ========== LIGHTBOX ==========
function initLightbox() {
const lb = $('#lightbox');
$('#lightbox-close').addEventListener('click', () => { lb.hidden = true; });
lb.addEventListener('click', (e) => { if (e.target === lb) lb.hidden = true; });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') lb.hidden = true; });
}
// ========== BURGER MENU ==========
function initBurger() {
const nav = $('#nav');
const burger = $('#burger');
burger.addEventListener('click', () => nav.classList.toggle('is-open'));
nav.addEventListener('click', (e) => {
if (e.target.tagName === 'A') nav.classList.remove('is-open');
});
}
// ========== FORM ==========
function initForm() {
const form = $('#signup-form');
const success = $('#form-success');
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const data = Object.fromEntries(new FormData(form).entries());
// MVP: log to console (real impl would POST to Formspree).
console.log('Заявка:', data);
success.hidden = false;
form.querySelectorAll('input, select, textarea').forEach((el) => { el.value = ''; });
success.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => { success.hidden = true; }, 8000);
});
}
// ========== INIT ==========
(async function init() {
const shifts = await loadShifts();
renderShifts(shifts);
renderGallery();
initLightbox();
initBurger();
initForm();
})();

306
style.css Normal file
View File

@ -0,0 +1,306 @@
/* ========== RESET / BASE ========== */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
font-weight: 400;
color: #1f2937;
background: #fff;
line-height: 1.6;
}
h1, h2, h3 { margin: 0 0 12px; font-weight: 800; color: #0f172a; line-height: 1.2; }
h2 { font-size: 2rem; }
p { margin: 0 0 12px; }
a { color: #FF6B35; text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; display: block; }
.container { width: 100%; max-width: 1140px; margin: 0 auto; padding: 0 20px; }
:root {
--orange: #FF6B35;
--orange-dark: #e55522;
--blue: #00AAFF;
--gray-bg: #f8fafc;
--border: #e5e7eb;
--shadow: 0 4px 12px rgba(0,0,0,0.08);
--shadow-hover: 0 8px 24px rgba(0,0,0,0.12);
}
/* ========== BUTTONS ========== */
.btn {
display: inline-block;
padding: 14px 28px;
border-radius: 999px;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
border: none;
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
text-align: center;
}
.btn:hover { transform: translateY(-2px); text-decoration: none; box-shadow: var(--shadow-hover); }
.btn--primary { background: var(--orange); color: #fff; }
.btn--primary:hover { background: var(--orange-dark); color: #fff; }
.btn--secondary { background: #fff; color: var(--orange); border: 2px solid var(--orange); }
.btn--secondary:hover { background: var(--orange); color: #fff; }
.btn--small { padding: 10px 20px; font-size: 0.95rem; }
.btn--ghost { background: transparent; color: var(--blue); border: 2px solid var(--blue); }
.btn--ghost:hover { background: var(--blue); color: #fff; }
/* ========== HEADER ========== */
.header {
position: sticky; top: 0; z-index: 50;
background: #fff;
border-bottom: 1px solid var(--border);
}
.header__inner {
display: flex; align-items: center; justify-content: space-between;
height: 70px;
}
.logo { display: flex; align-items: center; gap: 8px; font-weight: 800; font-size: 1.3rem; color: #0f172a; }
.logo:hover { text-decoration: none; }
.logo__mark { font-size: 1.6rem; }
.nav { display: flex; gap: 24px; }
.nav a { color: #334155; font-weight: 600; }
.nav a:hover { color: var(--orange); text-decoration: none; }
.nav__cta { margin-left: 16px; }
.burger { display: none; background: none; border: none; cursor: pointer; padding: 8px; }
.burger span { display: block; width: 26px; height: 3px; background: #0f172a; margin: 5px 0; border-radius: 2px; transition: 0.3s; }
/* ========== HERO ========== */
.hero { position: relative; min-height: 80vh; display: flex; align-items: center; color: #fff; overflow: hidden; }
.hero__bg {
position: absolute; inset: 0;
background:
linear-gradient(135deg, rgba(255,107,53,0.85) 0%, rgba(0,170,255,0.75) 100%),
radial-gradient(circle at 30% 40%, #ffd166 0%, transparent 50%),
linear-gradient(180deg, #4a90e2 0%, #2c5282 100%);
z-index: -1;
}
.hero__bg::after {
content: ''; position: absolute; inset: 0;
background-image:
radial-gradient(circle at 20% 80%, rgba(255,255,255,0.1) 0%, transparent 30%),
radial-gradient(circle at 80% 20%, rgba(255,255,255,0.15) 0%, transparent 40%);
}
.hero__content { position: relative; padding: 80px 20px; }
.hero__title { font-size: 3rem; color: #fff; max-width: 700px; text-shadow: 0 2px 12px rgba(0,0,0,0.3); }
.hero__subtitle { font-size: 1.25rem; max-width: 600px; text-shadow: 0 1px 6px rgba(0,0,0,0.3); }
.hero__buttons { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 24px; }
/* ========== SECTION ========== */
.section { padding: 80px 0; }
.section--gray { background: var(--gray-bg); }
.section__title { text-align: center; }
.section__lead { text-align: center; color: #64748b; margin-bottom: 40px; font-size: 1.1rem; }
/* ========== ABOUT ========== */
.about { display: grid; grid-template-columns: 1.2fr 1fr; gap: 48px; align-items: center; }
.about__points { list-style: none; padding: 0; margin: 20px 0 0; }
.about__points li { padding: 8px 0; font-size: 1.05rem; }
.about__photo {
height: 360px; border-radius: 20px;
background:
linear-gradient(135deg, rgba(255,107,53,0.3), rgba(0,170,255,0.3)),
repeating-linear-gradient(45deg, #fef3c7, #fef3c7 20px, #fde68a 20px, #fde68a 40px);
position: relative;
box-shadow: var(--shadow);
}
.about__photo::before {
content: '👨‍👩‍👧‍👦';
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
font-size: 6rem;
}
/* ========== SHIFTS ========== */
.shifts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
.shift {
background: #fff;
border-radius: 16px;
padding: 28px;
box-shadow: var(--shadow);
display: flex; flex-direction: column;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-top: 4px solid var(--orange);
}
.shift:nth-child(even) { border-top-color: var(--blue); }
.shift:hover { transform: translateY(-4px); box-shadow: var(--shadow-hover); }
.shift__name { font-size: 1.25rem; }
.shift__dates { color: #64748b; font-weight: 600; margin-bottom: 12px; }
.shift__desc { flex-grow: 0; color: #334155; }
.shift__includes { list-style: none; padding: 0; margin: 12px 0; font-size: 0.95rem; color: #475569; }
.shift__includes li { padding: 3px 0; }
.shift__includes li::before { content: '✓ '; color: var(--orange); font-weight: 700; }
.shift__price-row {
display: flex; align-items: baseline; justify-content: space-between;
margin: 16px 0 12px; padding-top: 16px; border-top: 1px solid var(--border);
}
.shift__price { font-size: 1.6rem; font-weight: 800; color: #0f172a; }
.shift__price small { font-size: 0.9rem; color: #64748b; font-weight: 600; }
.shift__spots { font-size: 0.9rem; color: #16a34a; font-weight: 700; }
.shift__spots--low { color: #dc2626; }
.shift__buttons { display: flex; gap: 8px; margin-top: auto; }
.shift__buttons .btn { flex: 1; padding: 12px 16px; font-size: 0.95rem; }
/* ========== SCHEDULE ========== */
.schedule {
width: 100%; max-width: 700px; margin: 0 auto;
border-collapse: collapse;
background: #fff; border-radius: 16px; overflow: hidden;
box-shadow: var(--shadow);
}
.schedule th, .schedule td { padding: 14px 20px; text-align: left; border-bottom: 1px solid var(--border); }
.schedule th { background: var(--orange); color: #fff; font-weight: 700; }
.schedule tbody tr:last-child td { border-bottom: none; }
.schedule tbody tr:hover { background: #fff7ed; }
.schedule td:first-child { font-weight: 700; color: var(--orange); width: 100px; }
/* ========== GALLERY ========== */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
.gallery__item {
aspect-ratio: 1 / 1;
border-radius: 12px;
cursor: pointer;
overflow: hidden;
position: relative;
transition: transform 0.2s ease, box-shadow 0.2s ease;
display: flex; align-items: center; justify-content: center;
font-size: 3rem;
}
.gallery__item:hover { transform: scale(1.03); box-shadow: var(--shadow-hover); }
/* ========== SIGNUP / FORM ========== */
.signup { display: grid; grid-template-columns: 1fr 1.3fr; gap: 48px; align-items: start; }
.signup__info h2 { text-align: left; }
.signup__note { color: #64748b; font-size: 0.95rem; font-style: italic; }
.form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
background: #fff;
padding: 32px;
border-radius: 16px;
box-shadow: var(--shadow);
}
.form__field { display: flex; flex-direction: column; gap: 6px; font-weight: 600; font-size: 0.95rem; color: #334155; }
.form__field--wide { grid-column: 1 / -1; }
.form__field input, .form__field select, .form__field textarea {
padding: 12px 14px;
border: 2px solid var(--border);
border-radius: 10px;
font-family: inherit;
font-size: 1rem;
font-weight: 400;
transition: border-color 0.15s ease;
background: #fff;
}
.form__field input:focus, .form__field select:focus, .form__field textarea:focus {
outline: none; border-color: var(--orange);
}
.form__field input:invalid:not(:placeholder-shown) { border-color: #dc2626; }
.form__submit { grid-column: 1 / -1; }
.form__success {
grid-column: 1 / -1;
background: #dcfce7;
color: #166534;
padding: 14px 18px;
border-radius: 10px;
font-weight: 700;
text-align: center;
margin: 0;
}
/* ========== PAYMENT ========== */
.payment-simple {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
}
.pay-card {
background: #fff;
padding: 28px 24px;
border-radius: 14px;
text-align: center;
box-shadow: var(--shadow);
}
.pay-card__icon { font-size: 2.5rem; margin-bottom: 8px; }
.pay-card h3 { font-size: 1.15rem; }
.pay-card p { margin: 6px 0; color: #334155; }
.pay-card__big { font-size: 1.25rem; font-weight: 700; color: #0f172a; }
.pay-card__hint { font-size: 0.9rem; color: #64748b; font-style: italic; }
.pay-card a { font-weight: 700; }
/* ========== CONTACTS ========== */
.contacts__grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px;
margin-top: 32px;
}
.contact-card {
background: #fff;
padding: 24px;
border-radius: 14px;
text-align: center;
box-shadow: var(--shadow);
}
.contact-card__icon { font-size: 2.5rem; margin-bottom: 8px; }
.contact-card h3 { font-size: 1.1rem; }
.contact-card a { display: block; font-weight: 700; padding: 4px 0; font-size: 1.05rem; }
.contact-card__sub { font-size: 0.9rem; color: #64748b; margin-top: 6px; }
/* ========== FOOTER ========== */
.footer { background: #0f172a; color: #cbd5e1; padding: 32px 0; }
.footer__inner { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; }
.footer strong { color: #fff; font-size: 1.1rem; }
.footer p { margin: 4px 0 0; font-size: 0.9rem; }
.footer__nav { display: flex; gap: 20px; flex-wrap: wrap; }
.footer__nav a { color: #cbd5e1; font-weight: 600; }
.footer__nav a:hover { color: var(--orange); }
/* ========== LIGHTBOX ========== */
.lightbox[hidden] { display: none !important; }
.lightbox {
position: fixed; inset: 0; z-index: 100;
background: rgba(0,0,0,0.92);
display: flex; align-items: center; justify-content: center;
padding: 20px;
}
.lightbox__img {
width: min(80vw, 600px); height: min(80vh, 600px);
border-radius: 16px;
display: flex; align-items: center; justify-content: center;
font-size: 12rem;
}
.lightbox__close {
position: absolute; top: 16px; right: 24px;
background: none; border: none; color: #fff; font-size: 3rem; cursor: pointer; line-height: 1;
}
/* ========== MOBILE ========== */
@media (max-width: 860px) {
.nav, .nav__cta { display: none; }
.nav.is-open {
display: flex; flex-direction: column;
position: absolute; top: 70px; left: 0; right: 0;
background: #fff; padding: 20px; gap: 16px;
box-shadow: var(--shadow);
border-top: 1px solid var(--border);
}
.burger { display: block; }
.hero__title { font-size: 2.2rem; }
.hero__subtitle { font-size: 1.1rem; }
.about, .signup { grid-template-columns: 1fr; }
.form { grid-template-columns: 1fr; padding: 24px; }
.section { padding: 56px 0; }
h2 { font-size: 1.6rem; }
}