Full Apple-style app with backend API, admin panel, scroll animations
This commit is contained in:
parent
d1848333cc
commit
9ae6383d9d
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.env
|
||||||
|
node_modules/
|
||||||
|
.vibe42-run.log
|
||||||
|
.vibe42-run.pid
|
||||||
|
*.local
|
||||||
14
AGENTS.md
14
AGENTS.md
@ -11,7 +11,7 @@ Workspace юзера `temp`. Это **учебная среда**, где обы
|
|||||||
1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/temp/<repo>/`, бот/приложение — запущены через `run` с живой ссылкой,
|
1. **работающим воплощением ЕГО идеи**: сайт — опубликован на `https://pages.git.vibe42.kz/temp/<repo>/`, бот/приложение — запущены через `run` с живой ссылкой,
|
||||||
2. ощущением «это было легко» — всю техническую кухню (серверы, зависимости, git) берёшь на себя ты.
|
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 ниже |
|
| Telegram-бот | **Node.js (CommonJS) + grammY** (предустановлен), long-polling | Рецепт T ниже |
|
||||||
| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже |
|
| Приложение с сервером: форма→сохраняет, API, дашборд с данными, счётчик, запись на время | **Node.js (CommonJS), сервер на `node:http` БЕЗ зависимостей + хранение в `data.json`** | Рецепт B ниже |
|
||||||
| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже |
|
| Нужен ИИ внутри (умный бот, генерация текста, ответы) | тот же Node-скелет + **`fetch` к `process.env.AI_BASE_URL`** (без SDK) | блок «ИИ» ниже |
|
||||||
@ -464,7 +464,7 @@ if (req.method === "POST" && req.url === "/api/upload") {
|
|||||||
| «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` |
|
| «приложение для записи» | реальное приложение с сервером: форма → бэкенд хранит записи (файл/JSON) → `run` |
|
||||||
| «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp |
|
| «магазин с корзиной» | рабочий прототип: каталог + корзина на бэкенде; оплату (это внешние договоры) пока замени кнопкой «оформить» → WhatsApp |
|
||||||
| «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` |
|
| «блог» | статика (pages) — если без админки; с админкой — бэкенд через `run` |
|
||||||
| «сайт-визитка / промо / портфолио» | статичный лендинг + публикация в `pages` — тут это лучший инструмент, а не компромисс |
|
| «сайт-визитка / промо / портфолио» | динамичный лендинг с красивыми и крутыми анимациями + публикация в `pages` — тут это лучший инструмент, а не компромисс |
|
||||||
| «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет |
|
| «соцсеть» | честно скажи, что за один заход не получится, и предложи первый работающий кусок (профиль + лента на бэкенде) — пусть юзер выберет |
|
||||||
|
|
||||||
Правила ведения:
|
Правила ведения:
|
||||||
@ -766,11 +766,11 @@ async function inbox() {
|
|||||||
5. Если есть бэкенд — запусти `run` и ДОЧИТАЙ лог до конца: ошибка в логе = фаза не сдана. Если только статика — посмотри вкладку Предпросмотр.
|
5. Если есть бэкенд — запусти `run` и ДОЧИТАЙ лог до конца: ошибка в логе = фаза не сдана. Если только статика — посмотри вкладку Предпросмотр.
|
||||||
6. Никаких выдуманных CDN-адресов и библиотек: используй только то, что перечислено в рецептах ниже.
|
6. Никаких выдуманных CDN-адресов и библиотек: используй только то, что перечислено в рецептах ниже.
|
||||||
|
|
||||||
## 📁 ТЫ УЖЕ ВНУТРИ ПАПКИ ПРОЕКТА — собирай сайт ЗДЕСЬ
|
## 📁 ТЫ УЖЕ ВНУТРИ ПАПКИ ПРОЕКТА — собирай проект ЗДЕСЬ
|
||||||
|
|
||||||
Твоя рабочая директория (cwd) — это **папка проекта юзера** (`.../users/<username>/<project>/`). Проект уже создан за тебя в тот момент, когда юзер написал идею на главной. Проверь: `pwd` — папка проекта, `ls` — там лежат AGENTS.md/design.md/README.md.
|
Твоя рабочая директория (cwd) — это **папка проекта юзера** (`.../users/<username>/<project>/`). Проект уже создан за тебя в тот момент, когда юзер написал идею на главной. Проверь: `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` в другие папки / в корень воркспейса.** Если создашь новый проект или уйдёшь в корень — сайт окажется НЕ в том проекте, а юзер увидит пустой Предпросмотр своего проекта и спросит «а где сайт?». Именно так это ломается.
|
❌ **НЕ запускай `./new-project`.** ❌ **НЕ делай `cd` в другие папки / в корень воркспейса.** Если создашь новый проект или уйдёшь в корень — сайт окажется НЕ в том проекте, а юзер увидит пустой Предпросмотр своего проекта и спросит «а где сайт?». Именно так это ломается.
|
||||||
|
|
||||||
@ -808,8 +808,8 @@ git push origin HEAD:pages
|
|||||||
|
|
||||||
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
|
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
|
||||||
|
|
||||||
### ⚠️ Проекты с бэкендом (server.js / run) и статика — НЕ путай юзера
|
### ⚠️ Проекты с бэкендом (server.js / run) — НЕ путай юзера
|
||||||
Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО статические файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому:
|
Вкладка **Превью** и публикация в Pages показывают ТОЛЬКО файлы — server.js там НЕ работает: формы method=POST и запросы к твоему серверу будут мертвы. Поэтому:
|
||||||
- Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия.
|
- Прежде чем переделывать работающий статический index.html на серверный рендер (форма POST, шаблоны из server.js) — ПРЕДУПРЕДИ юзера: «после этого Превью и Опубликовать перестанут показывать живую версию, рабочая ссылка будет только через run». Меняй только после его согласия.
|
||||||
- Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть.
|
- Если проект уже с бэкендом: держи index.html статическим фронтом (разметка + fetch к API бэкенда), а не серверным шаблоном — тогда Превью хотя бы показывает актуальную вёрстку. Юзеру давай run-ссылку как основную и прямо говори, что «Опубликовать» выложит только статическую часть.
|
||||||
- run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`.
|
- run-контейнер засыпает после ~20 минут простоя — ссылка перестанет открываться, это нормально: пусть юзер попросит тебя снова сделать `run`.
|
||||||
|
|||||||
318
API.md
Normal file
318
API.md
Normal file
@ -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"}'
|
||||||
|
```
|
||||||
243
admin.css
Normal file
243
admin.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
138
admin.html
Normal file
138
admin.html
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Админ-панель — Apple Style</title>
|
||||||
|
<link rel="stylesheet" href="admin.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="admin-nav">
|
||||||
|
<div class="admin-nav-container">
|
||||||
|
<a href="index.html" class="admin-logo"> Админка</a>
|
||||||
|
<a href="index.html" class="btn-back">← На сайт</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="admin-container">
|
||||||
|
<!-- Settings Section -->
|
||||||
|
<section class="admin-section">
|
||||||
|
<h2>🏠 Настройки сайта</h2>
|
||||||
|
<div class="admin-card">
|
||||||
|
<form id="settingsForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Заголовок Hero</label>
|
||||||
|
<input type="text" id="heroTitle" name="heroTitle" placeholder="Добро пожаловать в будущее">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Подзаголовок Hero</label>
|
||||||
|
<input type="text" id="heroSubtitle" name="heroSubtitle" placeholder="Инновации, которые меняют мир">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Тема</label>
|
||||||
|
<select id="theme" name="theme">
|
||||||
|
<option value="dark">Тёмная</option>
|
||||||
|
<option value="light">Светлая</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Сохранить настройки</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Products Section -->
|
||||||
|
<section class="admin-section">
|
||||||
|
<h2>📱 Продукты</h2>
|
||||||
|
<div class="admin-card">
|
||||||
|
<button class="btn btn-secondary" onclick="showAddProductForm()">+ Добавить продукт</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="addProductForm" class="admin-card" style="display:none">
|
||||||
|
<h3>Новый продукт</h3>
|
||||||
|
<form id="productForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Название</label>
|
||||||
|
<input type="text" id="productName" required placeholder="iPhone 18 Pro">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Слоган</label>
|
||||||
|
<input type="text" id="productTagline" required placeholder="Ещё мощнее. Ещё лучше.">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Описание</label>
|
||||||
|
<textarea id="productDescription" rows="3" placeholder="Подробное описание..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Цена ($)</label>
|
||||||
|
<input type="number" id="productPrice" required placeholder="999">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Категория</label>
|
||||||
|
<select id="productCategory">
|
||||||
|
<option value="premium">Premium</option>
|
||||||
|
<option value="audio">Audio</option>
|
||||||
|
<option value="wearables">Wearables</option>
|
||||||
|
<option value="computers">Computers</option>
|
||||||
|
<option value="phones">Phones</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>URL изображения</label>
|
||||||
|
<input type="text" id="productImage" placeholder="https://picsum.photos/800/600?random=1">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Особенности (через запятую)</label>
|
||||||
|
<input type="text" id="productFeatures" placeholder="M3 чип, 20ч работы, Face ID">
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Добавить</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="hideAddProductForm()">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="productsList" class="admin-list">
|
||||||
|
<!-- Products will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Announcements Section -->
|
||||||
|
<section class="admin-section">
|
||||||
|
<h2>📢 Объявления</h2>
|
||||||
|
<div class="admin-card">
|
||||||
|
<button class="btn btn-secondary" onclick="showAddAnnouncementForm()">+ Добавить объявление</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="addAnnouncementForm" class="admin-card" style="display:none">
|
||||||
|
<h3>Новое объявление</h3>
|
||||||
|
<form id="announcementForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Заголовок</label>
|
||||||
|
<input type="text" id="announcementTitle" required placeholder="Apple Event 2026">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Дата</label>
|
||||||
|
<input type="date" id="announcementDate" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Содержание</label>
|
||||||
|
<textarea id="announcementContent" rows="3" required placeholder="Смотрите презентацию..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Добавить</button>
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="hideAddAnnouncementForm()">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="announcementsList" class="admin-list">
|
||||||
|
<!-- Announcements will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="admin.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
267
admin.js
Normal file
267
admin.js
Normal file
@ -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 = '<p style="color:#86868b;text-align:center;padding:40px">Нет продуктов</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = json.data.map(p => `
|
||||||
|
<div class="list-item">
|
||||||
|
<div class="list-item-info">
|
||||||
|
<div class="list-item-title">${p.name}</div>
|
||||||
|
<div class="list-item-meta">
|
||||||
|
${p.tagline} • $${p.price} •
|
||||||
|
<span class="status-badge">${p.category}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-actions">
|
||||||
|
<button class="btn btn-secondary btn-small" onclick="editProduct(${p.id})">Изменить</button>
|
||||||
|
<button class="btn btn-danger btn-small" onclick="deleteProduct(${p.id})">Удалить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).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 = '<p style="color:#86868b;text-align:center;padding:40px">Нет объявлений</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = json.data.map(a => `
|
||||||
|
<div class="list-item">
|
||||||
|
<div class="list-item-info">
|
||||||
|
<div class="list-item-title">${a.title}</div>
|
||||||
|
<div class="list-item-meta">
|
||||||
|
${a.content} • ${a.date} •
|
||||||
|
<span class="status-badge">${a.active ? 'Активно' : 'Скрыто'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-actions">
|
||||||
|
<button class="btn btn-secondary btn-small" onclick="editAnnouncement(${a.id})">Изменить</button>
|
||||||
|
<button class="btn btn-danger btn-small" onclick="deleteAnnouncement(${a.id})">Удалить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).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);
|
||||||
|
}
|
||||||
|
}
|
||||||
166
app.js
Normal file
166
app.js
Normal file
@ -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} <a href="#">Подробнее →</a>`;
|
||||||
|
} 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 = '<p style="text-align:center;color:var(--text-secondary)">Нет продуктов</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = json.data.map((p, i) => `
|
||||||
|
<div class="product-card" style="transition-delay: ${i * 100}ms">
|
||||||
|
<img src="${p.image}" alt="${p.name}" class="product-image">
|
||||||
|
<div class="product-content">
|
||||||
|
<h3 class="product-name">${p.name}</h3>
|
||||||
|
<p class="product-tagline">${p.tagline}</p>
|
||||||
|
<p class="product-price">от ${p.price} $</p>
|
||||||
|
<div class="product-features">
|
||||||
|
${p.features.map(f => `<span class="feature-tag">${f}</span>`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Trigger visibility check after load
|
||||||
|
setTimeout(checkProductVisibility, 100);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load products:', e);
|
||||||
|
document.getElementById('productsContainer').innerHTML = '<p style="text-align:center;color:var(--text-secondary)">Ошибка загрузки</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
62
data.json
Normal file
62
data.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
755
index.html
755
index.html
@ -3,668 +3,171 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Исландия — Земля льда и пламени</title>
|
<title>Apple Style — Будущее здесь</title>
|
||||||
<style>
|
<link rel="stylesheet" href="style.css">
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--bg: #0B0F19;
|
|
||||||
--text: #FFFFFF;
|
|
||||||
--text-muted: #94A3B8;
|
|
||||||
--accent: #00D4FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
overflow-x: hidden;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Навигация */
|
|
||||||
nav {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
background: rgba(11, 15, 25, 0.9);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
padding: 16px 0;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-container {
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 32px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
background: linear-gradient(135deg, #00D4FF, #7C3AED);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a {
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: color 0.3s;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a:hover {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero секция */
|
|
||||||
.hero {
|
|
||||||
height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-layer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 120%;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-bg1 {
|
|
||||||
background-image: url('https://picsum.photos/1920/1200?random=10');
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-bg2 {
|
|
||||||
background-image: url('https://picsum.photos/1920/1200?random=11');
|
|
||||||
z-index: 2;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(180deg, rgba(11,15,25,0.3) 0%, var(--bg) 100%);
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 4;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 900px;
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero h1 {
|
|
||||||
font-size: clamp(56px, 12vw, 128px);
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: -4px;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
text-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(40px);
|
|
||||||
animation: fadeUp 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero p {
|
|
||||||
font-size: clamp(20px, 3vw, 28px);
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 40px;
|
|
||||||
font-weight: 300;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(40px);
|
|
||||||
animation: fadeUp 1.2s cubic-bezier(0.25, 1, 0.5, 1) forwards;
|
|
||||||
animation-delay: 0.6s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 18px 42px;
|
|
||||||
border-radius: 980px;
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1);
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, var(--accent), #7C3AED);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 10px 40px rgba(0, 212, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
transform: translateY(-4px) scale(1.02);
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 212, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Destinations */
|
|
||||||
.destinations {
|
|
||||||
padding: 140px 24px;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label {
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 14px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 3px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: clamp(40px, 6vw, 72px);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.destinations-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
|
||||||
gap: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.destination-card {
|
|
||||||
position: relative;
|
|
||||||
height: 500px;
|
|
||||||
border-radius: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.destination-card:hover {
|
|
||||||
transform: translateY(-12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.destination-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.destination-card:hover .destination-img {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.destination-content {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 40px;
|
|
||||||
background: linear-gradient(0deg, rgba(0,0,0,0.9) 0%, transparent 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.destination-card h3 {
|
|
||||||
font-size: 28px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.destination-card p {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Parallax Section */
|
|
||||||
.parallax-section {
|
|
||||||
position: relative;
|
|
||||||
height: 80vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 80px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-bg {
|
|
||||||
position: absolute;
|
|
||||||
top: -20%;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 140%;
|
|
||||||
background-image: url('https://picsum.photos/1920/1200?random=12');
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
z-index: 1;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(11, 15, 25, 0.7);
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-content {
|
|
||||||
position: relative;
|
|
||||||
z-index: 3;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 800px;
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-content h2 {
|
|
||||||
font-size: clamp(40px, 7vw, 80px);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -2px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-content p {
|
|
||||||
font-size: 20px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stats */
|
|
||||||
.stats {
|
|
||||||
padding: 120px 24px;
|
|
||||||
background: #080B14;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 60px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item h3 {
|
|
||||||
font-size: clamp(48px, 6vw, 80px);
|
|
||||||
font-weight: 800;
|
|
||||||
background: linear-gradient(135deg, var(--accent), #7C3AED);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item p {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 16px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Gallery */
|
|
||||||
.gallery {
|
|
||||||
padding: 140px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
grid-template-rows: repeat(2, 300px);
|
|
||||||
gap: 16px;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-item {
|
|
||||||
border-radius: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-item:first-child {
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
grid-row: 1 / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-item:hover .gallery-img {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* CTA */
|
|
||||||
.cta {
|
|
||||||
padding: 140px 24px;
|
|
||||||
text-align: center;
|
|
||||||
background: linear-gradient(180deg, var(--bg) 0%, #1a1f2e 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta h2 {
|
|
||||||
font-size: clamp(40px, 6vw, 72px);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -2px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta p {
|
|
||||||
font-size: 20px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
footer {
|
|
||||||
padding: 60px 24px;
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.1);
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 32px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a {
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a:hover {
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes fadeUp {
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(50px);
|
|
||||||
transition: all 1s cubic-bezier(0.25, 1, 0.5, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reveal.active {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.nav-links {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-item:first-child {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1;
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.destinations-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
gap: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Навигация -->
|
<!-- Announcement Bar -->
|
||||||
<nav>
|
<div id="announcementBar" class="announcement-bar"></div>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="nav" id="nav">
|
||||||
<div class="nav-container">
|
<div class="nav-container">
|
||||||
<div class="logo">ICELAND</div>
|
<a href="/" class="logo"></a>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="#destinations">Места</a>
|
<a href="#products">Продукты</a>
|
||||||
<a href="#about">О стране</a>
|
<a href="#features">Возможности</a>
|
||||||
<a href="#gallery">Галерея</a>
|
<a href="#vision">Vision</a>
|
||||||
<a href="#book">Забронировать</a>
|
<a href="admin.html" class="admin-link">Админка</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Hero -->
|
<!-- Hero Section -->
|
||||||
<section class="hero">
|
<section class="hero" id="hero">
|
||||||
<div class="parallax-layer hero-bg1" id="heroBg1"></div>
|
<div class="hero-bg" id="heroBg"></div>
|
||||||
<div class="parallax-layer hero-bg2" id="heroBg2"></div>
|
|
||||||
<div class="hero-overlay"></div>
|
<div class="hero-overlay"></div>
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<h1>Земля льда и пламени</h1>
|
<h1 id="heroTitle" class="hero-title"></h1>
|
||||||
<p>Открой для себя самую загадочную страну мира</p>
|
<p id="heroSubtitle" class="hero-subtitle"></p>
|
||||||
<a href="#book" class="btn btn-primary">Начать путешествие</a>
|
<div class="hero-cta">
|
||||||
|
<a href="#products" class="btn btn-primary">Смотреть продукты</a>
|
||||||
|
<a href="#vision" class="btn btn-secondary">Узнать больше</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="scroll-indicator">
|
||||||
|
<div class="mouse">
|
||||||
|
<div class="wheel"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Destinations -->
|
<!-- Product Showcase (Apple-style scroll) -->
|
||||||
<section class="destinations" id="destinations">
|
<section class="product-showcase" id="products">
|
||||||
<div class="section-header reveal">
|
<div class="showcase-intro">
|
||||||
<p class="section-label">Направления</p>
|
<h2 class="showcase-title">Выберите свой путь</h2>
|
||||||
<h2 class="section-title">Места, которые захватывают дух</h2>
|
<p class="showcase-subtitle">Инновации в каждой детали</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="destinations-grid">
|
<div class="products-container" id="productsContainer">
|
||||||
<div class="destination-card reveal">
|
<!-- Products will be loaded here -->
|
||||||
<img src="https://picsum.photos/600/800?random=20" alt="Водопад" class="destination-img">
|
</div>
|
||||||
<div class="destination-content">
|
</section>
|
||||||
<h3>Водопад Гюдльфосс</h3>
|
|
||||||
<p>Один из самых красивых водопадов Исландии</p>
|
<!-- Features Section with Parallax -->
|
||||||
|
<section class="features" id="features">
|
||||||
|
<div class="features-bg" id="featuresBg"></div>
|
||||||
|
<div class="features-overlay"></div>
|
||||||
|
<div class="features-content">
|
||||||
|
<h2 class="section-title reveal">Технологии будущего</h2>
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card reveal">
|
||||||
|
<div class="feature-icon">🚀</div>
|
||||||
|
<h3>Производительность</h3>
|
||||||
|
<p>Чипы M-серии меняют правила игры</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="feature-card reveal">
|
||||||
<div class="destination-card reveal">
|
<div class="feature-icon">🔒</div>
|
||||||
<img src="https://picsum.photos/600/800?random=21" alt="Гейзер" class="destination-img">
|
<h3>Безопасность</h3>
|
||||||
<div class="destination-content">
|
<p>Защита данных на аппаратном уровне</p>
|
||||||
<h3>Долина гейзеров</h3>
|
|
||||||
<p>Гейзер Строккур извергается каждые 5-10 минут</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="feature-card reveal">
|
||||||
<div class="destination-card reveal">
|
<div class="feature-icon">🌱</div>
|
||||||
<img src="https://picsum.photos/600/800?random=22" alt="Лагуна" class="destination-img">
|
<h3>Экология</h3>
|
||||||
<div class="destination-content">
|
<p>Углеродная нейтральность к 2030</p>
|
||||||
<h3>Голубая лагуна</h3>
|
|
||||||
<p>Геотермальный спа-курорт мирового класса</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="feature-card reveal">
|
||||||
<div class="destination-card reveal">
|
<div class="feature-icon">🤖</div>
|
||||||
<img src="https://picsum.photos/600/800?random=23" alt="Северное сияние" class="destination-img">
|
<h3>ИИ</h3>
|
||||||
<div class="destination-content">
|
<p>Apple Intelligence в каждом устройстве</p>
|
||||||
<h3>Северное сияние</h3>
|
|
||||||
<p>Природное явление, которое нужно увидеть хотя бы раз</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Parallax Section -->
|
<!-- Vision Section -->
|
||||||
<section class="parallax-section" id="about">
|
<section class="vision" id="vision">
|
||||||
<div class="parallax-bg" id="parallaxBg"></div>
|
<div class="vision-content">
|
||||||
<div class="parallax-overlay"></div>
|
<h2 class="vision-title reveal">Vision Pro</h2>
|
||||||
<div class="parallax-content reveal">
|
<p class="vision-subtitle reveal">Эра пространственных вычислений наступила</p>
|
||||||
<h2>Приключение всей жизни</h2>
|
<div class="vision-features reveal">
|
||||||
<p>Вулканы, ледники, водопады, гейзеры и северное сияние — Исландия собрала все чудеса природы в одном месте</p>
|
<div class="vision-feature">
|
||||||
<a href="#book" class="btn btn-primary">Выбрать тур</a>
|
<span class="vision-feature-icon">👁️</span>
|
||||||
|
<div>
|
||||||
|
<h4>eyeSight</h4>
|
||||||
|
<p>Оставайтесь на связи с окружающими</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vision-feature">
|
||||||
|
<span class="vision-feature-icon">🎭</span>
|
||||||
|
<div>
|
||||||
|
<h4>visionOS</h4>
|
||||||
|
<p>Интуитивный пространственный интерфейс</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vision-feature">
|
||||||
|
<span class="vision-feature-icon">🎬</span>
|
||||||
|
<div>
|
||||||
|
<h4>Immersion</h4>
|
||||||
|
<p>Погружение как в кинотеатре</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vision-image reveal">
|
||||||
|
<img src="https://picsum.photos/1200/800?random=50" alt="Vision Pro">
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Stats -->
|
<!-- Big Image Scroll Section -->
|
||||||
<section class="stats">
|
<section class="scroll-gallery" id="gallery">
|
||||||
<div class="stats-container">
|
<div class="gallery-item" id="galleryItem1">
|
||||||
<div class="stats-grid">
|
<img src="https://picsum.photos/1920/1080?random=60" alt="">
|
||||||
<div class="stat-item reveal">
|
<div class="gallery-caption">
|
||||||
<h3>10K+</h3>
|
<h3>Дизайн</h3>
|
||||||
<p>Водопадов</p>
|
<p>Каждая линия имеет значение</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item reveal">
|
</div>
|
||||||
<h3>130</h3>
|
<div class="gallery-item" id="galleryItem2">
|
||||||
<p>Вулканов</p>
|
<img src="https://picsum.photos/1920/1080?random=61" alt="">
|
||||||
</div>
|
<div class="gallery-caption">
|
||||||
<div class="stat-item reveal">
|
<h3>Инновации</h3>
|
||||||
<h3>600</h3>
|
<p>Технологии, которые вдохновляют</p>
|
||||||
<p>Горячих источников</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item reveal">
|
<div class="gallery-item" id="galleryItem3">
|
||||||
<h3>24/7</h3>
|
<img src="https://picsum.photos/1920/1080?random=62" alt="">
|
||||||
<p>Северное сияние зимой</p>
|
<div class="gallery-caption">
|
||||||
</div>
|
<h3>Будущее</h3>
|
||||||
|
<p>Создаём завтрашний день сегодня</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Gallery -->
|
<!-- CTA Section -->
|
||||||
<section class="gallery" id="gallery">
|
<section class="cta-section">
|
||||||
<div class="section-header reveal">
|
<h2 class="cta-title reveal">Готовы к будущему?</h2>
|
||||||
<p class="section-label">Галерея</p>
|
<p class="cta-subtitle reveal">Откройте для себя мир инноваций уже сегодня</p>
|
||||||
<h2 class="section-title">Моменты, которые останутся навсегда</h2>
|
<div class="cta-buttons reveal">
|
||||||
|
<a href="#products" class="btn btn-primary">Выбрать устройство</a>
|
||||||
|
<a href="#" class="btn btn-secondary">Связаться с нами</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery-grid">
|
|
||||||
<div class="gallery-item reveal">
|
|
||||||
<img src="https://picsum.photos/1200/800?random=30" alt="Пейзаж" class="gallery-img">
|
|
||||||
</div>
|
|
||||||
<div class="gallery-item reveal">
|
|
||||||
<img src="https://picsum.photos/600/600?random=31" alt="Горы" class="gallery-img">
|
|
||||||
</div>
|
|
||||||
<div class="gallery-item reveal">
|
|
||||||
<img src="https://picsum.photos/600/600?random=32" alt="Океан" class="gallery-img">
|
|
||||||
</div>
|
|
||||||
<div class="gallery-item reveal">
|
|
||||||
<img src="https://picsum.photos/600/600?random=33" alt="Ледник" class="gallery-img">
|
|
||||||
</div>
|
|
||||||
<div class="gallery-item reveal">
|
|
||||||
<img src="https://picsum.photos/600/600?random=34" alt="Пляж" class="gallery-img">
|
|
||||||
</div>
|
|
||||||
<div class="gallery-item reveal">
|
|
||||||
<img src="https://picsum.photos/600/600?random=35" alt="Фьорд" class="gallery-img">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
<section class="cta" id="book">
|
|
||||||
<h2 class="reveal">Готовы к приключению?</h2>
|
|
||||||
<p class="reveal">Забронируйте тур в Исландию прямо сейчас и получите скидку 20% на групповые поездки</p>
|
|
||||||
<a href="#" class="btn btn-primary reveal">Забронировать тур</a>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer>
|
<footer>
|
||||||
<div class="footer-links">
|
<div class="footer-content">
|
||||||
<a href="#">О нас</a>
|
<div class="footer-links">
|
||||||
<a href="#">Туры</a>
|
<a href="#">О компании</a>
|
||||||
<a href="#">Блог</a>
|
<a href="#">Карьера</a>
|
||||||
<a href="#">Контакты</a>
|
<a href="#">Инвесторам</a>
|
||||||
<a href="#">Политика конфиденциальности</a>
|
<a href="#">Новости</a>
|
||||||
|
<a href="#">Конфиденциальность</a>
|
||||||
|
<a href="#">Контакты</a>
|
||||||
|
</div>
|
||||||
|
<p class="copyright">© 2026 Apple Style Demo. Все права защищены.</p>
|
||||||
</div>
|
</div>
|
||||||
<p>© 2026 Iceland Travel. Все права защищены.</p>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script src="app.js"></script>
|
||||||
// Многослойный параллакс для Hero
|
|
||||||
const heroBg1 = document.getElementById('heroBg1');
|
|
||||||
const heroBg2 = document.getElementById('heroBg2');
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
const scrolled = window.pageYOffset;
|
|
||||||
if (scrolled < window.innerHeight) {
|
|
||||||
heroBg1.style.transform = `translateY(${scrolled * 0.6}px) scale(${1 + scrolled * 0.0008})`;
|
|
||||||
heroBg2.style.transform = `translateY(${scrolled * 0.3}px) scale(${1 + scrolled * 0.0005})`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Параллакс для средней секции
|
|
||||||
const parallaxBg = document.getElementById('parallaxBg');
|
|
||||||
const parallaxSection = document.getElementById('parallaxSection');
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
const rect = document.querySelector('.parallax-section').getBoundingClientRect();
|
|
||||||
if (rect.top < window.innerHeight && rect.bottom > 0) {
|
|
||||||
const offset = (window.innerHeight - rect.top) * 0.25;
|
|
||||||
parallaxBg.style.transform = `translateY(${offset}px)`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Анимация появления
|
|
||||||
const reveals = document.querySelectorAll('.reveal');
|
|
||||||
function 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();
|
|
||||||
|
|
||||||
// Плавный скролл
|
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
||||||
anchor.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const target = document.querySelector(this.getAttribute('href'));
|
|
||||||
if (target) {
|
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
8
package.json
Normal file
8
package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "apple-style-app",
|
||||||
|
"private": true,
|
||||||
|
"type": "commonjs",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
185
server.js
Normal file
185
server.js
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
const http = require("http");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
const DATA = path.join(__dirname, "data.json");
|
||||||
|
|
||||||
|
function readData() {
|
||||||
|
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { products: [], announcements: [], siteSettings: {} }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeData(data) {
|
||||||
|
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBody(req) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let s = "";
|
||||||
|
req.on("data", (c) => (s += c));
|
||||||
|
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function json(res, data, status = 200) {
|
||||||
|
res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
||||||
|
res.end(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function notFound(res) {
|
||||||
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
||||||
|
res.end("Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
// PUBLIC API
|
||||||
|
"GET /api/products": (req, res) => {
|
||||||
|
const data = readData();
|
||||||
|
json(res, { ok: true, data: data.products.sort((a, b) => a.order - b.order) });
|
||||||
|
},
|
||||||
|
|
||||||
|
"GET /api/products/:id": (req, res, params) => {
|
||||||
|
const data = readData();
|
||||||
|
const product = data.products.find(p => p.id === parseInt(params.id));
|
||||||
|
if (!product) return json(res, { ok: false, error: "Product not found" }, 404);
|
||||||
|
json(res, { ok: true, data: product });
|
||||||
|
},
|
||||||
|
|
||||||
|
"GET /api/announcements": (req, res) => {
|
||||||
|
const data = readData();
|
||||||
|
json(res, { ok: true, data: data.announcements.filter(a => a.active) });
|
||||||
|
},
|
||||||
|
|
||||||
|
"GET /api/settings": (req, res) => {
|
||||||
|
const data = readData();
|
||||||
|
json(res, { ok: true, data: data.siteSettings });
|
||||||
|
},
|
||||||
|
|
||||||
|
// ADMIN API
|
||||||
|
"POST /api/admin/products": async (req, res) => {
|
||||||
|
const body = await parseBody(req);
|
||||||
|
const data = readData();
|
||||||
|
const newProduct = {
|
||||||
|
id: Date.now(),
|
||||||
|
...body,
|
||||||
|
order: data.products.length + 1
|
||||||
|
};
|
||||||
|
data.products.push(newProduct);
|
||||||
|
writeData(data);
|
||||||
|
json(res, { ok: true, data: newProduct }, 201);
|
||||||
|
},
|
||||||
|
|
||||||
|
"PUT /api/admin/products/:id": async (req, res, params) => {
|
||||||
|
const body = await parseBody(req);
|
||||||
|
const data = readData();
|
||||||
|
const idx = data.products.findIndex(p => p.id === parseInt(params.id));
|
||||||
|
if (idx === -1) return json(res, { ok: false, error: "Product not found" }, 404);
|
||||||
|
data.products[idx] = { ...data.products[idx], ...body, id: parseInt(params.id) };
|
||||||
|
writeData(data);
|
||||||
|
json(res, { ok: true, data: data.products[idx] });
|
||||||
|
},
|
||||||
|
|
||||||
|
"DELETE /api/admin/products/:id": (req, res, params) => {
|
||||||
|
const data = readData();
|
||||||
|
data.products = data.products.filter(p => p.id !== parseInt(params.id));
|
||||||
|
writeData(data);
|
||||||
|
json(res, { ok: true, deleted: parseInt(params.id) });
|
||||||
|
},
|
||||||
|
|
||||||
|
"POST /api/admin/announcements": async (req, res) => {
|
||||||
|
const body = await parseBody(req);
|
||||||
|
const data = readData();
|
||||||
|
const newAnnouncement = { id: Date.now(), ...body, active: true };
|
||||||
|
data.announcements.push(newAnnouncement);
|
||||||
|
writeData(data);
|
||||||
|
json(res, { ok: true, data: newAnnouncement }, 201);
|
||||||
|
},
|
||||||
|
|
||||||
|
"PUT /api/admin/announcements/:id": async (req, res, params) => {
|
||||||
|
const body = await parseBody(req);
|
||||||
|
const data = readData();
|
||||||
|
const idx = data.announcements.findIndex(a => a.id === parseInt(params.id));
|
||||||
|
if (idx === -1) return json(res, { ok: false, error: "Not found" }, 404);
|
||||||
|
data.announcements[idx] = { ...data.announcements[idx], ...body, id: parseInt(params.id) };
|
||||||
|
writeData(data);
|
||||||
|
json(res, { ok: true, data: data.announcements[idx] });
|
||||||
|
},
|
||||||
|
|
||||||
|
"DELETE /api/admin/announcements/:id": (req, res, params) => {
|
||||||
|
const data = readData();
|
||||||
|
data.announcements = data.announcements.filter(a => a.id !== parseInt(params.id));
|
||||||
|
writeData(data);
|
||||||
|
json(res, { ok: true, deleted: parseInt(params.id) });
|
||||||
|
},
|
||||||
|
|
||||||
|
"PUT /api/admin/settings": async (req, res) => {
|
||||||
|
const body = await parseBody(req);
|
||||||
|
const data = readData();
|
||||||
|
data.siteSettings = { ...data.siteSettings, ...body };
|
||||||
|
writeData(data);
|
||||||
|
json(res, { ok: true, data: data.siteSettings });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mimeTypes = {
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".js": "application/javascript; charset=utf-8",
|
||||||
|
".json": "application/json; charset=utf-8",
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".svg": "image/svg+xml"
|
||||||
|
};
|
||||||
|
|
||||||
|
http.createServer(async (req, res) => {
|
||||||
|
const url = req.url.split("?")[0];
|
||||||
|
const method = req.method;
|
||||||
|
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||||
|
|
||||||
|
if (method === "OPTIONS") {
|
||||||
|
res.writeHead(204);
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// API ROUTING
|
||||||
|
for (const [route, handler] of Object.entries(routes)) {
|
||||||
|
const [m, p] = route.split(" ");
|
||||||
|
if (method !== m) continue;
|
||||||
|
|
||||||
|
const routeParts = p.split("/");
|
||||||
|
const urlParts = url.split("/");
|
||||||
|
|
||||||
|
if (routeParts.length !== urlParts.length) continue;
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
let match = true;
|
||||||
|
for (let i = 0; i < routeParts.length; i++) {
|
||||||
|
if (routeParts[i].startsWith(":")) {
|
||||||
|
params[routeParts[i].slice(1)] = urlParts[i];
|
||||||
|
} else if (routeParts[i] !== urlParts[i]) {
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return handler(req, res, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATIC FILES
|
||||||
|
let file = url === "/" ? "/index.html" : url;
|
||||||
|
const full = path.join(__dirname, file);
|
||||||
|
|
||||||
|
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
||||||
|
const ext = path.extname(full);
|
||||||
|
const type = mimeTypes[ext] || "application/octet-stream";
|
||||||
|
res.writeHead(200, { "Content-Type": type });
|
||||||
|
return res.end(fs.readFileSync(full));
|
||||||
|
}
|
||||||
|
|
||||||
|
notFound(res);
|
||||||
|
}).listen(PORT, () => console.log("Server running on port " + PORT));
|
||||||
663
style.css
Normal file
663
style.css
Normal file
@ -0,0 +1,663 @@
|
|||||||
|
/* ===================================
|
||||||
|
APPLE-STYLE DESIGN SYSTEM
|
||||||
|
=================================== */
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-primary: #000000;
|
||||||
|
--bg-secondary: #1d1d1f;
|
||||||
|
--bg-tertiary: #2d2d30;
|
||||||
|
--text-primary: #f5f5f7;
|
||||||
|
--text-secondary: #86868b;
|
||||||
|
--text-tertiary: #6e6e73;
|
||||||
|
--accent: #2997ff;
|
||||||
|
--accent-hover: #147ce5;
|
||||||
|
--gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
--gradient-2: linear-gradient(180deg, #1a1a2e 0%, #000000 100%);
|
||||||
|
--nav-height: 48px;
|
||||||
|
--transition-smooth: cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
--transition-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Announcement Bar */
|
||||||
|
.announcement-bar {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 12px 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
transition: all 0.4s var(--transition-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-bar.hidden {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-bar a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-bar a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: var(--nav-height);
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
z-index: 1000;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
|
transition: all 0.3s var(--transition-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav.scrolled {
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 24px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 22px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
transition: color 0.3s;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links a:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links .admin-link {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero {
|
||||||
|
height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 120%;
|
||||||
|
background: radial-gradient(ellipse at center, #2a2a3e 0%, #0a0a0f 50%, #000000 100%);
|
||||||
|
z-index: 1;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(ellipse at center, transparent 0%, rgba(0,0,0,0.6) 100%);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 900px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: clamp(56px, 10vw, 112px);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1.05;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #b0b0b5 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(40px);
|
||||||
|
animation: fadeUp 1s var(--transition-smooth) forwards;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: clamp(24px, 4vw, 32px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(40px);
|
||||||
|
animation: fadeUp 1s var(--transition-smooth) forwards;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(40px);
|
||||||
|
animation: fadeUp 1s var(--transition-smooth) forwards;
|
||||||
|
animation-delay: 0.7s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 14px 28px;
|
||||||
|
border-radius: 980px;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s var(--transition-smooth);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll Indicator */
|
||||||
|
.scroll-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 40px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 3;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeIn 1s ease forwards;
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mouse {
|
||||||
|
width: 26px;
|
||||||
|
height: 40px;
|
||||||
|
border: 2px solid var(--text-secondary);
|
||||||
|
border-radius: 13px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel {
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
background: var(--text-secondary);
|
||||||
|
border-radius: 2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
animation: scroll 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0%, 100% { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||||
|
50% { opacity: 0.5; transform: translateX(-50%) translateY(12px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeUp {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Product Showcase */
|
||||||
|
.product-showcase {
|
||||||
|
padding: 140px 24px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-intro {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-title {
|
||||||
|
font-size: clamp(40px, 6vw, 64px);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showcase-subtitle {
|
||||||
|
font-size: clamp(20px, 3vw, 28px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.6s var(--transition-smooth);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover {
|
||||||
|
transform: translateY(-12px);
|
||||||
|
box-shadow: 0 30px 60px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 280px;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.6s var(--transition-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover .product-image {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-content {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-tagline {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-features {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-tag {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 980px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Features Section */
|
||||||
|
.features {
|
||||||
|
position: relative;
|
||||||
|
padding: 140px 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 120%;
|
||||||
|
background: url('https://picsum.photos/1920/1200?random=100') center/cover;
|
||||||
|
opacity: 0.15;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(180deg, var(--bg-primary) 0%, rgba(0,0,0,0.8) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: clamp(40px, 6vw, 64px);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: rgba(29, 29, 31, 0.6);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
transition: all 0.4s var(--transition-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
background: rgba(29, 29, 31, 0.8);
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 22px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vision Section */
|
||||||
|
.vision {
|
||||||
|
padding: 140px 24px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 60px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-content {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-title {
|
||||||
|
font-size: clamp(40px, 5vw, 64px);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-subtitle {
|
||||||
|
font-size: clamp(20px, 3vw, 26px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-features {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-feature {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-feature-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-feature h4 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-feature p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-image {
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll Gallery */
|
||||||
|
.scroll-gallery {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
height: 80vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-caption {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60px;
|
||||||
|
left: 60px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-caption h3 {
|
||||||
|
font-size: clamp(32px, 5vw, 56px);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-caption p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: rgba(255,255,255,0.9);
|
||||||
|
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA Section */
|
||||||
|
.cta-section {
|
||||||
|
padding: 140px 24px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--gradient-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-title {
|
||||||
|
font-size: clamp(40px, 6vw, 72px);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-subtitle {
|
||||||
|
font-size: clamp(20px, 3vw, 26px);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
footer {
|
||||||
|
padding: 60px 24px;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 32px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reveal Animation */
|
||||||
|
.reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50px);
|
||||||
|
transition: all 1s var(--transition-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-links {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vision {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-caption {
|
||||||
|
left: 24px;
|
||||||
|
bottom: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user