diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bfa8ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +node_modules/ +.vibe42-run.log +.vibe42-run.pid +*.local diff --git a/AGENTS.md b/AGENTS.md index 331fbab..e0e7bf6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ Workspace юзера `temp`. Это **учебная среда**, где обы 1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/temp//`, бот/приложение — запущены через `run` с живой ссылкой, 2. ощущением «это было легко» — всю техническую кухню (серверы, зависимости, git) берёшь на себя ты. -Юзер не разработчик. Ему важен **работающий результат**, а не код. +Юзер не разработчик. Ему важен **работающий результат**, и оптимизированный и эффективный код. --- @@ -23,7 +23,7 @@ Workspace юзера `temp`. Это **учебная среда**, где обы | Что хочет юзер | Стек (ЖЁСТКО) | Рецепт | |----------------|---------------|--------| -| Веб сайт фронтенд на javascript | **Статика:** `index.html` + `style.css` + `script.js` + еще файлы для фронтенда, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. Tailwind или bootstrap — только CDN. | старт с `design.md`, публикация в `pages` | +| Веб сайт фронтенд с API на javascript | **Статика:** `index.html` + `style.css` + `script.js` + еще файлы для фронтенда, ванильный JS. БЕЗ сборки, БЕЗ React/Vue/Vite, БЕЗ npm. Tailwind или bootstrap — только CDN. | старт с `design.md`, публикация в `pages` | | Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже | | Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже | | Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже | @@ -464,7 +464,7 @@ if (req.method === "POST" && req.url === "/api/upload") { | «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` | | «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp | | «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` | -| «сайт-визитка / промо / портфолио» | статичный лендинг + публикация в `pages` — тут это лучший инструмент, а не компромисс | +| «сайт-визитка / промо / портфолио» | динамичный лендинг с красивыми и крутыми анимациями + публикация в `pages` — тут это лучший инструмент, а не компромисс | | «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет | Правила ведения: @@ -766,11 +766,11 @@ async function inbox() { 5. Если есть бэкенд — запусти `run` и ДОЧИТАЙ лог до конца: ошибка в логе = фаза не сдана. Если только статика — посмотри вкладку Предпросмотр. 6. Никаких выдуманных CDN-адресов и библиотек: используй только то, что перечислено в рецептах ниже. -## 📁 ТЫ УЖЕ ВНУТРИ ПАПКИ ПРОЕКТА — собирай сайт ЗДЕСЬ +## 📁 ТЫ УЖЕ ВНУТРИ ПАПКИ ПРОЕКТА — собирай проект ЗДЕСЬ Твоя рабочая директория (cwd) — это **папка проекта юзера** (`.../users///`). Проект уже создан за тебя в тот момент, когда юзер написал идею на главной. Проверь: `pwd` — папка проекта, `ls` — там лежат AGENTS.md/design.md/README.md. -**Собирай сайт ПРЯМО В ТЕКУЩЕЙ папке:** создавай `index.html`, `style.css`, `script.js` здесь же, в cwd. +**Собирай сайт ПРЯМО В ТЕКУЩЕЙ папке:** создавай `index.html`, `style.css`, `script.js` и другие необходимые файлы здесь же, в cwd. ❌ **НЕ запускай `./new-project`.** ❌ **НЕ делай `cd` в другие папки / в корень воркспейса.** Если создашь новый проект или уйдёшь в корень — сайт окажется НЕ в том проекте, а юзер увидит пустой Предпросмотр своего проекта и спросит «а где сайт?». Именно так это ломается. @@ -808,8 +808,8 @@ git push origin HEAD:pages - **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f` -### ⚠️ Проекты с бэкендом (server.js / run) и статика — НЕ путай юзера -Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО статические файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому: +### ⚠️ Проекты с бэкендом (server.js / run) — НЕ путай юзера +Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому: - Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия. - Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть. - run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`. diff --git a/API.md b/API.md new file mode 100644 index 0000000..e7f3931 --- /dev/null +++ b/API.md @@ -0,0 +1,318 @@ +# API Документация — Apple Style App + +## 📚 Обзор + +Бэкенд построен на **Node.js http** (без зависимостей). Данные хранятся в `data.json`. + +--- + +## 🔌 PUBLIC API (без авторизации) + +### Получить все продукты +``` +GET /api/products +``` + +**Ответ:** +```json +{ + "ok": true, + "data": [ + { + "id": 1, + "name": "Vision Pro", + "tagline": "Пространственный компьютер здесь", + "description": "...", + "price": 3499, + "image": "https://...", + "features": ["M2", "R1"], + "category": "premium", + "order": 1 + } + ] +} +``` + +--- + +### Получить продукт по ID +``` +GET /api/products/:id +``` + +**Ответ:** +```json +{ + "ok": true, + "data": { /* объект продукта */ } +} +``` + +--- + +### Получить активные объявления +``` +GET /api/announcements +``` + +**Ответ:** +```json +{ + "ok": true, + "data": [ + { + "id": 1, + "title": "Apple Event 2026", + "date": "2026-09-15", + "content": "Смотрите презентацию...", + "active": true + } + ] +} +``` + +--- + +### Получить настройки сайта +``` +GET /api/settings +``` + +**Ответ:** +```json +{ + "ok": true, + "data": { + "heroTitle": "Добро пожаловать в будущее", + "heroSubtitle": "Инновации, которые меняют мир", + "theme": "dark" + } +} +``` + +--- + +## 🔐 ADMIN API (для редактирования) + +### Добавить продукт +``` +POST /api/admin/products +Content-Type: application/json + +{ + "name": "iPhone 18 Pro", + "tagline": "Ещё мощнее", + "description": "Описание...", + "price": 999, + "category": "phones", + "image": "https://...", + "features": ["A18 чип", "Face ID", "5G"] +} +``` + +**Ответ:** +```json +{ + "ok": true, + "data": { /* созданный продукт с id и order */ } +} +``` + +--- + +### Обновить продукт +``` +PUT /api/admin/products/:id +Content-Type: application/json + +{ + "name": "Новое название", + "price": 1099 +} +``` + +--- + +### Удалить продукт +``` +DELETE /api/admin/products/:id +``` + +**Ответ:** +```json +{ + "ok": true, + "deleted": 123 +} +``` + +--- + +### Добавить объявление +``` +POST /api/admin/announcements +Content-Type: application/json + +{ + "title": "Специальное предложение", + "date": "2026-12-01", + "content": "Скидки до 30%", + "active": true +} +``` + +--- + +### Обновить объявление +``` +PUT /api/admin/announcements/:id +Content-Type: application/json + +{ + "title": "Обновлённый заголовок", + "active": false +} +``` + +--- + +### Удалить объявление +``` +DELETE /api/admin/announcements/:id +``` + +--- + +### Обновить настройки сайта +``` +PUT /api/admin/settings +Content-Type: application/json + +{ + "heroTitle": "Новый заголовок", + "heroSubtitle": "Новый подзаголовок", + "theme": "light" +} +``` + +--- + +## 🗂 Структура данных (data.json) + +```json +{ + "products": [ + { + "id": number, + "name": string, + "tagline": string, + "description": string, + "price": number, + "image": string (URL), + "features": string[], + "category": string, + "order": number + } + ], + "announcements": [ + { + "id": number, + "title": string, + "date": string (ISO date), + "content": string, + "active": boolean + } + ], + "siteSettings": { + "heroTitle": string, + "heroSubtitle": string, + "theme": string ("dark" | "light") + } +} +``` + +--- + +## 🚀 Архитектура сервера + +``` +server.js +├── Routes (API) +│ ├── GET /api/products → список продуктов +│ ├── GET /api/products/:id → продукт по ID +│ ├── GET /api/announcements → активные объявления +│ ├── GET /api/settings → настройки сайта +│ │ +│ └── ADMIN (CRUD) +│ ├── POST /api/admin/products +│ ├── PUT /api/admin/products/:id +│ ├── DELETE /api/admin/products/:id +│ ├── POST /api/admin/announcements +│ ├── PUT /api/admin/announcements/:id +│ ├── DELETE /api/admin/announcements/:id +│ └── PUT /api/admin/settings +│ +└── Static Files + ├── / → index.html + ├── /admin.html + ├── /style.css + ├── /app.js + └── /admin.js +``` + +--- + +## 📝 Примеры использования (Frontend) + +### Загрузка продуктов +```javascript +async function loadProducts() { + const res = await fetch('/api/products'); + const json = await res.json(); + if (json.ok) { + console.log(json.data); // массив продуктов + } +} +``` + +### Добавление продукта (из админки) +```javascript +async function addProduct(data) { + const res = await fetch('/api/admin/products', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const json = await res.json(); + return json.ok; +} +``` + +--- + +## 🔒 Безопасность (TODO для продакшена) + +Сейчас API открыт. Для продакшена добавить: +- JWT или session аутентификацию для `/api/admin/*` +- Валидацию входных данных +- Rate limiting +- CORS только для доверенных доменов + +--- + +## 🧪 Тестирование + +```bash +# Получить все продукты +curl http://localhost:3000/api/products + +# Добавить продукт +curl -X POST http://localhost:3000/api/admin/products \ + -H "Content-Type: application/json" \ + -d '{"name":"Test","tagline":"Test","price":99,"category":"test","features":[]}' + +# Обновить настройки +curl -X PUT http://localhost:3000/api/admin/settings \ + -H "Content-Type: application/json" \ + -d '{"heroTitle":"Hello"}' +``` diff --git a/admin.css b/admin.css new file mode 100644 index 0000000..073bf91 --- /dev/null +++ b/admin.css @@ -0,0 +1,243 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif; + background: #0a0a0a; + color: #f5f5f7; + line-height: 1.5; +} + +.admin-nav { + position: sticky; + top: 0; + background: rgba(29, 29, 31, 0.95); + backdrop-filter: blur(20px); + border-bottom: 1px solid rgba(255,255,255,0.1); + z-index: 100; +} + +.admin-nav-container { + max-width: 1200px; + margin: 0 auto; + padding: 16px 24px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.admin-logo { + font-size: 20px; + font-weight: 600; + color: #f5f5f7; + text-decoration: none; +} + +.btn-back { + color: #2997ff; + text-decoration: none; + font-size: 14px; + font-weight: 500; + transition: opacity 0.3s; +} + +.btn-back:hover { + opacity: 0.8; +} + +.admin-container { + max-width: 1200px; + margin: 0 auto; + padding: 40px 24px; +} + +.admin-section { + margin-bottom: 60px; +} + +.admin-section h2 { + font-size: 28px; + font-weight: 600; + margin-bottom: 24px; + letter-spacing: -0.01em; +} + +.admin-card { + background: #1d1d1f; + border-radius: 16px; + padding: 32px; + margin-bottom: 24px; + border: 1px solid rgba(255,255,255,0.08); +} + +.admin-card h3 { + font-size: 20px; + margin-bottom: 24px; + font-weight: 600; +} + +.admin-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.list-item { + background: #2d2d30; + border-radius: 12px; + padding: 24px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + flex-wrap: wrap; +} + +.list-item-info { + flex: 1; + min-width: 200px; +} + +.list-item-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 6px; +} + +.list-item-meta { + font-size: 14px; + color: #86868b; +} + +.list-item-actions { + display: flex; + gap: 12px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + font-size: 14px; + font-weight: 500; + margin-bottom: 8px; + color: #86868b; +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 12px 16px; + background: #0a0a0a; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 10px; + color: #f5f5f7; + font-size: 16px; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: #2997ff; +} + +.form-group textarea { + resize: vertical; + min-height: 80px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +.form-actions { + display: flex; + gap: 12px; + margin-top: 24px; +} + +.btn { + padding: 12px 24px; + border-radius: 980px; + font-size: 15px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.3s; +} + +.btn-primary { + background: #2997ff; + color: white; +} + +.btn-primary:hover { + background: #147ce5; +} + +.btn-secondary { + background: transparent; + color: #2997ff; + border: 2px solid #2997ff; +} + +.btn-secondary:hover { + background: #2997ff; + color: white; +} + +.btn-danger { + background: #ff3b30; + color: white; +} + +.btn-danger:hover { + background: #d63027; +} + +.btn-small { + padding: 8px 16px; + font-size: 13px; +} + +.status-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 980px; + font-size: 12px; + font-weight: 500; + background: rgba(41, 151, 255, 0.15); + color: #2997ff; +} + +.status-badge.inactive { + background: rgba(134, 134, 139, 0.15); + color: #86868b; +} + +@media (max-width: 768px) { + .form-row { + grid-template-columns: 1fr; + } + + .list-item { + flex-direction: column; + align-items: flex-start; + } + + .list-item-actions { + width: 100%; + justify-content: flex-end; + } +} diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..d008309 --- /dev/null +++ b/admin.html @@ -0,0 +1,138 @@ + + + + + + Админ-панель — Apple Style + + + + + +
+ +
+

🏠 Настройки сайта

+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + +
+

📱 Продукты

+
+ +
+ + + +
+ +
+
+ + +
+

📢 Объявления

+
+ +
+ + + +
+ +
+
+
+ + + + diff --git a/admin.js b/admin.js new file mode 100644 index 0000000..60e6a42 --- /dev/null +++ b/admin.js @@ -0,0 +1,267 @@ +// =================================== +// ADMIN PANEL - JAVASCRIPT +// =================================== + +const API = { + products: '/api/products', + adminProducts: '/api/admin/products', + announcements: '/api/announcements', + adminAnnouncements: '/api/admin/announcements', + settings: '/api/settings', + adminSettings: '/api/admin/settings' +}; + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + loadSettings(); + loadProducts(); + loadAnnouncements(); + setupForms(); +}); + +// Setup forms +function setupForms() { + document.getElementById('settingsForm').addEventListener('submit', saveSettings); + document.getElementById('productForm').addEventListener('submit', addProduct); + document.getElementById('announcementForm').addEventListener('submit', addAnnouncement); +} + +// ============ SETTINGS ============ +async function loadSettings() { + try { + const res = await fetch(API.settings); + const json = await res.json(); + if (json.ok && json.data) { + document.getElementById('heroTitle').value = json.data.heroTitle || ''; + document.getElementById('heroSubtitle').value = json.data.heroSubtitle || ''; + document.getElementById('theme').value = json.data.theme || 'dark'; + } + } catch (e) { + console.error('Failed to load settings:', e); + } +} + +async function saveSettings(e) { + e.preventDefault(); + const data = { + heroTitle: document.getElementById('heroTitle').value, + heroSubtitle: document.getElementById('heroSubtitle').value, + theme: document.getElementById('theme').value + }; + + try { + const res = await fetch(API.adminSettings, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const json = await res.json(); + if (json.ok) { + alert('Настройки сохранены! ✅'); + } else { + alert('Ошибка: ' + json.error); + } + } catch (e) { + alert('Ошибка сохранения: ' + e.message); + } +} + +// ============ PRODUCTS ============ +async function loadProducts() { + try { + const res = await fetch(API.products); + const json = await res.json(); + const list = document.getElementById('productsList'); + + if (!json.ok || !json.data || json.data.length === 0) { + list.innerHTML = '

Нет продуктов

'; + return; + } + + list.innerHTML = json.data.map(p => ` +
+
+
${p.name}
+
+ ${p.tagline} • $${p.price} • + ${p.category} +
+
+
+ + +
+
+ `).join(''); + } catch (e) { + console.error('Failed to load products:', e); + } +} + +function showAddProductForm() { + document.getElementById('addProductForm').style.display = 'block'; + document.getElementById('productForm').reset(); +} + +function hideAddProductForm() { + document.getElementById('addProductForm').style.display = 'none'; +} + +async function addProduct(e) { + e.preventDefault(); + + const features = document.getElementById('productFeatures').value + .split(',') + .map(f => f.trim()) + .filter(f => f); + + const data = { + name: document.getElementById('productName').value, + tagline: document.getElementById('productTagline').value, + description: document.getElementById('productDescription').value, + price: parseFloat(document.getElementById('productPrice').value), + category: document.getElementById('productCategory').value, + image: document.getElementById('productImage').value || 'https://picsum.photos/800/600?random=' + Date.now(), + features: features + }; + + try { + const res = await fetch(API.adminProducts, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const json = await res.json(); + + if (json.ok) { + alert('Продукт добавлен! ✅'); + hideAddProductForm(); + loadProducts(); + } else { + alert('Ошибка: ' + json.error); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} + +async function editProduct(id) { + const res = await fetch(`${API.products}/${id}`); + const json = await res.json(); + + if (!json.ok) return alert('Продукт не найден'); + + const p = json.data; + if (confirm(`Редактировать "${p.name}"?\n\n(В полной версии откроется форма редактирования)`)) { + // В полной версии здесь открывается форма с заполненными данными + window.location.href = `edit-product.html?id=${id}`; + } +} + +async function deleteProduct(id) { + if (!confirm('Вы уверены? Это действие нельзя отменить.')) return; + + try { + const res = await fetch(`${API.adminProducts}/${id}`, { method: 'DELETE' }); + const json = await res.json(); + + if (json.ok) { + alert('Продукт удалён'); + loadProducts(); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} + +// ============ ANNOUNCEMENTS ============ +async function loadAnnouncements() { + try { + const res = await fetch(API.announcements); + const json = await res.json(); + const list = document.getElementById('announcementsList'); + + if (!json.ok || !json.data || json.data.length === 0) { + list.innerHTML = '

Нет объявлений

'; + return; + } + + list.innerHTML = json.data.map(a => ` +
+
+
${a.title}
+
+ ${a.content} • ${a.date} • + ${a.active ? 'Активно' : 'Скрыто'} +
+
+
+ + +
+
+ `).join(''); + } catch (e) { + console.error('Failed to load announcements:', e); + } +} + +function showAddAnnouncementForm() { + document.getElementById('addAnnouncementForm').style.display = 'block'; + document.getElementById('announcementForm').reset(); +} + +function hideAddAnnouncementForm() { + document.getElementById('addAnnouncementForm').style.display = 'none'; +} + +async function addAnnouncement(e) { + e.preventDefault(); + + const data = { + title: document.getElementById('announcementTitle').value, + date: document.getElementById('announcementDate').value, + content: document.getElementById('announcementContent').value + }; + + try { + const res = await fetch(API.adminAnnouncements, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + const json = await res.json(); + + if (json.ok) { + alert('Объявление добавлено! ✅'); + hideAddAnnouncementForm(); + loadAnnouncements(); + } else { + alert('Ошибка: ' + json.error); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} + +async function editAnnouncement(id) { + if (confirm(`Редактировать объявление #${id}?\n\n(В полной версии откроется форма)`)) { + window.location.href = `edit-announcement.html?id=${id}`; + } +} + +async function deleteAnnouncement(id) { + if (!confirm('Удалить это объявление?')) return; + + try { + const res = await fetch(`${API.adminAnnouncements}/${id}`, { method: 'DELETE' }); + const json = await res.json(); + + if (json.ok) { + alert('Объявление удалено'); + loadAnnouncements(); + } + } catch (e) { + alert('Ошибка: ' + e.message); + } +} diff --git a/app.js b/app.js new file mode 100644 index 0000000..5c7170d --- /dev/null +++ b/app.js @@ -0,0 +1,166 @@ +// =================================== +// APPLE-STYLE APP - FRONTEND +// =================================== + +const API = { + products: '/api/products', + announcements: '/api/announcements', + settings: '/api/settings' +}; + +// Load settings and content +async function init() { + await loadSettings(); + await loadAnnouncements(); + await loadProducts(); + setupScrollEffects(); + setupRevealAnimations(); +} + +// Load site settings +async function loadSettings() { + try { + const res = await fetch(API.settings); + const json = await res.json(); + if (json.ok && json.data) { + document.getElementById('heroTitle').textContent = json.data.heroTitle || 'Добро пожаловать'; + document.getElementById('heroSubtitle').textContent = json.data.heroSubtitle || 'Инновации здесь'; + } + } catch (e) { + console.error('Failed to load settings:', e); + } +} + +// Load announcements +async function loadAnnouncements() { + try { + const res = await fetch(API.announcements); + const json = await res.json(); + const bar = document.getElementById('announcementBar'); + if (json.ok && json.data.length > 0) { + const ann = json.data[0]; + bar.innerHTML = `${ann.title} — ${ann.content} Подробнее →`; + } else { + bar.classList.add('hidden'); + } + } catch (e) { + console.error('Failed to load announcements:', e); + document.getElementById('announcementBar').classList.add('hidden'); + } +} + +// Load products +async function loadProducts() { + try { + const res = await fetch(API.products); + const json = await res.json(); + const container = document.getElementById('productsContainer'); + if (!json.ok || !json.data) { + container.innerHTML = '

Нет продуктов

'; + return; + } + + container.innerHTML = json.data.map((p, i) => ` +
+ ${p.name} +
+

${p.name}

+

${p.tagline}

+

от ${p.price} $

+
+ ${p.features.map(f => `${f}`).join('')} +
+
+
+ `).join(''); + + // Trigger visibility check after load + setTimeout(checkProductVisibility, 100); + } catch (e) { + console.error('Failed to load products:', e); + document.getElementById('productsContainer').innerHTML = '

Ошибка загрузки

'; + } +} + +// Scroll effects for navigation +function setupScrollEffects() { + const nav = document.getElementById('nav'); + const heroBg = document.getElementById('heroBg'); + const featuresBg = document.getElementById('featuresBg'); + + window.addEventListener('scroll', () => { + const scrolled = window.pageYOffset; + + // Nav background + if (scrolled > 100) { + nav.classList.add('scrolled'); + } else { + nav.classList.remove('scrolled'); + } + + // Hero parallax + if (scrolled < window.innerHeight) { + heroBg.style.transform = `translateY(${scrolled * 0.5}px) scale(${1 + scrolled * 0.0008})`; + } + + // Features parallax + const featuresSection = document.getElementById('features'); + if (featuresSection) { + const rect = featuresSection.getBoundingClientRect(); + if (rect.top < window.innerHeight && rect.bottom > 0) { + const offset = (window.innerHeight - rect.top) * 0.2; + featuresBg.style.transform = `translateY(${offset}px)`; + } + } + }); +} + +// Reveal animations on scroll +function setupRevealAnimations() { + const reveals = document.querySelectorAll('.reveal'); + + const checkReveal = () => { + reveals.forEach(el => { + const top = el.getBoundingClientRect().top; + const trigger = window.innerHeight * 0.85; + if (top < trigger) { + el.classList.add('active'); + } + }); + }; + + window.addEventListener('scroll', checkReveal); + checkReveal(); +} + +// Check product cards visibility +function checkProductVisibility() { + const cards = document.querySelectorAll('.product-card'); + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + } + }); + }, { threshold: 0.1 }); + + cards.forEach(card => observer.observe(card)); +} + +// Smooth scroll for anchor links +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + const href = this.getAttribute('href'); + if (href !== '#') { + e.preventDefault(); + const target = document.querySelector(href); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + } + }); + }); + + init(); +}); diff --git a/data.json b/data.json new file mode 100644 index 0000000..11c67f4 --- /dev/null +++ b/data.json @@ -0,0 +1,62 @@ +{ + "products": [ + { + "id": 1, + "name": "Vision Pro", + "tagline": "Пространственный компьютер здесь", + "description": "Революционный интерфейс между человеком и машиной. Погрузитесь в мир цифровых возможностей.", + "price": 3499, + "image": "https://picsum.photos/800/600?random=1", + "features": ["M2 чип", "R1 сопроцессор", "23 млн пикселей", "visionOS"], + "category": "premium", + "order": 1 + }, + { + "id": 2, + "name": "AirPods Max", + "tagline": "Звук в новом измерении", + "description": "Высокая точность звука. Активное шумоподавление. Пространственное аудио.", + "price": 549, + "image": "https://picsum.photos/800/600?random=2", + "features": ["ANC", "40ч работы", "Пространственное аудио", "H1 чип"], + "category": "audio", + "order": 2 + }, + { + "id": 3, + "name": "Apple Watch Ultra", + "tagline": "Приключения ждут", + "description": "Самые прочные и функциональные часы для экстремальных условий.", + "price": 799, + "image": "https://picsum.photos/800/600?random=3", + "features": ["49мм титан", "36ч работы", "GPS", "100м водолазные"], + "category": "wearables", + "order": 3 + }, + { + "id": 4, + "name": "MacBook Pro 16", + "tagline": "Мощность профессионала", + "description": "Чип M3 Max. Невероятная производительность для самых требовательных задач.", + "price": 2499, + "image": "https://picsum.photos/800/600?random=4", + "features": ["M3 Max", "128GB RAM", "8TB SSD", "22ч работы"], + "category": "computers", + "order": 4 + } + ], + "announcements": [ + { + "id": 1, + "title": "Apple Event 2026", + "date": "2026-09-15", + "content": "Смотрите презентацию новых продуктов в прямом эфире", + "active": true + } + ], + "siteSettings": { + "heroTitle": "Добро пожаловать в будущее", + "heroSubtitle": "Инновации, которые меняют мир", + "theme": "dark" + } +} diff --git a/index.html b/index.html index 5025d0d..6e5870c 100644 --- a/index.html +++ b/index.html @@ -3,668 +3,171 @@ - Исландия — Земля льда и пламени - + Apple Style — Будущее здесь + - -