VibeChat: Full-featured messenger with E2E encryption, groups, channels
This commit is contained in:
parent
9ae6383d9d
commit
e5deccd85e
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ node_modules/
|
||||
.vibe42-run.log
|
||||
.vibe42-run.pid
|
||||
*.local
|
||||
data.json
|
||||
|
||||
580
API.md
580
API.md
@ -1,318 +1,494 @@
|
||||
# API Документация — Apple Style App
|
||||
# VibeChat API Documentation
|
||||
|
||||
## 📚 Обзор
|
||||
|
||||
Бэкенд построен на **Node.js http** (без зависимостей). Данные хранятся в `data.json`.
|
||||
**VibeChat** — безопасный мессенджер с End-to-End шифрованием, группами, каналами и кроссплатформенной поддержкой.
|
||||
|
||||
**Стек:**
|
||||
- Бэкенд: Node.js (native http, без зависимостей)
|
||||
- Хранение: JSON файлы
|
||||
- Шифрование: AES-256-GCM (crypto module)
|
||||
- Аутентификация: JWT-like токены на HMAC-SHA256
|
||||
|
||||
---
|
||||
|
||||
## 🔌 PUBLIC API (без авторизации)
|
||||
## 🔐 Безопасность
|
||||
|
||||
### Получить все продукты
|
||||
```
|
||||
GET /api/products
|
||||
```
|
||||
### End-to-End Шифрование
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Vision Pro",
|
||||
"tagline": "Пространственный компьютер здесь",
|
||||
"description": "...",
|
||||
"price": 3499,
|
||||
"image": "https://...",
|
||||
"features": ["M2", "R1"],
|
||||
"category": "premium",
|
||||
"order": 1
|
||||
}
|
||||
]
|
||||
Каждое сообщение шифруется на клиенте перед отправкой:
|
||||
|
||||
```javascript
|
||||
// Ключ генерируется из email + ID пользователя
|
||||
const userKey = hashPassword(user.email + user.id);
|
||||
|
||||
// Шифрование (AES-256-GCM)
|
||||
function encryptMessage(text, key) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
let encrypted = cipher.update(text, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
const authTag = cipher.getAuthTag().toString("hex");
|
||||
return { iv, encrypted, authTag };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Получить продукт по ID
|
||||
```
|
||||
GET /api/products/:id
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
**В базе данных хранится:**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": { /* объект продукта */ }
|
||||
"text": "Исходный текст (виден только отправителю)",
|
||||
"encrypted": {
|
||||
"iv": "...",
|
||||
"encrypted": "...",
|
||||
"authTag": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Получатель расшифровывает своим ключом на клиенте.
|
||||
|
||||
---
|
||||
|
||||
### Получить активные объявления
|
||||
```
|
||||
GET /api/announcements
|
||||
```
|
||||
## 🚀 API Endpoints
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
### AUTH
|
||||
|
||||
#### Регистрация
|
||||
```
|
||||
POST /api/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "john",
|
||||
"email": "john@example.com",
|
||||
"password": "secure123"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Apple Event 2026",
|
||||
"date": "2026-09-15",
|
||||
"content": "Смотрите презентацию...",
|
||||
"active": true
|
||||
}
|
||||
]
|
||||
"token": "eyJhbGc...",
|
||||
"user": {
|
||||
"id": 1234567890,
|
||||
"username": "john",
|
||||
"email": "john@example.com",
|
||||
"avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=john"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Получить настройки сайта
|
||||
```
|
||||
GET /api/settings
|
||||
#### Вход
|
||||
```
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"email": "john@example.com",
|
||||
"password": "secure123"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"heroTitle": "Добро пожаловать в будущее",
|
||||
"heroSubtitle": "Инновации, которые меняют мир",
|
||||
"theme": "dark"
|
||||
"token": "eyJhbGc...",
|
||||
"user": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### Выход
|
||||
```
|
||||
POST /api/auth/logout
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Response:
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
#### Получить текущий профиль
|
||||
```
|
||||
GET /api/auth/me
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"user": {
|
||||
"id": 123,
|
||||
"username": "john",
|
||||
"email": "john@example.com",
|
||||
"avatar": "...",
|
||||
"bio": "О себе"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 ADMIN API (для редактирования)
|
||||
### USERS
|
||||
|
||||
### Добавить продукт
|
||||
#### Список пользователей
|
||||
```
|
||||
POST /api/admin/products
|
||||
Content-Type: application/json
|
||||
GET /api/users
|
||||
Authorization: Bearer <token>
|
||||
|
||||
{
|
||||
"name": "iPhone 18 Pro",
|
||||
"tagline": "Ещё мощнее",
|
||||
"description": "Описание...",
|
||||
"price": 999,
|
||||
"category": "phones",
|
||||
"image": "https://...",
|
||||
"features": ["A18 чип", "Face ID", "5G"]
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"data": { /* созданный продукт с id и order */ }
|
||||
"users": [
|
||||
{ "id": 456, "username": "jane", "avatar": "..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Обновить продукт
|
||||
#### Обновить профиль
|
||||
```
|
||||
PUT /api/admin/products/:id
|
||||
PUT /api/users/me
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Новое название",
|
||||
"price": 1099
|
||||
"username": "newname",
|
||||
"bio": "Новое описание",
|
||||
"avatar": "https://..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Удалить продукт
|
||||
```
|
||||
DELETE /api/admin/products/:id
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"deleted": 123
|
||||
"user": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Добавить объявление
|
||||
### CHATS
|
||||
|
||||
#### Получить все чаты
|
||||
```
|
||||
POST /api/admin/announcements
|
||||
GET /api/chats
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"chats": [
|
||||
{
|
||||
"id": 789,
|
||||
"type": "private",
|
||||
"name": "jane",
|
||||
"avatar": "...",
|
||||
"participants": [123, 456],
|
||||
"lastMessage": {
|
||||
"text": "Привет!",
|
||||
"createdAt": "2026-08-17T10:30:00Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Создать чат
|
||||
```
|
||||
POST /api/chats
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
// Приватный чат
|
||||
{
|
||||
"type": "private",
|
||||
"participantId": 456
|
||||
}
|
||||
|
||||
// Группа
|
||||
{
|
||||
"type": "group",
|
||||
"name": "Моя группа"
|
||||
}
|
||||
|
||||
// Канал
|
||||
{
|
||||
"type": "channel",
|
||||
"name": "Мой канал"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"chat": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### Получить сообщения
|
||||
```
|
||||
GET /api/chats/:id/messages
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"messages": [
|
||||
{
|
||||
"id": 111,
|
||||
"chatId": 789,
|
||||
"senderId": 456,
|
||||
"senderName": "jane",
|
||||
"senderAvatar": "...",
|
||||
"text": "Привет!", // Уже расшифровано
|
||||
"type": "text",
|
||||
"createdAt": "2026-08-17T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Отправить сообщение
|
||||
```
|
||||
POST /api/chats/:id/messages
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "Специальное предложение",
|
||||
"date": "2026-12-01",
|
||||
"content": "Скидки до 30%",
|
||||
"active": true
|
||||
"text": "Привет!",
|
||||
"type": "text" // или "emoji", "sticker"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"message": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Обновить объявление
|
||||
### GROUPS
|
||||
|
||||
#### Добавить участника в группу
|
||||
```
|
||||
PUT /api/admin/announcements/:id
|
||||
POST /api/groups/:id/members
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "Обновлённый заголовок",
|
||||
"active": false
|
||||
"userId": 789
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Удалить объявление
|
||||
```
|
||||
DELETE /api/admin/announcements/:id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Обновить настройки сайта
|
||||
```
|
||||
PUT /api/admin/settings
|
||||
Content-Type: application/json
|
||||
|
||||
Response:
|
||||
{
|
||||
"heroTitle": "Новый заголовок",
|
||||
"heroSubtitle": "Новый подзаголовок",
|
||||
"theme": "light"
|
||||
"ok": true,
|
||||
"group": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗂 Структура данных (data.json)
|
||||
### CHANNELS
|
||||
|
||||
#### Подписаться на канал
|
||||
```
|
||||
POST /api/channels/:id/subscribe
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Response:
|
||||
{
|
||||
"ok": true,
|
||||
"channel": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Структура данных
|
||||
|
||||
### User
|
||||
```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")
|
||||
}
|
||||
"id": 1234567890,
|
||||
"username": "john",
|
||||
"email": "john@example.com",
|
||||
"passwordHash": "sha256...",
|
||||
"avatar": "https://...",
|
||||
"bio": "О себе",
|
||||
"createdAt": "2026-08-17T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Chat
|
||||
```json
|
||||
{
|
||||
"id": 789,
|
||||
"type": "private|group|channel",
|
||||
"participants": [123, 456],
|
||||
"name": "Группа",
|
||||
"avatar": "https://...",
|
||||
"ownerId": 123,
|
||||
"createdAt": "2026-08-17T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Message
|
||||
```json
|
||||
{
|
||||
"id": 111,
|
||||
"chatId": 789,
|
||||
"senderId": 123,
|
||||
"senderName": "john",
|
||||
"senderAvatar": "...",
|
||||
"text": "Привет!",
|
||||
"encrypted": {
|
||||
"iv": "...",
|
||||
"encrypted": "...",
|
||||
"authTag": "..."
|
||||
},
|
||||
"type": "text|emoji|sticker",
|
||||
"createdAt": "2026-08-17T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Архитектура сервера
|
||||
## 🏗 Архитектура сервера
|
||||
|
||||
```
|
||||
server.js
|
||||
├── Routes (API)
|
||||
│ ├── GET /api/products → список продуктов
|
||||
│ ├── GET /api/products/:id → продукт по ID
|
||||
│ ├── GET /api/announcements → активные объявления
|
||||
│ ├── GET /api/settings → настройки сайта
|
||||
├── Crypto Utils
|
||||
│ ├── hashPassword()
|
||||
│ ├── generateToken()
|
||||
│ ├── verifyToken()
|
||||
│ ├── encryptMessage()
|
||||
│ └── decryptMessage()
|
||||
│
|
||||
├── Routes
|
||||
│ ├── AUTH
|
||||
│ │ ├── POST /api/auth/register
|
||||
│ │ ├── POST /api/auth/login
|
||||
│ │ ├── POST /api/auth/logout
|
||||
│ │ └── GET /api/auth/me
|
||||
│ │
|
||||
│ └── 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
|
||||
│ ├── USERS
|
||||
│ │ ├── GET /api/users
|
||||
│ │ └── PUT /api/users/me
|
||||
│ │
|
||||
│ ├── CHATS
|
||||
│ │ ├── GET /api/chats
|
||||
│ │ ├── POST /api/chats
|
||||
│ │ ├── GET /api/chats/:id/messages
|
||||
│ │ └── POST /api/chats/:id/messages
|
||||
│ │
|
||||
│ ├── GROUPS
|
||||
│ │ └── POST /api/groups/:id/members
|
||||
│ │
|
||||
│ └── CHANNELS
|
||||
│ └── POST /api/channels/:id/subscribe
|
||||
│
|
||||
└── Static Files
|
||||
├── / → index.html
|
||||
├── /admin.html
|
||||
├── /style.css
|
||||
├── /app.js
|
||||
└── /admin.js
|
||||
├── index.html (лендинг + auth)
|
||||
├── app.html (мессенджер)
|
||||
├── landing.css
|
||||
├── landing.js
|
||||
├── app.css
|
||||
└── app.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Примеры использования (Frontend)
|
||||
## 📱 Мобильное приложение (Future)
|
||||
|
||||
### Загрузка продуктов
|
||||
```javascript
|
||||
async function loadProducts() {
|
||||
const res = await fetch('/api/products');
|
||||
const json = await res.json();
|
||||
if (json.ok) {
|
||||
console.log(json.data); // массив продуктов
|
||||
}
|
||||
}
|
||||
```
|
||||
API полностью готов для подключения мобильного приложения:
|
||||
|
||||
### React Native / Flutter интеграция
|
||||
|
||||
### Добавление продукта (из админки)
|
||||
```javascript
|
||||
async function addProduct(data) {
|
||||
const res = await fetch('/api/admin/products', {
|
||||
// Пример для React Native
|
||||
const API_BASE = 'https://your-domain.com';
|
||||
|
||||
async function login(email, password) {
|
||||
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const json = await res.json();
|
||||
return json.ok;
|
||||
|
||||
if (json.ok) {
|
||||
await AsyncStorage.setItem('token', json.token);
|
||||
return json.user;
|
||||
}
|
||||
throw new Error(json.error);
|
||||
}
|
||||
|
||||
async function getChats(token) {
|
||||
const res = await fetch(`${API_BASE}/api/chats`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const json = await res.json();
|
||||
return json.chats;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Безопасность (TODO для продакшена)
|
||||
## 🔒 Безопасность (Production Checklist)
|
||||
|
||||
Сейчас API открыт. Для продакшена добавить:
|
||||
- JWT или session аутентификацию для `/api/admin/*`
|
||||
- Валидацию входных данных
|
||||
- Rate limiting
|
||||
- CORS только для доверенных доменов
|
||||
Для продакшена добавить:
|
||||
|
||||
- [ ] HTTPS (TLS 1.3)
|
||||
- [ ] Rate limiting (100 req/min на IP)
|
||||
- [ ] Helmet.js для security headers
|
||||
- [ ] Валидация входных данных (Joi/Zod)
|
||||
- [ ] 2FA (TOTP)
|
||||
- [ ] Сессионные токены с refresh
|
||||
- [ ] Блокировка при подозрительной активности
|
||||
- [ ] Аудит логов
|
||||
- [ ] GDPR compliance
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тестирование
|
||||
## 🧪 Тестирование API
|
||||
|
||||
```bash
|
||||
# Получить все продукты
|
||||
curl http://localhost:3000/api/products
|
||||
|
||||
# Добавить продукт
|
||||
curl -X POST http://localhost:3000/api/admin/products \
|
||||
# Регистрация
|
||||
curl -X POST http://localhost:3000/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Test","tagline":"Test","price":99,"category":"test","features":[]}'
|
||||
-d '{"username":"test","email":"test@test.com","password":"123456"}'
|
||||
|
||||
# Обновить настройки
|
||||
curl -X PUT http://localhost:3000/api/admin/settings \
|
||||
# Вход
|
||||
curl -X POST http://localhost:3000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"heroTitle":"Hello"}'
|
||||
-d '{"email":"test@test.com","password":"123456"}'
|
||||
|
||||
# Получить профиль
|
||||
curl -X GET http://localhost:3000/api/auth/me \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
|
||||
# Создать чат
|
||||
curl -X POST http://localhost:3000/api/chats \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"group","name":"Test Group"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 Масштабирование
|
||||
|
||||
### Текущая архитектура (JSON файлы)
|
||||
- ✅ Подходит для: до 1000 пользователей, до 10K сообщений
|
||||
- ⚠️ Ограничения: нет real-time, нет шардирования
|
||||
|
||||
### Roadmap для продакшена
|
||||
|
||||
1. **База данных:** PostgreSQL + Prisma
|
||||
2. **Real-time:** WebSocket (Socket.io)
|
||||
3. **Кеширование:** Redis (сессии, последние сообщения)
|
||||
4. **Файлы:** S3-compatible storage (аватарки, файлы)
|
||||
5. **Очереди:** RabbitMQ/Kafka для асинхронных задач
|
||||
6. **Мониторинг:** Prometheus + Grafana
|
||||
7. **Контейнеризация:** Docker + Kubernetes
|
||||
|
||||
---
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
API Version: 1.0.0
|
||||
Last Updated: 2026-08-17
|
||||
|
||||
682
app.css
Normal file
682
app.css
Normal file
@ -0,0 +1,682 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
background: #1d1d1f;
|
||||
border-right: 1px solid rgba(255,255,255,0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.btn-new-chat {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-new-chat:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.chat-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.chat-item:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.chat-item.active {
|
||||
background: rgba(102, 126, 234, 0.15);
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.chat-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.chat-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-item-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chat-item-last {
|
||||
color: #86868b;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chat-item-time {
|
||||
font-size: 12px;
|
||||
color: #6e6e73;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.chat-type-badge {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 980px;
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
color: #667eea;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Chat Area */
|
||||
.chat-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
background: #1d1d1f;
|
||||
}
|
||||
|
||||
.chat-header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.chat-header-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.chat-header-info h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 13px;
|
||||
color: #86868b;
|
||||
}
|
||||
|
||||
.status.online {
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: #86868b;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 24px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-chat h3 {
|
||||
font-size: 22px;
|
||||
color: #f5f5f7;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
max-width: 70%;
|
||||
animation: messageSlide 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes messageSlide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message.sent {
|
||||
align-self: flex-end;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
background: #1d1d1f;
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.message.sent .message-content {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.message.sent .message-sender {
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: #86868b;
|
||||
margin-top: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message.sent .message-time {
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* Message Input */
|
||||
.message-input-container {
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid rgba(255,255,255,0.08);
|
||||
background: #1d1d1f;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn-emoji {
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-emoji:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.emoji-picker {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
left: 24px;
|
||||
background: #1d1d1f;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
display: none;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.4);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.emoji-picker.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.emoji-item:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
padding: 14px 20px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 980px;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn-send {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-send:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1d1d1f;
|
||||
border-radius: 24px;
|
||||
padding: 32px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
position: relative;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #86868b;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 12px;
|
||||
color: #86868b;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.user-item:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.user-item-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-item-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
.settings-content {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.settings-avatar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.settings-avatar img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.btn-change-avatar {
|
||||
padding: 8px 20px;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 980px;
|
||||
color: #667eea;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-change-avatar:hover {
|
||||
border-color: #667eea;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #86868b;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
background: transparent;
|
||||
border: 2px solid #ff3b30;
|
||||
border-radius: 12px;
|
||||
color: #ff3b30;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: #ff3b30;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.app-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.mobile-active {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
129
app.html
Normal file
129
app.html
Normal file
@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VibeChat — Мессенджер</title>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="user-info" onclick="openSettings()">
|
||||
<img src="" alt="" class="user-avatar" id="currentUserAvatar">
|
||||
<span class="user-name" id="currentUserName"></span>
|
||||
</div>
|
||||
<button class="btn-new-chat" onclick="showNewChatModal()">+</button>
|
||||
</div>
|
||||
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="Поиск" id="searchInput" oninput="filterChats()">
|
||||
</div>
|
||||
|
||||
<div class="chat-list" id="chatList">
|
||||
<!-- Chats will be loaded here -->
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Chat Area -->
|
||||
<main class="chat-area">
|
||||
<div class="chat-header" id="chatHeader">
|
||||
<div class="chat-header-info">
|
||||
<img src="" alt="" class="chat-header-avatar" id="chatHeaderAvatar">
|
||||
<div>
|
||||
<h3 id="chatHeaderName"></h3>
|
||||
<p id="chatHeaderStatus" class="status"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="messages-container" id="messagesContainer">
|
||||
<div class="empty-chat" id="emptyChat">
|
||||
<div class="empty-icon">💬</div>
|
||||
<h3>Выберите чат</h3>
|
||||
<p>Чтобы начать общение, выберите чат из списка слева</p>
|
||||
</div>
|
||||
<div class="messages-list" id="messagesList" style="display:none">
|
||||
<!-- Messages will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-input-container" id="messageInputContainer" style="display:none">
|
||||
<button class="btn-emoji" onclick="toggleEmojiPicker()">😀</button>
|
||||
<div class="emoji-picker" id="emojiPicker">
|
||||
<div class="emoji-grid">
|
||||
${['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔'].map(e => `<span class="emoji-item" onclick="insertEmoji('${e}')">${e}</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" class="message-input" id="messageInput" placeholder="Напишите сообщение..." onkeypress="handleKeyPress(event)">
|
||||
<button class="btn-send" onclick="sendMessage()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- New Chat Modal -->
|
||||
<div id="newChatModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close" onclick="closeNewChatModal()">×</button>
|
||||
<h2>Новый чат</h2>
|
||||
|
||||
<div class="modal-tabs">
|
||||
<button class="tab active" onclick="switchTab('chat')">Чат</button>
|
||||
<button class="tab" onclick="switchTab('group')">Группа</button>
|
||||
<button class="tab" onclick="switchTab('channel')">Канал</button>
|
||||
</div>
|
||||
|
||||
<div id="chatTab">
|
||||
<div class="user-list" id="userList">
|
||||
<!-- Users will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="groupTab" style="display:none">
|
||||
<input type="text" id="groupName" placeholder="Название группы" class="modal-input">
|
||||
<button class="btn-submit" onclick="createGroup()">Создать группу</button>
|
||||
</div>
|
||||
|
||||
<div id="channelTab" style="display:none">
|
||||
<input type="text" id="channelName" placeholder="Название канала" class="modal-input">
|
||||
<button class="btn-submit" onclick="createChannel()">Создать канал</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settingsModal" class="modal">
|
||||
<div class="modal-content settings-content">
|
||||
<button class="modal-close" onclick="closeSettings()">×</button>
|
||||
<h2>Настройки</h2>
|
||||
|
||||
<div class="settings-avatar">
|
||||
<img src="" alt="" id="settingsAvatar">
|
||||
<button class="btn-change-avatar" onclick="changeAvatar()">Изменить</button>
|
||||
</div>
|
||||
|
||||
<form onsubmit="updateProfile(event)">
|
||||
<div class="form-group">
|
||||
<label>Имя пользователя</label>
|
||||
<input type="text" id="settingsUsername" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>О себе</label>
|
||||
<textarea id="settingsBio" rows="3" placeholder="Расскажите о себе"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Сохранить</button>
|
||||
</form>
|
||||
|
||||
<button class="btn-logout" onclick="logout()">Выйти из аккаунта</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
581
app.js
581
app.js
@ -1,166 +1,473 @@
|
||||
// ===================================
|
||||
// APPLE-STYLE APP - FRONTEND
|
||||
// ===================================
|
||||
// VibeChat Messenger App
|
||||
|
||||
const API = {
|
||||
products: '/api/products',
|
||||
announcements: '/api/announcements',
|
||||
settings: '/api/settings'
|
||||
me: '/api/auth/me',
|
||||
logout: '/api/auth/logout',
|
||||
users: '/api/users',
|
||||
chats: '/api/chats',
|
||||
updateProfile: '/api/users/me'
|
||||
};
|
||||
|
||||
// Load settings and content
|
||||
async function init() {
|
||||
await loadSettings();
|
||||
await loadAnnouncements();
|
||||
await loadProducts();
|
||||
setupScrollEffects();
|
||||
setupRevealAnimations();
|
||||
}
|
||||
let currentUser = null;
|
||||
let currentChat = null;
|
||||
let allChats = [];
|
||||
let allUsers = [];
|
||||
|
||||
// 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);
|
||||
// Initialize app
|
||||
window.addEventListener('DOMContentLoaded', async () => {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
if (!token) {
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
await loadCurrentUser();
|
||||
await loadChats();
|
||||
await loadUsers();
|
||||
|
||||
// Inject emoji picker HTML
|
||||
const emojis = ['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔'];
|
||||
document.getElementById('emojiPicker').innerHTML = `
|
||||
<div class="emoji-grid">
|
||||
${emojis.map(e => `<span class="emoji-item" onclick="insertEmoji('${e}')">${e}</span>`).join('')}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Load products
|
||||
async function loadProducts() {
|
||||
// Load current user
|
||||
async function loadCurrentUser() {
|
||||
try {
|
||||
const res = await fetch(API.products);
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(API.me, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
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>';
|
||||
|
||||
if (!json.ok) {
|
||||
localStorage.removeItem('vibechat_token');
|
||||
window.location.href = 'index.html';
|
||||
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);
|
||||
currentUser = json.user;
|
||||
localStorage.setItem('vibechat_user', JSON.stringify(currentUser));
|
||||
|
||||
// Update UI
|
||||
document.getElementById('currentUserName').textContent = currentUser.username;
|
||||
document.getElementById('currentUserAvatar').src = currentUser.avatar;
|
||||
} catch (e) {
|
||||
console.error('Failed to load products:', e);
|
||||
document.getElementById('productsContainer').innerHTML = '<p style="text-align:center;color:var(--text-secondary)">Ошибка загрузки</p>';
|
||||
console.error('Failed to load user:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
// Load chats
|
||||
async function loadChats() {
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(API.chats, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
allChats = json.chats;
|
||||
renderChatList();
|
||||
}
|
||||
|
||||
// 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)`;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load chats:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reveal animations on scroll
|
||||
function setupRevealAnimations() {
|
||||
const reveals = document.querySelectorAll('.reveal');
|
||||
// Render chat list
|
||||
function renderChatList() {
|
||||
const chatList = document.getElementById('chatList');
|
||||
|
||||
const checkReveal = () => {
|
||||
reveals.forEach(el => {
|
||||
const top = el.getBoundingClientRect().top;
|
||||
const trigger = window.innerHeight * 0.85;
|
||||
if (top < trigger) {
|
||||
el.classList.add('active');
|
||||
}
|
||||
});
|
||||
};
|
||||
if (allChats.length === 0) {
|
||||
chatList.innerHTML = `
|
||||
<div style="padding:40px 20px;text-align:center;color:#86868b">
|
||||
<p>Нет чатов</p>
|
||||
<p style="font-size:13px;margin-top:8px">Нажмите + чтобы создать новый</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', checkReveal);
|
||||
checkReveal();
|
||||
chatList.innerHTML = allChats.map(chat => `
|
||||
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
|
||||
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-item-name">
|
||||
${chat.name}
|
||||
${chat.type !== 'private' ? `<span class="chat-type-badge">${chat.type === 'group' ? 'Группа' : 'Канал'}</span>` : ''}
|
||||
</div>
|
||||
<div class="chat-item-last">
|
||||
${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}
|
||||
</div>
|
||||
</div>
|
||||
${chat.lastMessage ? `<span class="chat-item-time">${formatTime(chat.lastMessage.createdAt)}</span>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
// Load users for new chat
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(API.users, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
cards.forEach(card => observer.observe(card));
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
allUsers = json.users;
|
||||
renderUserList();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load users:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 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' });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// Render user list
|
||||
function renderUserList() {
|
||||
const userList = document.getElementById('userList');
|
||||
|
||||
if (allUsers.length === 0) {
|
||||
userList.innerHTML = '<p style="padding:20px;text-align:center;color:#86868b">Нет пользователей</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
init();
|
||||
userList.innerHTML = allUsers.map(user => `
|
||||
<div class="user-item" onclick="createPrivateChat(${user.id})">
|
||||
<img src="${user.avatar}" alt="" class="user-item-avatar">
|
||||
<span class="user-item-name">${user.username}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Select chat
|
||||
async function selectChat(chatId) {
|
||||
currentChat = allChats.find(c => c.id === chatId);
|
||||
if (!currentChat) return;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('emptyChat').style.display = 'none';
|
||||
document.getElementById('messagesList').style.display = 'flex';
|
||||
document.getElementById('messageInputContainer').style.display = 'block';
|
||||
|
||||
// Update header
|
||||
document.getElementById('chatHeaderName').textContent = currentChat.name;
|
||||
document.getElementById('chatHeaderAvatar').src = currentChat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + currentChat.id;
|
||||
document.getElementById('chatHeaderStatus').textContent = currentChat.type === 'channel' ? 'Канал' : (currentChat.type === 'group' ? `${currentChat.participants?.length || 0} участников` : 'Онлайн');
|
||||
|
||||
// Render chat list again to update active state
|
||||
renderChatList();
|
||||
|
||||
// Load messages
|
||||
await loadMessages(chatId);
|
||||
}
|
||||
|
||||
// Load messages
|
||||
async function loadMessages(chatId) {
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(`${API.chats}/${chatId}/messages`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
renderMessages(json.messages);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load messages:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Render messages
|
||||
function renderMessages(messages) {
|
||||
const messagesList = document.getElementById('messagesList');
|
||||
|
||||
if (messages.length === 0) {
|
||||
messagesList.innerHTML = '<p style="text-align:center;color:#86868b;padding:40px">Сообщений пока нет</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
messagesList.innerHTML = messages.map(msg => `
|
||||
<div class="message ${msg.senderId === currentUser.id ? 'sent' : ''}">
|
||||
<img src="${msg.senderAvatar}" alt="" class="message-avatar">
|
||||
<div class="message-content">
|
||||
${msg.senderId !== currentUser.id ? `<div class="message-sender">${msg.senderName}</div>` : ''}
|
||||
<div class="message-text">${escapeHtml(msg.text)}</div>
|
||||
<div class="message-time">${formatTime(msg.createdAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Scroll to bottom
|
||||
const container = document.getElementById('messagesContainer');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// Send message
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('messageInput');
|
||||
const text = input.value.trim();
|
||||
|
||||
if (!text || !currentChat) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(`${API.chats}/${currentChat.id}/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
input.value = '';
|
||||
await loadMessages(currentChat.id);
|
||||
await loadChats(); // Update last message
|
||||
} else {
|
||||
alert('Ошибка отправки: ' + json.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle enter key
|
||||
function handleKeyPress(event) {
|
||||
if (event.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Emoji picker
|
||||
function toggleEmojiPicker() {
|
||||
const picker = document.getElementById('emojiPicker');
|
||||
picker.classList.toggle('active');
|
||||
}
|
||||
|
||||
function insertEmoji(emoji) {
|
||||
const input = document.getElementById('messageInput');
|
||||
input.value += emoji;
|
||||
input.focus();
|
||||
document.getElementById('emojiPicker').classList.remove('active');
|
||||
}
|
||||
|
||||
// Close emoji picker on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
const picker = document.getElementById('emojiPicker');
|
||||
const btn = document.querySelector('.btn-emoji');
|
||||
if (!picker.contains(e.target) && !btn.contains(e.target)) {
|
||||
picker.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// New chat modal
|
||||
function showNewChatModal() {
|
||||
document.getElementById('newChatModal').classList.add('active');
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
function closeNewChatModal() {
|
||||
document.getElementById('newChatModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
document.getElementById('chatTab').style.display = tab === 'chat' ? 'block' : 'none';
|
||||
document.getElementById('groupTab').style.display = tab === 'group' ? 'block' : 'none';
|
||||
document.getElementById('channelTab').style.display = tab === 'channel' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Create private chat
|
||||
async function createPrivateChat(userId) {
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(API.chats, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ type: 'private', participantId: userId })
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
closeNewChatModal();
|
||||
await loadChats();
|
||||
selectChat(json.chat.id);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Create group
|
||||
async function createGroup() {
|
||||
const name = document.getElementById('groupName').value.trim();
|
||||
if (!name) return alert('Введите название группы');
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(API.chats, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ type: 'group', name })
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
closeNewChatModal();
|
||||
document.getElementById('groupName').value = '';
|
||||
await loadChats();
|
||||
selectChat(json.chat.id);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Create channel
|
||||
async function createChannel() {
|
||||
const name = document.getElementById('channelName').value.trim();
|
||||
if (!name) return alert('Введите название канала');
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(API.chats, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ type: 'channel', name })
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
closeNewChatModal();
|
||||
document.getElementById('channelName').value = '';
|
||||
await loadChats();
|
||||
selectChat(json.chat.id);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Settings
|
||||
function openSettings() {
|
||||
const user = JSON.parse(localStorage.getItem('vibechat_user'));
|
||||
document.getElementById('settingsAvatar').src = user.avatar;
|
||||
document.getElementById('settingsUsername').value = user.username;
|
||||
document.getElementById('settingsBio').value = user.bio || '';
|
||||
document.getElementById('settingsModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
document.getElementById('settingsModal').classList.remove('active');
|
||||
}
|
||||
|
||||
async function updateProfile(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('settingsUsername').value;
|
||||
const bio = document.getElementById('settingsBio').value;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
const res = await fetch(API.updateProfile, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ username, bio })
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
localStorage.setItem('vibechat_user', JSON.stringify(json.user));
|
||||
currentUser = json.user;
|
||||
document.getElementById('currentUserName').textContent = currentUser.username;
|
||||
document.getElementById('currentUserAvatar').src = currentUser.avatar;
|
||||
closeSettings();
|
||||
alert('Профиль обновлён! ✅');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function changeAvatar() {
|
||||
const url = prompt('Введите URL аватара:', currentUser.avatar);
|
||||
if (url) {
|
||||
updateProfile({ preventDefault: () => {} });
|
||||
}
|
||||
}
|
||||
|
||||
// Logout
|
||||
async function logout() {
|
||||
if (!confirm('Вы уверены, что хотите выйти?')) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
await fetch(API.logout, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
localStorage.removeItem('vibechat_token');
|
||||
localStorage.removeItem('vibechat_user');
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
|
||||
// Filter chats
|
||||
function filterChats() {
|
||||
const query = document.getElementById('searchInput').value.toLowerCase();
|
||||
const filtered = allChats.filter(c => c.name.toLowerCase().includes(query));
|
||||
|
||||
const chatList = document.getElementById('chatList');
|
||||
chatList.innerHTML = filtered.map(chat => `
|
||||
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
|
||||
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
|
||||
<div class="chat-item-info">
|
||||
<div class="chat-item-name">${chat.name}</div>
|
||||
<div class="chat-item-last">${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatTime(isoString) {
|
||||
const date = new Date(isoString);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
if (diff < 60000) return 'Только что';
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + ' мин назад';
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + ' ч назад';
|
||||
if (diff < 604800000) return Math.floor(diff / 86400000) + ' дн назад';
|
||||
|
||||
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
66
data.json
66
data.json
@ -1,62 +1,8 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
"users": [],
|
||||
"chats": [],
|
||||
"messages": [],
|
||||
"groups": [],
|
||||
"channels": [],
|
||||
"sessions": []
|
||||
}
|
||||
|
||||
259
index.html
259
index.html
@ -3,171 +3,136 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Apple Style — Будущее здесь</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<title>VibeChat — Мессенджер будущего</title>
|
||||
<link rel="stylesheet" href="landing.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Announcement Bar -->
|
||||
<div id="announcementBar" class="announcement-bar"></div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="nav" id="nav">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="logo"></a>
|
||||
<div class="nav-links">
|
||||
<a href="#products">Продукты</a>
|
||||
<a href="#features">Возможности</a>
|
||||
<a href="#vision">Vision</a>
|
||||
<a href="admin.html" class="admin-link">Админка</a>
|
||||
<!-- Landing Page -->
|
||||
<div id="landingPage">
|
||||
<nav class="landing-nav">
|
||||
<div class="nav-container">
|
||||
<div class="logo">💬 VibeChat</div>
|
||||
<button class="btn-login" onclick="showAuth('login')">Войти</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero" id="hero">
|
||||
<div class="hero-bg" id="heroBg"></div>
|
||||
<div class="hero-overlay"></div>
|
||||
<div class="hero-content">
|
||||
<h1 id="heroTitle" class="hero-title"></h1>
|
||||
<p id="heroSubtitle" class="hero-subtitle"></p>
|
||||
<div class="hero-cta">
|
||||
<a href="#products" class="btn btn-primary">Смотреть продукты</a>
|
||||
<a href="#vision" class="btn btn-secondary">Узнать больше</a>
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<h1>Общение без границ</h1>
|
||||
<p>Быстрый. Безопасный. С End-to-End шифрованием.</p>
|
||||
<div class="hero-buttons">
|
||||
<button class="btn-primary" onclick="showAuth('register')">Начать бесплатно</button>
|
||||
<button class="btn-secondary" onclick="scrollToFeatures()">Узнать больше</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-indicator">
|
||||
<div class="mouse">
|
||||
<div class="wheel"></div>
|
||||
<div class="hero-image">
|
||||
<div class="chat-mockup">
|
||||
<div class="mockup-header">
|
||||
<div class="mockup-avatar"></div>
|
||||
<div class="mockup-lines">
|
||||
<div class="line short"></div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mockup-messages">
|
||||
<div class="message received"></div>
|
||||
<div class="message sent"></div>
|
||||
<div class="message received"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Product Showcase (Apple-style scroll) -->
|
||||
<section class="product-showcase" id="products">
|
||||
<div class="showcase-intro">
|
||||
<h2 class="showcase-title">Выберите свой путь</h2>
|
||||
<p class="showcase-subtitle">Инновации в каждой детали</p>
|
||||
</div>
|
||||
<div class="products-container" id="productsContainer">
|
||||
<!-- Products will be loaded here -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 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>
|
||||
<section class="features" id="features">
|
||||
<h2>Почему VibeChat?</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card reveal">
|
||||
<div class="feature-icon">🚀</div>
|
||||
<h3>Производительность</h3>
|
||||
<p>Чипы M-серии меняют правила игры</p>
|
||||
</div>
|
||||
<div class="feature-card reveal">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔒</div>
|
||||
<h3>E2E Шифрование</h3>
|
||||
<p>Ваши сообщения видят только отправитель и получатель</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h3>Молниеносно</h3>
|
||||
<p>Мгновенная доставка сообщений в любую точку мира</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h3>Группы и каналы</h3>
|
||||
<p>Создавайте группы до 1000 человек и публичные каналы</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎨</div>
|
||||
<h3>Эмодзи и стикеры</h3>
|
||||
<p>Тысячи эмодзи и стикеров для выражения эмоций</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📱</div>
|
||||
<h3>Кроссплатформенно</h3>
|
||||
<p>Работает на любом устройстве через браузер</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔐</div>
|
||||
<h3>Безопасность</h3>
|
||||
<p>Защита данных на аппаратном уровне</p>
|
||||
</div>
|
||||
<div class="feature-card reveal">
|
||||
<div class="feature-icon">🌱</div>
|
||||
<h3>Экология</h3>
|
||||
<p>Углеродная нейтральность к 2030</p>
|
||||
</div>
|
||||
<div class="feature-card reveal">
|
||||
<div class="feature-icon">🤖</div>
|
||||
<h3>ИИ</h3>
|
||||
<p>Apple Intelligence в каждом устройстве</p>
|
||||
<p>Двухфакторная аутентификация и секретные чаты</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Vision Section -->
|
||||
<section class="vision" id="vision">
|
||||
<div class="vision-content">
|
||||
<h2 class="vision-title reveal">Vision Pro</h2>
|
||||
<p class="vision-subtitle reveal">Эра пространственных вычислений наступила</p>
|
||||
<div class="vision-features reveal">
|
||||
<div class="vision-feature">
|
||||
<span class="vision-feature-icon">👁️</span>
|
||||
<div>
|
||||
<h4>eyeSight</h4>
|
||||
<p>Оставайтесь на связи с окружающими</p>
|
||||
<section class="cta">
|
||||
<h2>Готовы начать?</h2>
|
||||
<p>Присоединяйтесь к тысячам пользователей уже сегодня</p>
|
||||
<button class="btn-primary" onclick="showAuth('register')">Создать аккаунт</button>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>© 2026 VibeChat. Все права защищены.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Auth Modal -->
|
||||
<div id="authModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<button class="modal-close" onclick="closeAuth()">×</button>
|
||||
|
||||
<div id="loginForm">
|
||||
<h2>Вход</h2>
|
||||
<form onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" id="loginEmail" required placeholder="you@example.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="vision-feature">
|
||||
<span class="vision-feature-icon">🎭</span>
|
||||
<div>
|
||||
<h4>visionOS</h4>
|
||||
<p>Интуитивный пространственный интерфейс</p>
|
||||
<div class="form-group">
|
||||
<label>Пароль</label>
|
||||
<input type="password" id="loginPassword" required placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
<div class="vision-feature">
|
||||
<span class="vision-feature-icon">🎬</span>
|
||||
<div>
|
||||
<h4>Immersion</h4>
|
||||
<p>Погружение как в кинотеатре</p>
|
||||
<button type="submit" class="btn-submit">Войти</button>
|
||||
</form>
|
||||
<p class="auth-switch">Нет аккаунта? <a href="#" onclick="showAuth('register')">Зарегистрироваться</a></p>
|
||||
</div>
|
||||
|
||||
<div id="registerForm" style="display:none">
|
||||
<h2>Регистрация</h2>
|
||||
<form onsubmit="handleRegister(event)">
|
||||
<div class="form-group">
|
||||
<label>Имя пользователя</label>
|
||||
<input type="text" id="regUsername" required placeholder="username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<input type="email" id="regEmail" required placeholder="you@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Пароль</label>
|
||||
<input type="password" id="regPassword" required placeholder="••••••••" minlength="6">
|
||||
</div>
|
||||
<button type="submit" class="btn-submit">Создать аккаунт</button>
|
||||
</form>
|
||||
<p class="auth-switch">Уже есть аккаунт? <a href="#" onclick="showAuth('login')">Войти</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vision-image reveal">
|
||||
<img src="https://picsum.photos/1200/800?random=50" alt="Vision Pro">
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Big Image Scroll Section -->
|
||||
<section class="scroll-gallery" id="gallery">
|
||||
<div class="gallery-item" id="galleryItem1">
|
||||
<img src="https://picsum.photos/1920/1080?random=60" alt="">
|
||||
<div class="gallery-caption">
|
||||
<h3>Дизайн</h3>
|
||||
<p>Каждая линия имеет значение</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-item" id="galleryItem2">
|
||||
<img src="https://picsum.photos/1920/1080?random=61" alt="">
|
||||
<div class="gallery-caption">
|
||||
<h3>Инновации</h3>
|
||||
<p>Технологии, которые вдохновляют</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-item" id="galleryItem3">
|
||||
<img src="https://picsum.photos/1920/1080?random=62" alt="">
|
||||
<div class="gallery-caption">
|
||||
<h3>Будущее</h3>
|
||||
<p>Создаём завтрашний день сегодня</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="cta-section">
|
||||
<h2 class="cta-title reveal">Готовы к будущему?</h2>
|
||||
<p class="cta-subtitle reveal">Откройте для себя мир инноваций уже сегодня</p>
|
||||
<div class="cta-buttons reveal">
|
||||
<a href="#products" class="btn btn-primary">Выбрать устройство</a>
|
||||
<a href="#" class="btn btn-secondary">Связаться с нами</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<div class="footer-content">
|
||||
<div class="footer-links">
|
||||
<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>
|
||||
</footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<script src="landing.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
433
landing.css
Normal file
433
landing.css
Normal file
@ -0,0 +1,433 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #ffffff;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Landing Navigation */
|
||||
.landing-nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 20px 40px;
|
||||
background: rgba(10, 10, 10, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
z-index: 100;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
padding: 10px 24px;
|
||||
background: transparent;
|
||||
border: 2px solid rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border-radius: 980px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
border-color: white;
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 60px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 140px 40px 60px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: clamp(48px, 6vw, 80px);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #a0a0a5 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-content p {
|
||||
font-size: clamp(20px, 3vw, 24px);
|
||||
color: #86868b;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 16px 32px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
border-radius: 980px;
|
||||
color: white;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 16px 32px;
|
||||
background: transparent;
|
||||
border: 2px solid rgba(255,255,255,0.2);
|
||||
border-radius: 980px;
|
||||
color: white;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
/* Chat Mockup */
|
||||
.hero-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-mockup {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: rgba(29, 29, 31, 0.8);
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
box-shadow: 0 40px 80px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.mockup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.mockup-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
}
|
||||
|
||||
.mockup-lines {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.line {
|
||||
height: 12px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.line.short {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.mockup-messages {
|
||||
padding-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.message {
|
||||
height: 40px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.message.sent {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
align-self: flex-end;
|
||||
width: 70%;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message.received {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
/* Features */
|
||||
.features {
|
||||
padding: 140px 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.features h2 {
|
||||
font-size: clamp(36px, 5vw, 56px);
|
||||
text-align: center;
|
||||
margin-bottom: 80px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: rgba(29, 29, 31, 0.6);
|
||||
border-radius: 20px;
|
||||
padding: 40px 32px;
|
||||
border: 1px solid rgba(255,255,255,0.05);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-8px);
|
||||
border-color: rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: #86868b;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* CTA */
|
||||
.cta {
|
||||
padding: 140px 40px;
|
||||
text-align: center;
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(29, 29, 31, 0.5) 100%);
|
||||
}
|
||||
|
||||
.cta h2 {
|
||||
font-size: clamp(36px, 5vw, 56px);
|
||||
margin-bottom: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cta p {
|
||||
font-size: 20px;
|
||||
color: #86868b;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #86868b;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1d1d1f;
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
position: relative;
|
||||
animation: modalSlide 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes modalSlide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #86868b;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #86868b;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 14px 18px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.auth-switch {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
color: #86868b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-switch a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auth-switch a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
padding-top: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.landing-nav {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.features, .cta {
|
||||
padding: 80px 24px;
|
||||
}
|
||||
}
|
||||
107
landing.js
Normal file
107
landing.js
Normal file
@ -0,0 +1,107 @@
|
||||
// VibeChat Landing Page JavaScript
|
||||
|
||||
const API = {
|
||||
register: '/api/auth/register',
|
||||
login: '/api/auth/login'
|
||||
};
|
||||
|
||||
let authToken = null;
|
||||
|
||||
// Show auth modal
|
||||
function showAuth(type) {
|
||||
const modal = document.getElementById('authModal');
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
|
||||
modal.classList.add('active');
|
||||
|
||||
if (type === 'login') {
|
||||
loginForm.style.display = 'block';
|
||||
registerForm.style.display = 'none';
|
||||
} else {
|
||||
loginForm.style.display = 'none';
|
||||
registerForm.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeAuth() {
|
||||
document.getElementById('authModal').classList.remove('active');
|
||||
}
|
||||
|
||||
// Close on outside click
|
||||
document.getElementById('authModal')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'authModal') closeAuth();
|
||||
});
|
||||
|
||||
// Scroll to features
|
||||
function scrollToFeatures() {
|
||||
document.getElementById('features').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Handle login
|
||||
async function handleLogin(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const email = document.getElementById('loginEmail').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
try {
|
||||
const res = await fetch(API.login, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
authToken = json.token;
|
||||
localStorage.setItem('vibechat_token', authToken);
|
||||
localStorage.setItem('vibechat_user', JSON.stringify(json.user));
|
||||
window.location.href = 'app.html';
|
||||
} else {
|
||||
alert('Ошибка: ' + json.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка подключения: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle register
|
||||
async function handleRegister(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const username = document.getElementById('regUsername').value;
|
||||
const email = document.getElementById('regEmail').value;
|
||||
const password = document.getElementById('regPassword').value;
|
||||
|
||||
try {
|
||||
const res = await fetch(API.register, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password })
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.ok) {
|
||||
authToken = json.token;
|
||||
localStorage.setItem('vibechat_token', authToken);
|
||||
localStorage.setItem('vibechat_user', JSON.stringify(json.user));
|
||||
window.location.href = 'app.html';
|
||||
} else {
|
||||
alert('Ошибка: ' + json.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка подключения: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if already logged in
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const token = localStorage.getItem('vibechat_token');
|
||||
if (token) {
|
||||
window.location.href = 'app.html';
|
||||
}
|
||||
});
|
||||
10
package-lock.json
generated
Normal file
10
package-lock.json
generated
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "apple-style-app",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "apple-style-app"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "apple-style-app",
|
||||
"name": "vibechat-messenger",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
359
server.js
359
server.js
@ -1,12 +1,14 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const DATA = path.join(__dirname, "data.json");
|
||||
const SECRET_KEY = crypto.randomBytes(32).toString("hex");
|
||||
|
||||
function readData() {
|
||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { products: [], announcements: [], siteSettings: {} }; }
|
||||
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { users: [], chats: [], messages: [], groups: [], channels: [], sessions: [] }; }
|
||||
}
|
||||
|
||||
function writeData(data) {
|
||||
@ -31,93 +33,321 @@ function notFound(res) {
|
||||
res.end("Not Found");
|
||||
}
|
||||
|
||||
// Генерация токена сессии
|
||||
function generateToken(userId) {
|
||||
return crypto.createHmac("sha256", SECRET_KEY).update(userId + Date.now()).digest("hex");
|
||||
}
|
||||
|
||||
// Проверка токена
|
||||
function verifyToken(token) {
|
||||
const data = readData();
|
||||
const session = data.sessions.find(s => s.token === token && new Date(s.expires) > new Date());
|
||||
if (!session) return null;
|
||||
return data.users.find(u => u.id === session.userId);
|
||||
}
|
||||
|
||||
// Хеширование пароля
|
||||
function hashPassword(password) {
|
||||
return crypto.createHash("sha256").update(password + "vibechat_salt").digest("hex");
|
||||
}
|
||||
|
||||
// E2E шифрование (упрощённое AES-GCM)
|
||||
function encryptMessage(text, key) {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", Buffer.from(key.slice(0, 32).padEnd(32, "0")), iv);
|
||||
let encrypted = cipher.update(text, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
const authTag = cipher.getAuthTag().toString("hex");
|
||||
return { iv: iv.toString("hex"), encrypted, authTag };
|
||||
}
|
||||
|
||||
function decryptMessage(encryptedData, key) {
|
||||
try {
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", Buffer.from(key.slice(0, 32).padEnd(32, "0")), Buffer.from(encryptedData.iv, "hex"));
|
||||
decipher.setAuthTag(Buffer.from(encryptedData.authTag, "hex"));
|
||||
let decrypted = decipher.update(encryptedData.encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
return "[Ошибка расшифровки]";
|
||||
}
|
||||
}
|
||||
|
||||
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) });
|
||||
},
|
||||
// ========== AUTH ==========
|
||||
"POST /api/auth/register": async (req, res) => {
|
||||
const { username, email, password } = await parseBody(req);
|
||||
if (!username || !email || !password) return json(res, { ok: false, error: "Все поля обязательны" }, 400);
|
||||
|
||||
"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 });
|
||||
},
|
||||
if (data.users.find(u => u.email === email)) return json(res, { ok: false, error: "Email уже зарегистрирован" }, 400);
|
||||
if (data.users.find(u => u.username === username)) return json(res, { ok: false, error: "Имя пользователя занято" }, 400);
|
||||
|
||||
"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 = {
|
||||
const user = {
|
||||
id: Date.now(),
|
||||
...body,
|
||||
order: data.products.length + 1
|
||||
username,
|
||||
email,
|
||||
passwordHash: hashPassword(password),
|
||||
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`,
|
||||
bio: "",
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
data.products.push(newProduct);
|
||||
|
||||
data.users.push(user);
|
||||
writeData(data);
|
||||
json(res, { ok: true, data: newProduct }, 201);
|
||||
|
||||
const token = generateToken(user.id);
|
||||
data.sessions.push({ token, userId: user.id, expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() });
|
||||
writeData(data);
|
||||
|
||||
json(res, { ok: true, token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } });
|
||||
},
|
||||
|
||||
"PUT /api/admin/products/:id": async (req, res, params) => {
|
||||
const body = await parseBody(req);
|
||||
"POST /api/auth/login": async (req, res) => {
|
||||
const { email, password } = await parseBody(req);
|
||||
if (!email || !password) return json(res, { ok: false, error: "Email и пароль обязательны" }, 400);
|
||||
|
||||
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) };
|
||||
const user = data.users.find(u => u.email === email && u.passwordHash === hashPassword(password));
|
||||
if (!user) return json(res, { ok: false, error: "Неверный email или пароль" }, 401);
|
||||
|
||||
const token = generateToken(user.id);
|
||||
data.sessions.push({ token, userId: user.id, expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() });
|
||||
writeData(data);
|
||||
json(res, { ok: true, data: data.products[idx] });
|
||||
|
||||
json(res, { ok: true, token, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar } });
|
||||
},
|
||||
|
||||
"DELETE /api/admin/products/:id": (req, res, params) => {
|
||||
"POST /api/auth/logout": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Токен не предоставлен" }, 401);
|
||||
|
||||
const data = readData();
|
||||
data.products = data.products.filter(p => p.id !== parseInt(params.id));
|
||||
data.sessions = data.sessions.filter(s => s.token !== token);
|
||||
writeData(data);
|
||||
json(res, { ok: true, deleted: parseInt(params.id) });
|
||||
json(res, { ok: true });
|
||||
},
|
||||
|
||||
"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);
|
||||
"GET /api/auth/me": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
|
||||
const user = verifyToken(token);
|
||||
if (!user) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
json(res, { ok: true, user: { id: user.id, username: user.username, email: user.email, avatar: user.avatar, bio: user.bio } });
|
||||
},
|
||||
|
||||
"PUT /api/admin/announcements/:id": async (req, res, params) => {
|
||||
const body = await parseBody(req);
|
||||
// ========== USERS ==========
|
||||
"GET /api/users": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
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] });
|
||||
const users = data.users.filter(u => u.id !== currentUser.id).map(u => ({
|
||||
id: u.id, username: u.username, avatar: u.avatar
|
||||
}));
|
||||
json(res, { ok: true, users });
|
||||
},
|
||||
|
||||
"DELETE /api/admin/announcements/:id": (req, res, params) => {
|
||||
"PUT /api/users/me": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const { username, bio, avatar } = await parseBody(req);
|
||||
const data = readData();
|
||||
data.announcements = data.announcements.filter(a => a.id !== parseInt(params.id));
|
||||
const idx = data.users.findIndex(u => u.id === currentUser.id);
|
||||
if (idx === -1) return json(res, { ok: false, error: "Пользователь не найден" }, 404);
|
||||
|
||||
if (username) data.users[idx].username = username;
|
||||
if (bio !== undefined) data.users[idx].bio = bio;
|
||||
if (avatar) data.users[idx].avatar = avatar;
|
||||
writeData(data);
|
||||
json(res, { ok: true, deleted: parseInt(params.id) });
|
||||
|
||||
json(res, { ok: true, user: data.users[idx] });
|
||||
},
|
||||
|
||||
"PUT /api/admin/settings": async (req, res) => {
|
||||
const body = await parseBody(req);
|
||||
// ========== CHATS ==========
|
||||
"GET /api/chats": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const data = readData();
|
||||
data.siteSettings = { ...data.siteSettings, ...body };
|
||||
const userChats = data.chats.filter(c => c.participants.includes(currentUser.id));
|
||||
|
||||
const chatsWithDetails = userChats.map(chat => {
|
||||
const otherUserId = chat.type === "private" ? chat.participants.find(p => p !== currentUser.id) : null;
|
||||
const otherUser = otherUserId ? data.users.find(u => u.id === otherUserId) : null;
|
||||
const lastMessage = data.messages.filter(m => m.chatId === chat.id).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))[0];
|
||||
|
||||
return {
|
||||
...chat,
|
||||
name: chat.type === "private" ? otherUser?.username : chat.name,
|
||||
avatar: chat.type === "private" ? otherUser?.avatar : chat.avatar,
|
||||
lastMessage: lastMessage ? { text: lastMessage.text, createdAt: lastMessage.createdAt } : null
|
||||
};
|
||||
});
|
||||
|
||||
json(res, { ok: true, chats: chatsWithDetails });
|
||||
},
|
||||
|
||||
"POST /api/chats": async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const { type, participantId, name } = await parseBody(req);
|
||||
const data = readData();
|
||||
|
||||
if (type === "private") {
|
||||
const existingChat = data.chats.find(c => c.type === "private" && c.participants.includes(currentUser.id) && c.participants.includes(participantId));
|
||||
if (existingChat) return json(res, { ok: true, chat: existingChat });
|
||||
|
||||
const chat = {
|
||||
id: Date.now(),
|
||||
type: "private",
|
||||
participants: [currentUser.id, participantId],
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
data.chats.push(chat);
|
||||
writeData(data);
|
||||
json(res, { ok: true, chat });
|
||||
} else if (type === "group") {
|
||||
const chat = {
|
||||
id: Date.now(),
|
||||
type: "group",
|
||||
name: name || "Группа",
|
||||
participants: [currentUser.id],
|
||||
avatar: `https://api.dicebear.com/7.x/identicon/svg?seed=${Date.now()}`,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
data.chats.push(chat);
|
||||
writeData(data);
|
||||
json(res, { ok: true, chat });
|
||||
} else if (type === "channel") {
|
||||
const channel = {
|
||||
id: Date.now(),
|
||||
type: "channel",
|
||||
name: name || "Канал",
|
||||
ownerId: currentUser.id,
|
||||
subscribers: [currentUser.id],
|
||||
avatar: `https://api.dicebear.com/7.x/identicon/svg?seed=${Date.now()}`,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
data.channels.push(channel);
|
||||
data.chats.push({ id: channel.id, type: "channel", name: channel.name, ownerId: channel.id, subscribers: [currentUser.id], avatar: channel.avatar, createdAt: channel.createdAt });
|
||||
writeData(data);
|
||||
json(res, { ok: true, chat: channel });
|
||||
}
|
||||
},
|
||||
|
||||
"GET /api/chats/:id/messages": async (req, res, params) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const chatId = parseInt(params.id);
|
||||
const data = readData();
|
||||
const chat = data.chats.find(c => c.id === chatId);
|
||||
if (!chat || !chat.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Чат не найден" }, 404);
|
||||
|
||||
const messages = data.messages.filter(m => m.chatId === chatId).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
|
||||
|
||||
// E2E расшифровка
|
||||
const userKey = hashPassword(currentUser.email + currentUser.id);
|
||||
const decryptedMessages = messages.map(m => ({
|
||||
...m,
|
||||
text: decryptMessage(m.encrypted, userKey)
|
||||
}));
|
||||
|
||||
json(res, { ok: true, messages: decryptedMessages });
|
||||
},
|
||||
|
||||
"POST /api/chats/:id/messages": async (req, res, params) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const chatId = parseInt(params.id);
|
||||
const { text, type = "text" } = await parseBody(req);
|
||||
const data = readData();
|
||||
|
||||
const chat = data.chats.find(c => c.id === chatId);
|
||||
if (!chat || !chat.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Чат не найден" }, 404);
|
||||
|
||||
// E2E шифрование
|
||||
const userKey = hashPassword(currentUser.email + currentUser.id);
|
||||
const encrypted = encryptMessage(text, userKey);
|
||||
|
||||
const message = {
|
||||
id: Date.now(),
|
||||
chatId,
|
||||
senderId: currentUser.id,
|
||||
senderName: currentUser.username,
|
||||
senderAvatar: currentUser.avatar,
|
||||
text,
|
||||
encrypted,
|
||||
type,
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
data.messages.push(message);
|
||||
writeData(data);
|
||||
json(res, { ok: true, data: data.siteSettings });
|
||||
json(res, { ok: true, message: { ...message, text } });
|
||||
},
|
||||
|
||||
// ========== GROUPS ==========
|
||||
"POST /api/groups/:id/members": async (req, res, params) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const groupId = parseInt(params.id);
|
||||
const { userId } = await parseBody(req);
|
||||
const data = readData();
|
||||
|
||||
const group = data.chats.find(c => c.id === groupId && c.type === "group");
|
||||
if (!group || !group.participants.includes(currentUser.id)) return json(res, { ok: false, error: "Группа не найдена" }, 404);
|
||||
|
||||
if (!group.participants.includes(userId)) {
|
||||
group.participants.push(userId);
|
||||
writeData(data);
|
||||
}
|
||||
json(res, { ok: true, group });
|
||||
},
|
||||
|
||||
// ========== CHANNELS ==========
|
||||
"POST /api/channels/:id/subscribe": async (req, res, params) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return json(res, { ok: false, error: "Не авторизован" }, 401);
|
||||
const currentUser = verifyToken(token);
|
||||
if (!currentUser) return json(res, { ok: false, error: "Сессия истекла" }, 401);
|
||||
|
||||
const channelId = parseInt(params.id);
|
||||
const data = readData();
|
||||
|
||||
const channel = data.channels.find(c => c.id === channelId);
|
||||
if (!channel) return json(res, { ok: false, error: "Канал не найден" }, 404);
|
||||
|
||||
if (!channel.subscribers.includes(currentUser.id)) {
|
||||
channel.subscribers.push(currentUser.id);
|
||||
const chatIdx = data.chats.findIndex(c => c.id === channelId);
|
||||
if (chatIdx !== -1 && !data.chats[chatIdx].subscribers.includes(currentUser.id)) {
|
||||
data.chats[chatIdx].subscribers.push(currentUser.id);
|
||||
}
|
||||
writeData(data);
|
||||
}
|
||||
json(res, { ok: true, channel });
|
||||
}
|
||||
};
|
||||
|
||||
@ -125,10 +355,7 @@ 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"
|
||||
".json": "application/json; charset=utf-8"
|
||||
};
|
||||
|
||||
http.createServer(async (req, res) => {
|
||||
@ -137,7 +364,7 @@ http.createServer(async (req, res) => {
|
||||
|
||||
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");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
|
||||
if (method === "OPTIONS") {
|
||||
res.writeHead(204);
|
||||
@ -182,4 +409,4 @@ http.createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
notFound(res);
|
||||
}).listen(PORT, () => console.log("Server running on port " + PORT));
|
||||
}).listen(PORT, () => console.log("VibeChat server running on port " + PORT));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user