feat: redesign landing sections and interactions
Refresh visual design across hero, map, features, FAQ, and performance sections with tighter spacing, richer animations, updated branding assets, and localization/content tweaks.
This commit is contained in:
+9
-9
@@ -1,9 +1,9 @@
|
|||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
README.md
|
README.md
|
||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
.cache
|
.cache
|
||||||
|
|||||||
@@ -1,187 +1,187 @@
|
|||||||
# 🐛 Отладка Gekon
|
# 🐛 Отладка Gekon
|
||||||
|
|
||||||
## Проблема: Не видно частицы
|
## Проблема: Не видно частицы
|
||||||
|
|
||||||
### Возможные причины:
|
### Возможные причины:
|
||||||
|
|
||||||
1. **Canvas не поддерживается браузером**
|
1. **Canvas не поддерживается браузером**
|
||||||
- Откройте консоль браузера (F12)
|
- Откройте консоль браузера (F12)
|
||||||
- Проверьте ошибки JavaScript
|
- Проверьте ошибки JavaScript
|
||||||
|
|
||||||
2. **Частицы за другими элементами**
|
2. **Частицы за другими элементами**
|
||||||
- Проверьте z-index
|
- Проверьте z-index
|
||||||
- Убедитесь, что ParticlesBackground рендерится
|
- Убедитесь, что ParticlesBackground рендерится
|
||||||
|
|
||||||
3. **Opacity слишком низкая**
|
3. **Opacity слишком низкая**
|
||||||
- Частицы имеют opacity: 0.4
|
- Частицы имеют opacity: 0.4
|
||||||
- На светлом фоне могут быть не видны
|
- На светлом фоне могут быть не видны
|
||||||
|
|
||||||
### Быстрая проверка:
|
### Быстрая проверка:
|
||||||
|
|
||||||
1. Откройте http://localhost:5173 (или 8080)
|
1. Откройте http://localhost:5173 (или 8080)
|
||||||
2. Нажмите F12 (DevTools)
|
2. Нажмите F12 (DevTools)
|
||||||
3. Перейдите в Console
|
3. Перейдите в Console
|
||||||
4. Проверьте ошибки
|
4. Проверьте ошибки
|
||||||
|
|
||||||
### Типичные ошибки:
|
### Типичные ошибки:
|
||||||
|
|
||||||
#### Ошибка: "Cannot read property 'getContext' of null"
|
#### Ошибка: "Cannot read property 'getContext' of null"
|
||||||
**Решение:** Canvas ref не инициализирован
|
**Решение:** Canvas ref не инициализирован
|
||||||
```typescript
|
```typescript
|
||||||
// Проверьте, что canvas монтируется
|
// Проверьте, что canvas монтируется
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
console.error('Canvas not found!');
|
console.error('Canvas not found!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ...
|
// ...
|
||||||
}, []);
|
}, []);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Ошибка: "ResizeObserver loop limit exceeded"
|
#### Ошибка: "ResizeObserver loop limit exceeded"
|
||||||
**Решение:** Это предупреждение, можно игнорировать
|
**Решение:** Это предупреждение, можно игнорировать
|
||||||
|
|
||||||
#### Частицы не видны
|
#### Частицы не видны
|
||||||
**Решение 1:** Увеличьте opacity
|
**Решение 1:** Увеличьте opacity
|
||||||
```typescript
|
```typescript
|
||||||
// В ParticlesBackground.tsx, строка ~60
|
// В ParticlesBackground.tsx, строка ~60
|
||||||
ctx.fillStyle = `rgba(16, 185, 129, ${particle.opacity * 2})`; // Увеличили в 2 раза
|
ctx.fillStyle = `rgba(16, 185, 129, ${particle.opacity * 2})`; // Увеличили в 2 раза
|
||||||
```
|
```
|
||||||
|
|
||||||
**Решение 2:** Увеличьте размер частиц
|
**Решение 2:** Увеличьте размер частиц
|
||||||
```typescript
|
```typescript
|
||||||
// В ParticlesBackground.tsx, строка ~40
|
// В ParticlesBackground.tsx, строка ~40
|
||||||
size: Math.random() * 4 + 2, // Было: * 2 + 1
|
size: Math.random() * 4 + 2, // Было: * 2 + 1
|
||||||
```
|
```
|
||||||
|
|
||||||
**Решение 3:** Увеличьте количество
|
**Решение 3:** Увеличьте количество
|
||||||
```typescript
|
```typescript
|
||||||
// В ParticlesBackground.tsx, строка ~35
|
// В ParticlesBackground.tsx, строка ~35
|
||||||
const particleCount = Math.min(200, ...); // Было: 100
|
const particleCount = Math.min(200, ...); // Было: 100
|
||||||
```
|
```
|
||||||
|
|
||||||
### Проверка рендеринга:
|
### Проверка рендеринга:
|
||||||
|
|
||||||
Добавьте console.log в ParticlesBackground:
|
Добавьте console.log в ParticlesBackground:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('ParticlesBackground mounted!');
|
console.log('ParticlesBackground mounted!');
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
console.error('Canvas not found!');
|
console.error('Canvas not found!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Canvas found:', canvas.width, 'x', canvas.height);
|
console.log('Canvas found:', canvas.width, 'x', canvas.height);
|
||||||
console.log('Particles count:', particlesRef.current.length);
|
console.log('Particles count:', particlesRef.current.length);
|
||||||
// ...
|
// ...
|
||||||
}, []);
|
}, []);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Проверка в браузере:
|
### Проверка в браузере:
|
||||||
|
|
||||||
1. Откройте DevTools (F12)
|
1. Откройте DevTools (F12)
|
||||||
2. Elements tab
|
2. Elements tab
|
||||||
3. Найдите `<canvas>` элемент
|
3. Найдите `<canvas>` элемент
|
||||||
4. Проверьте стили:
|
4. Проверьте стили:
|
||||||
- `position: absolute`
|
- `position: absolute`
|
||||||
- `inset: 0`
|
- `inset: 0`
|
||||||
- `opacity: 0.4`
|
- `opacity: 0.4`
|
||||||
- `z-index` не перекрыт
|
- `z-index` не перекрыт
|
||||||
|
|
||||||
### Временное решение (для теста):
|
### Временное решение (для теста):
|
||||||
|
|
||||||
Сделайте частицы очень заметными:
|
Сделайте частицы очень заметными:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// В ParticlesBackground.tsx
|
// В ParticlesBackground.tsx
|
||||||
// Строка ~60 - увеличьте opacity
|
// Строка ~60 - увеличьте opacity
|
||||||
ctx.fillStyle = `rgba(16, 185, 129, 1)`; // Полная непрозрачность
|
ctx.fillStyle = `rgba(16, 185, 129, 1)`; // Полная непрозрачность
|
||||||
|
|
||||||
// Строка ~40 - увеличьте размер
|
// Строка ~40 - увеличьте размер
|
||||||
size: Math.random() * 10 + 5, // Большие частицы
|
size: Math.random() * 10 + 5, // Большие частицы
|
||||||
|
|
||||||
// Строка ~35 - больше частиц
|
// Строка ~35 - больше частиц
|
||||||
const particleCount = 200;
|
const particleCount = 200;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Проверка карты серверов:
|
### Проверка карты серверов:
|
||||||
|
|
||||||
Если карта не работает:
|
Если карта не работает:
|
||||||
|
|
||||||
1. Откройте консоль (F12)
|
1. Откройте консоль (F12)
|
||||||
2. Проверьте ошибки в ServerMap
|
2. Проверьте ошибки в ServerMap
|
||||||
3. Убедитесь, что hover работает
|
3. Убедитесь, что hover работает
|
||||||
|
|
||||||
**Типичная проблема:** Tooltips не показываются
|
**Типичная проблема:** Tooltips не показываются
|
||||||
```typescript
|
```typescript
|
||||||
// Проверьте z-index в ServerMap.tsx
|
// Проверьте z-index в ServerMap.tsx
|
||||||
// Tooltip должен иметь z-10
|
// Tooltip должен иметь z-10
|
||||||
className="... z-10 ..."
|
className="... z-10 ..."
|
||||||
```
|
```
|
||||||
|
|
||||||
### Перезапуск с чистого листа:
|
### Перезапуск с чистого листа:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Остановите Docker
|
# Остановите Docker
|
||||||
wsl bash -c "cd gekon-speed-boost-3ba17197-main && docker compose down"
|
wsl bash -c "cd gekon-speed-boost-3ba17197-main && docker compose down"
|
||||||
|
|
||||||
# Удалите volumes
|
# Удалите volumes
|
||||||
wsl bash -c "cd gekon-speed-boost-3ba17197-main && docker compose down -v"
|
wsl bash -c "cd gekon-speed-boost-3ba17197-main && docker compose down -v"
|
||||||
|
|
||||||
# Пересоберите
|
# Пересоберите
|
||||||
wsl bash -c "cd gekon-speed-boost-3ba17197-main && docker compose up --build"
|
wsl bash -c "cd gekon-speed-boost-3ba17197-main && docker compose up --build"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Проверка портов:
|
### Проверка портов:
|
||||||
|
|
||||||
Текущий порт может быть 8080 вместо 5173:
|
Текущий порт может быть 8080 вместо 5173:
|
||||||
- Попробуйте http://localhost:8080
|
- Попробуйте http://localhost:8080
|
||||||
- Или http://localhost:5173
|
- Или http://localhost:5173
|
||||||
|
|
||||||
### Логи Docker:
|
### Логи Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Смотрите логи в реальном времени
|
# Смотрите логи в реальном времени
|
||||||
wsl bash -c "cd gekon-speed-boost-3ba17197-main && docker compose logs -f"
|
wsl bash -c "cd gekon-speed-boost-3ba17197-main && docker compose logs -f"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Если ничего не помогает:
|
### Если ничего не помогает:
|
||||||
|
|
||||||
1. Откройте http://localhost:8080 (или 5173)
|
1. Откройте http://localhost:8080 (или 5173)
|
||||||
2. Нажмите F12
|
2. Нажмите F12
|
||||||
3. Скопируйте все ошибки из Console
|
3. Скопируйте все ошибки из Console
|
||||||
4. Проверьте Network tab - все ли файлы загружаются
|
4. Проверьте Network tab - все ли файлы загружаются
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Быстрый тест частиц:
|
## Быстрый тест частиц:
|
||||||
|
|
||||||
Добавьте в консоль браузера:
|
Добавьте в консоль браузера:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Проверка Canvas API
|
// Проверка Canvas API
|
||||||
const canvas = document.querySelector('canvas');
|
const canvas = document.querySelector('canvas');
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
console.log('Canvas found!', canvas.width, canvas.height);
|
console.log('Canvas found!', canvas.width, canvas.height);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.fillStyle = 'red';
|
ctx.fillStyle = 'red';
|
||||||
ctx.fillRect(100, 100, 200, 200); // Красный квадрат для теста
|
ctx.fillRect(100, 100, 200, 200); // Красный квадрат для теста
|
||||||
} else {
|
} else {
|
||||||
console.error('Canvas NOT found!');
|
console.error('Canvas NOT found!');
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Если увидите красный квадрат - Canvas работает, проблема в коде частиц.
|
Если увидите красный квадрат - Canvas работает, проблема в коде частиц.
|
||||||
Если нет - Canvas не рендерится.
|
Если нет - Canvas не рендерится.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Текущий статус:**
|
**Текущий статус:**
|
||||||
- ✅ Docker запущен
|
- ✅ Docker запущен
|
||||||
- ✅ Vite работает
|
- ✅ Vite работает
|
||||||
- ❓ Частицы - нужна проверка
|
- ❓ Частицы - нужна проверка
|
||||||
- ❓ Карта - нужна проверка
|
- ❓ Карта - нужна проверка
|
||||||
|
|
||||||
**Порт:** http://localhost:8080 или http://localhost:5173
|
**Порт:** http://localhost:8080 или http://localhost:5173
|
||||||
|
|||||||
+18
-18
@@ -1,18 +1,18 @@
|
|||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files
|
# Copy package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Copy project files
|
# Copy project files
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 5173
|
EXPOSE 5173
|
||||||
|
|
||||||
# Start dev server
|
# Start dev server
|
||||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||||
|
|||||||
+305
-305
@@ -1,305 +1,305 @@
|
|||||||
# ✨ Gekon - Новые возможности
|
# ✨ Gekon - Новые возможности
|
||||||
|
|
||||||
## 🎨 1. Анимированный фон с частицами
|
## 🎨 1. Анимированный фон с частицами
|
||||||
|
|
||||||
### Описание
|
### Описание
|
||||||
Динамический фон с анимированными частицами, создающий эффект сетевых соединений.
|
Динамический фон с анимированными частицами, создающий эффект сетевых соединений.
|
||||||
|
|
||||||
### Технические детали
|
### Технические детали
|
||||||
- **Технология:** Canvas API
|
- **Технология:** Canvas API
|
||||||
- **Производительность:** Оптимизирован для 60 FPS
|
- **Производительность:** Оптимизирован для 60 FPS
|
||||||
- **Адаптивность:** Автоматическое масштабирование под размер экрана
|
- **Адаптивность:** Автоматическое масштабирование под размер экрана
|
||||||
- **Цвета:** Gekon green (#10b981) и cyan (#06b6d4)
|
- **Цвета:** Gekon green (#10b981) и cyan (#06b6d4)
|
||||||
|
|
||||||
### Возможности
|
### Возможности
|
||||||
✅ Плавная анимация частиц
|
✅ Плавная анимация частиц
|
||||||
✅ Динамические связи между близкими частицами
|
✅ Динамические связи между близкими частицами
|
||||||
✅ Wrap-around эффект (частицы появляются с другой стороны)
|
✅ Wrap-around эффект (частицы появляются с другой стороны)
|
||||||
✅ Автоматическая очистка при размонтировании
|
✅ Автоматическая очистка при размонтировании
|
||||||
✅ Blend mode для лучшей интеграции с фоном
|
✅ Blend mode для лучшей интеграции с фоном
|
||||||
|
|
||||||
### Настройка
|
### Настройка
|
||||||
|
|
||||||
#### Количество частиц
|
#### Количество частиц
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/ParticlesBackground.tsx, строка ~35
|
// src/components/ParticlesBackground.tsx, строка ~35
|
||||||
const particleCount = Math.min(100, Math.floor((canvas.width * canvas.height) / 15000));
|
const particleCount = Math.min(100, Math.floor((canvas.width * canvas.height) / 15000));
|
||||||
```
|
```
|
||||||
- Увеличьте делитель (15000 → 20000) для меньшего количества
|
- Увеличьте делитель (15000 → 20000) для меньшего количества
|
||||||
- Уменьшите (15000 → 10000) для большего количества
|
- Уменьшите (15000 → 10000) для большего количества
|
||||||
|
|
||||||
#### Скорость движения
|
#### Скорость движения
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/ParticlesBackground.tsx, строка ~40
|
// src/components/ParticlesBackground.tsx, строка ~40
|
||||||
vx: (Math.random() - 0.5) * 0.5, // Измените 0.5 на нужную скорость
|
vx: (Math.random() - 0.5) * 0.5, // Измените 0.5 на нужную скорость
|
||||||
vy: (Math.random() - 0.5) * 0.5,
|
vy: (Math.random() - 0.5) * 0.5,
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Дистанция связей
|
#### Дистанция связей
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/ParticlesBackground.tsx, строка ~70
|
// src/components/ParticlesBackground.tsx, строка ~70
|
||||||
if (distance < 150) { // Измените 150 на нужную дистанцию
|
if (distance < 150) { // Измените 150 на нужную дистанцию
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Цвета
|
#### Цвета
|
||||||
```typescript
|
```typescript
|
||||||
// Частицы (строка ~60)
|
// Частицы (строка ~60)
|
||||||
ctx.fillStyle = `rgba(16, 185, 129, ${particle.opacity})`;
|
ctx.fillStyle = `rgba(16, 185, 129, ${particle.opacity})`;
|
||||||
|
|
||||||
// Связи (строка ~75)
|
// Связи (строка ~75)
|
||||||
ctx.strokeStyle = `rgba(6, 182, 212, ${opacity})`;
|
ctx.strokeStyle = `rgba(6, 182, 212, ${opacity})`;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Производительность
|
### Производительность
|
||||||
- **Desktop:** ~100 частиц, 60 FPS
|
- **Desktop:** ~100 частиц, 60 FPS
|
||||||
- **Tablet:** ~60 частиц, 60 FPS
|
- **Tablet:** ~60 частиц, 60 FPS
|
||||||
- **Mobile:** ~40 частиц, 60 FPS
|
- **Mobile:** ~40 частиц, 60 FPS
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🗺️ 2. Интерактивная карта серверов
|
## 🗺️ 2. Интерактивная карта серверов
|
||||||
|
|
||||||
### Описание
|
### Описание
|
||||||
Интерактивная карта с 25+ локациями серверов по всему миру, показывающая статистику в реальном времени.
|
Интерактивная карта с 25+ локациями серверов по всему миру, показывающая статистику в реальном времени.
|
||||||
|
|
||||||
### Технические детали
|
### Технические детали
|
||||||
- **Технология:** SVG + React
|
- **Технология:** SVG + React
|
||||||
- **Интерактивность:** Hover tooltips
|
- **Интерактивность:** Hover tooltips
|
||||||
- **Анимация:** Пульсирующие точки, анимированные линии
|
- **Анимация:** Пульсирующие точки, анимированные линии
|
||||||
- **Данные:** Статические (можно подключить API)
|
- **Данные:** Статические (можно подключить API)
|
||||||
|
|
||||||
### Возможности
|
### Возможности
|
||||||
✅ 25+ локаций серверов
|
✅ 25+ локаций серверов
|
||||||
✅ Hover tooltips с деталями
|
✅ Hover tooltips с деталями
|
||||||
✅ Статистика (пользователи, задержка)
|
✅ Статистика (пользователи, задержка)
|
||||||
✅ Индикаторы статуса (online/maintenance)
|
✅ Индикаторы статуса (online/maintenance)
|
||||||
✅ Анимированные пульсации
|
✅ Анимированные пульсации
|
||||||
✅ Линии соединений между серверами
|
✅ Линии соединений между серверами
|
||||||
✅ Адаптивный дизайн
|
✅ Адаптивный дизайн
|
||||||
|
|
||||||
### Локации серверов
|
### Локации серверов
|
||||||
|
|
||||||
#### Северная Америка (3)
|
#### Северная Америка (3)
|
||||||
- 🇺🇸 Los Angeles - 1,250 users, 12ms
|
- 🇺🇸 Los Angeles - 1,250 users, 12ms
|
||||||
- 🇺🇸 New York - 1,840 users, 8ms
|
- 🇺🇸 New York - 1,840 users, 8ms
|
||||||
- 🇨🇦 Toronto - 680 users, 15ms
|
- 🇨🇦 Toronto - 680 users, 15ms
|
||||||
|
|
||||||
#### Южная Америка (2)
|
#### Южная Америка (2)
|
||||||
- 🇧🇷 São Paulo - 520 users, 45ms
|
- 🇧🇷 São Paulo - 520 users, 45ms
|
||||||
- 🇦🇷 Buenos Aires - 280 users, 52ms
|
- 🇦🇷 Buenos Aires - 280 users, 52ms
|
||||||
|
|
||||||
#### Европа (10)
|
#### Европа (10)
|
||||||
- 🇬🇧 London - 2,100 users, 5ms
|
- 🇬🇧 London - 2,100 users, 5ms
|
||||||
- 🇩🇪 Frankfurt - 1,650 users, 7ms
|
- 🇩🇪 Frankfurt - 1,650 users, 7ms
|
||||||
- 🇫🇷 Paris - 980 users, 9ms
|
- 🇫🇷 Paris - 980 users, 9ms
|
||||||
- 🇳🇱 Amsterdam - 1,420 users, 6ms
|
- 🇳🇱 Amsterdam - 1,420 users, 6ms
|
||||||
- 🇸🇪 Stockholm - 540 users, 12ms
|
- 🇸🇪 Stockholm - 540 users, 12ms
|
||||||
- 🇵🇱 Warsaw - 720 users, 14ms
|
- 🇵🇱 Warsaw - 720 users, 14ms
|
||||||
- 🇪🇸 Madrid - 650 users, 18ms
|
- 🇪🇸 Madrid - 650 users, 18ms
|
||||||
- 🇮🇹 Milan - 580 users, 16ms
|
- 🇮🇹 Milan - 580 users, 16ms
|
||||||
- 🇹🇷 Istanbul - 680 users, 20ms
|
- 🇹🇷 Istanbul - 680 users, 20ms
|
||||||
- 🇮🇱 Tel Aviv - 420 users, 26ms
|
- 🇮🇱 Tel Aviv - 420 users, 26ms
|
||||||
|
|
||||||
#### Азия (8)
|
#### Азия (8)
|
||||||
- 🇯🇵 Tokyo - 1,950 users, 8ms
|
- 🇯🇵 Tokyo - 1,950 users, 8ms
|
||||||
- 🇸🇬 Singapore - 1,680 users, 10ms
|
- 🇸🇬 Singapore - 1,680 users, 10ms
|
||||||
- 🇭🇰 Hong Kong - 1,420 users, 12ms
|
- 🇭🇰 Hong Kong - 1,420 users, 12ms
|
||||||
- 🇰🇷 Seoul - 1,280 users, 9ms
|
- 🇰🇷 Seoul - 1,280 users, 9ms
|
||||||
- 🇮🇳 Mumbai - 890 users, 25ms
|
- 🇮🇳 Mumbai - 890 users, 25ms
|
||||||
- 🇦🇪 Dubai - 740 users, 22ms
|
- 🇦🇪 Dubai - 740 users, 22ms
|
||||||
- 🇹🇭 Bangkok - 620 users, 28ms
|
- 🇹🇭 Bangkok - 620 users, 28ms
|
||||||
- 🇻🇳 Ho Chi Minh - 480 users, 32ms
|
- 🇻🇳 Ho Chi Minh - 480 users, 32ms
|
||||||
|
|
||||||
#### Океания (2)
|
#### Океания (2)
|
||||||
- 🇦🇺 Sydney - 920 users, 18ms
|
- 🇦🇺 Sydney - 920 users, 18ms
|
||||||
- 🇳🇿 Auckland - 340 users, 24ms
|
- 🇳🇿 Auckland - 340 users, 24ms
|
||||||
|
|
||||||
#### Африка (1)
|
#### Африка (1)
|
||||||
- 🇿🇦 Cape Town - 380 users, 48ms
|
- 🇿🇦 Cape Town - 380 users, 48ms
|
||||||
|
|
||||||
**Всего:** 25 серверов, 23,000+ активных пользователей
|
**Всего:** 25 серверов, 23,000+ активных пользователей
|
||||||
|
|
||||||
### Добавление нового сервера
|
### Добавление нового сервера
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/ServerMap.tsx, добавьте в массив servers:
|
// src/components/ServerMap.tsx, добавьте в массив servers:
|
||||||
{
|
{
|
||||||
id: "moscow", // Уникальный ID
|
id: "moscow", // Уникальный ID
|
||||||
country: "Russia", // Страна
|
country: "Russia", // Страна
|
||||||
city: "Moscow", // Город
|
city: "Moscow", // Город
|
||||||
x: 55, // Позиция X (% от левого края, 0-100)
|
x: 55, // Позиция X (% от левого края, 0-100)
|
||||||
y: 30, // Позиция Y (% от верхнего края, 0-100)
|
y: 30, // Позиция Y (% от верхнего края, 0-100)
|
||||||
users: 2500, // Количество пользователей
|
users: 2500, // Количество пользователей
|
||||||
latency: 5, // Задержка в миллисекундах
|
latency: 5, // Задержка в миллисекундах
|
||||||
status: "online" // Статус: "online" или "maintenance"
|
status: "online" // Статус: "online" или "maintenance"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Позиционирование серверов
|
### Позиционирование серверов
|
||||||
|
|
||||||
Координаты X и Y в процентах (0-100):
|
Координаты X и Y в процентах (0-100):
|
||||||
|
|
||||||
```
|
```
|
||||||
Примерные координаты регионов:
|
Примерные координаты регионов:
|
||||||
- Западное побережье США: x: 15, y: 35
|
- Западное побережье США: x: 15, y: 35
|
||||||
- Восточное побережье США: x: 22, y: 32
|
- Восточное побережье США: x: 22, y: 32
|
||||||
- Западная Европа: x: 48-52, y: 28-35
|
- Западная Европа: x: 48-52, y: 28-35
|
||||||
- Восточная Европа: x: 54-58, y: 30-35
|
- Восточная Европа: x: 54-58, y: 30-35
|
||||||
- Ближний Восток: x: 56-60, y: 35-42
|
- Ближний Восток: x: 56-60, y: 35-42
|
||||||
- Южная Азия: x: 68-72, y: 42-48
|
- Южная Азия: x: 68-72, y: 42-48
|
||||||
- Восточная Азия: x: 76-82, y: 34-42
|
- Восточная Азия: x: 76-82, y: 34-42
|
||||||
- Юго-Восточная Азия: x: 74-78, y: 48-52
|
- Юго-Восточная Азия: x: 74-78, y: 48-52
|
||||||
- Австралия: x: 85, y: 72
|
- Австралия: x: 85, y: 72
|
||||||
```
|
```
|
||||||
|
|
||||||
### Настройка внешнего вида
|
### Настройка внешнего вида
|
||||||
|
|
||||||
#### Цвет точек
|
#### Цвет точек
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/ServerMap.tsx, строка ~180
|
// src/components/ServerMap.tsx, строка ~180
|
||||||
className={`... ${
|
className={`... ${
|
||||||
server.status === "online"
|
server.status === "online"
|
||||||
? "bg-gekon-green shadow-lg shadow-gekon-green/50" // Зелёный для online
|
? "bg-gekon-green shadow-lg shadow-gekon-green/50" // Зелёный для online
|
||||||
: "bg-yellow-500 shadow-lg shadow-yellow-500/50" // Жёлтый для maintenance
|
: "bg-yellow-500 shadow-lg shadow-yellow-500/50" // Жёлтый для maintenance
|
||||||
}`}
|
}`}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Размер точек
|
#### Размер точек
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/ServerMap.tsx, строка ~182
|
// src/components/ServerMap.tsx, строка ~182
|
||||||
<div className="relative h-3 w-3 rounded-full ...">
|
<div className="relative h-3 w-3 rounded-full ...">
|
||||||
// Измените h-3 w-3 на h-4 w-4 для больших точек
|
// Измените h-3 w-3 на h-4 w-4 для больших точек
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Скорость пульсации
|
#### Скорость пульсации
|
||||||
```css
|
```css
|
||||||
/* src/styles.css */
|
/* src/styles.css */
|
||||||
.animate-ping {
|
.animate-ping {
|
||||||
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||||
}
|
}
|
||||||
/* Измените 1s на 2s для медленной пульсации */
|
/* Измените 1s на 2s для медленной пульсации */
|
||||||
```
|
```
|
||||||
|
|
||||||
### Интеграция с API
|
### Интеграция с API
|
||||||
|
|
||||||
Для подключения реальных данных:
|
Для подключения реальных данных:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/hooks/useServerStats.ts (создайте этот файл)
|
// src/hooks/useServerStats.ts (создайте этот файл)
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
export function useServerStats() {
|
export function useServerStats() {
|
||||||
const [servers, setServers] = useState<ServerLocation[]>([]);
|
const [servers, setServers] = useState<ServerLocation[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
const response = await fetch('https://your-api.com/servers');
|
const response = await fetch('https://your-api.com/servers');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setServers(data);
|
setServers(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchStats();
|
fetchStats();
|
||||||
const interval = setInterval(fetchStats, 60000); // Обновление каждую минуту
|
const interval = setInterval(fetchStats, 60000); // Обновление каждую минуту
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return servers;
|
return servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
// В ServerMap.tsx:
|
// В ServerMap.tsx:
|
||||||
const servers = useServerStats(); // Вместо статического массива
|
const servers = useServerStats(); // Вместо статического массива
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🎯 Статистика
|
## 🎯 Статистика
|
||||||
|
|
||||||
### Общая статистика карты
|
### Общая статистика карты
|
||||||
- **Всего серверов:** 25+
|
- **Всего серверов:** 25+
|
||||||
- **Активных пользователей:** 23,000+
|
- **Активных пользователей:** 23,000+
|
||||||
- **Средняя задержка:** 18ms
|
- **Средняя задержка:** 18ms
|
||||||
- **Покрытие:** 50+ стран
|
- **Покрытие:** 50+ стран
|
||||||
|
|
||||||
### Производительность
|
### Производительность
|
||||||
- **Рендеринг:** < 16ms (60 FPS)
|
- **Рендеринг:** < 16ms (60 FPS)
|
||||||
- **Размер компонента:** ~8KB (gzipped)
|
- **Размер компонента:** ~8KB (gzipped)
|
||||||
- **Зависимости:** Только React + Lucide icons
|
- **Зависимости:** Только React + Lucide icons
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌍 Интернационализация
|
## 🌍 Интернационализация
|
||||||
|
|
||||||
Добавлены переводы для новых секций:
|
Добавлены переводы для новых секций:
|
||||||
|
|
||||||
### Английский (en)
|
### Английский (en)
|
||||||
```typescript
|
```typescript
|
||||||
tech_title_1: "Global Network"
|
tech_title_1: "Global Network"
|
||||||
tech_title_2: "Infrastructure"
|
tech_title_2: "Infrastructure"
|
||||||
tech_subtitle: "Our worldwide network of optimization nodes..."
|
tech_subtitle: "Our worldwide network of optimization nodes..."
|
||||||
```
|
```
|
||||||
|
|
||||||
### Русский (ru)
|
### Русский (ru)
|
||||||
```typescript
|
```typescript
|
||||||
tech_title_1: "Глобальная сетевая"
|
tech_title_1: "Глобальная сетевая"
|
||||||
tech_title_2: "инфраструктура"
|
tech_title_2: "инфраструктура"
|
||||||
tech_subtitle: "Наша всемирная сеть узлов оптимизации..."
|
tech_subtitle: "Наша всемирная сеть узлов оптимизации..."
|
||||||
```
|
```
|
||||||
|
|
||||||
### Китайский (zh)
|
### Китайский (zh)
|
||||||
```typescript
|
```typescript
|
||||||
tech_title_1: "全球网络"
|
tech_title_1: "全球网络"
|
||||||
tech_title_2: "基础设施"
|
tech_title_2: "基础设施"
|
||||||
tech_subtitle: "我们的全球优化节点网络..."
|
tech_subtitle: "我们的全球优化节点网络..."
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 Сравнение до/после
|
## 📊 Сравнение до/после
|
||||||
|
|
||||||
### До добавления новых возможностей
|
### До добавления новых возможностей
|
||||||
- ❌ Статичный фон
|
- ❌ Статичный фон
|
||||||
- ❌ Нет визуализации серверов
|
- ❌ Нет визуализации серверов
|
||||||
- ❌ Только текстовое описание инфраструктуры
|
- ❌ Только текстовое описание инфраструктуры
|
||||||
|
|
||||||
### После добавления
|
### После добавления
|
||||||
- ✅ Динамический анимированный фон
|
- ✅ Динамический анимированный фон
|
||||||
- ✅ Интерактивная карта с 25+ серверами
|
- ✅ Интерактивная карта с 25+ серверами
|
||||||
- ✅ Визуализация глобальной сети
|
- ✅ Визуализация глобальной сети
|
||||||
- ✅ Статистика в реальном времени
|
- ✅ Статистика в реальном времени
|
||||||
- ✅ Улучшенный UX и визуальная привлекательность
|
- ✅ Улучшенный UX и визуальная привлекательность
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Следующие улучшения
|
## 🚀 Следующие улучшения
|
||||||
|
|
||||||
### Возможные дополнения:
|
### Возможные дополнения:
|
||||||
|
|
||||||
1. **Real-time данные**
|
1. **Real-time данные**
|
||||||
- Подключение к API для актуальной статистики
|
- Подключение к API для актуальной статистики
|
||||||
- WebSocket для обновлений в реальном времени
|
- WebSocket для обновлений в реальном времени
|
||||||
|
|
||||||
2. **Фильтры на карте**
|
2. **Фильтры на карте**
|
||||||
- Фильтр по регионам
|
- Фильтр по регионам
|
||||||
- Фильтр по задержке
|
- Фильтр по задержке
|
||||||
- Поиск по городу/стране
|
- Поиск по городу/стране
|
||||||
|
|
||||||
3. **Расширенная аналитика**
|
3. **Расширенная аналитика**
|
||||||
- График загрузки серверов
|
- График загрузки серверов
|
||||||
- История задержек
|
- История задержек
|
||||||
- Прогноз доступности
|
- Прогноз доступности
|
||||||
|
|
||||||
4. **3D визуализация**
|
4. **3D визуализация**
|
||||||
- Three.js для 3D глобуса
|
- Three.js для 3D глобуса
|
||||||
- Анимированные траектории данных
|
- Анимированные траектории данных
|
||||||
|
|
||||||
5. **Дополнительные эффекты**
|
5. **Дополнительные эффекты**
|
||||||
- Particle trails при движении мыши
|
- Particle trails при движении мыши
|
||||||
- Звуковые эффекты при hover
|
- Звуковые эффекты при hover
|
||||||
- Тематические цветовые схемы
|
- Тематические цветовые схемы
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Версия:** 1.0.0
|
**Версия:** 1.0.0
|
||||||
**Дата:** 23.01.2026
|
**Дата:** 23.01.2026
|
||||||
**Статус:** ✅ Готово к использованию
|
**Статус:** ✅ Готово к использованию
|
||||||
|
|||||||
+111
-111
@@ -1,111 +1,111 @@
|
|||||||
# 🚀 Gekon - Быстрый старт
|
# 🚀 Gekon - Быстрый старт
|
||||||
|
|
||||||
## За 3 минуты до запуска
|
## За 3 минуты до запуска
|
||||||
|
|
||||||
### 1. Установка (1 мин)
|
### 1. Установка (1 мин)
|
||||||
```bash
|
```bash
|
||||||
cd gekon-speed-boost-3ba17197-main
|
cd gekon-speed-boost-3ba17197-main
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Запуск (30 сек)
|
### 2. Запуск (30 сек)
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Открыть (30 сек)
|
### 3. Открыть (30 сек)
|
||||||
```
|
```
|
||||||
http://localhost:5173
|
http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✅ Что добавлено
|
## ✅ Что добавлено
|
||||||
|
|
||||||
### Анимированный фон с частицами
|
### Анимированный фон с частицами
|
||||||
- Файл: `src/components/ParticlesBackground.tsx`
|
- Файл: `src/components/ParticlesBackground.tsx`
|
||||||
- Плавная анимация на Canvas
|
- Плавная анимация на Canvas
|
||||||
- Цвета бренда Gekon
|
- Цвета бренда Gekon
|
||||||
|
|
||||||
### Интерактивная карта серверов
|
### Интерактивная карта серверов
|
||||||
- Файл: `src/components/ServerMap.tsx`
|
- Файл: `src/components/ServerMap.tsx`
|
||||||
- 25+ локаций по всему миру
|
- 25+ локаций по всему миру
|
||||||
- Hover для деталей сервера
|
- Hover для деталей сервера
|
||||||
- Статистика в реальном времени
|
- Статистика в реальном времени
|
||||||
|
|
||||||
## 🎨 Быстрая настройка
|
## 🎨 Быстрая настройка
|
||||||
|
|
||||||
### Добавить свой сервер на карту
|
### Добавить свой сервер на карту
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/ServerMap.tsx, строка ~15
|
// src/components/ServerMap.tsx, строка ~15
|
||||||
{
|
{
|
||||||
id: "moscow",
|
id: "moscow",
|
||||||
country: "Russia",
|
country: "Russia",
|
||||||
city: "Moscow",
|
city: "Moscow",
|
||||||
x: 55, // % слева (0-100)
|
x: 55, // % слева (0-100)
|
||||||
y: 30, // % сверху (0-100)
|
y: 30, // % сверху (0-100)
|
||||||
users: 2500, // количество пользователей
|
users: 2500, // количество пользователей
|
||||||
latency: 5, // задержка в мс
|
latency: 5, // задержка в мс
|
||||||
status: "online"
|
status: "online"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Изменить количество частиц
|
### Изменить количество частиц
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/ParticlesBackground.tsx, строка ~35
|
// src/components/ParticlesBackground.tsx, строка ~35
|
||||||
const particleCount = Math.min(100, ...);
|
const particleCount = Math.min(100, ...);
|
||||||
// Измените 100 на нужное число
|
// Измените 100 на нужное число
|
||||||
```
|
```
|
||||||
|
|
||||||
### Подключить к Sub-Bridge
|
### Подключить к Sub-Bridge
|
||||||
```typescript
|
```typescript
|
||||||
// Создайте: src/config/subscription.ts
|
// Создайте: src/config/subscription.ts
|
||||||
export const SUBSCRIPTION_CONFIG = {
|
export const SUBSCRIPTION_CONFIG = {
|
||||||
baseUrl: 'https://subb.ydns.eu',
|
baseUrl: 'https://subb.ydns.eu',
|
||||||
supportUrl: 'https://subb.ydns.eu/',
|
supportUrl: 'https://subb.ydns.eu/',
|
||||||
};
|
};
|
||||||
|
|
||||||
// В компонентах замените:
|
// В компонентах замените:
|
||||||
<Button onClick={() => window.location.href = SUBSCRIPTION_CONFIG.baseUrl}>
|
<Button onClick={() => window.location.href = SUBSCRIPTION_CONFIG.baseUrl}>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌍 Языки
|
## 🌍 Языки
|
||||||
|
|
||||||
Переключатель в навигации (правый верхний угол):
|
Переключатель в навигации (правый верхний угол):
|
||||||
- 🇬🇧 English
|
- 🇬🇧 English
|
||||||
- 🇷🇺 Русский
|
- 🇷🇺 Русский
|
||||||
- 🇨🇳 中文
|
- 🇨🇳 中文
|
||||||
|
|
||||||
## 📦 Сборка
|
## 📦 Сборка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 Что дальше?
|
## 🎯 Что дальше?
|
||||||
|
|
||||||
1. Добавьте реальные ссылки на подписку
|
1. Добавьте реальные ссылки на подписку
|
||||||
2. Интегрируйте Telegram бот
|
2. Интегрируйте Telegram бот
|
||||||
3. Настройте аналитику
|
3. Настройте аналитику
|
||||||
4. Оптимизируйте SEO
|
4. Оптимизируйте SEO
|
||||||
5. Добавьте Cookie Consent
|
5. Добавьте Cookie Consent
|
||||||
|
|
||||||
## 📚 Документация
|
## 📚 Документация
|
||||||
|
|
||||||
- `SETUP.md` - Полная инструкция
|
- `SETUP.md` - Полная инструкция
|
||||||
- `README.ru.md` - Подробное описание на русском
|
- `README.ru.md` - Подробное описание на русском
|
||||||
- Исходный код хорошо документирован
|
- Исходный код хорошо документирован
|
||||||
|
|
||||||
## 🐛 Проблемы?
|
## 🐛 Проблемы?
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Переустановите зависимости
|
# Переустановите зависимости
|
||||||
rm -rf node_modules package-lock.json
|
rm -rf node_modules package-lock.json
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Очистите кэш
|
# Очистите кэш
|
||||||
npm run build -- --force
|
npm run build -- --force
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Готово!** Лендинг запущен и готов к кастомизации 🎉
|
**Готово!** Лендинг запущен и готов к кастомизации 🎉
|
||||||
|
|||||||
+278
-278
@@ -1,278 +1,278 @@
|
|||||||
# Gekon - Лендинг для сервиса ускорения интернета
|
# Gekon - Лендинг для сервиса ускорения интернета
|
||||||
|
|
||||||
## 🎯 Описание
|
## 🎯 Описание
|
||||||
|
|
||||||
Современный одностраничный лендинг для сервиса **Gekon** - технологичного решения для оптимизации и ускорения интернет-соединений.
|
Современный одностраничный лендинг для сервиса **Gekon** - технологичного решения для оптимизации и ускорения интернет-соединений.
|
||||||
|
|
||||||
## ✨ Новые возможности
|
## ✨ Новые возможности
|
||||||
|
|
||||||
### 1. Анимированный фон с частицами ✅
|
### 1. Анимированный фон с частицами ✅
|
||||||
- Плавная анимация частиц на Canvas
|
- Плавная анимация частиц на Canvas
|
||||||
- Динамические связи между частицами
|
- Динамические связи между частицами
|
||||||
- Цвета бренда Gekon (зелёный/циан)
|
- Цвета бренда Gekon (зелёный/циан)
|
||||||
- Оптимизирован для производительности
|
- Оптимизирован для производительности
|
||||||
- Адаптивный под размер экрана
|
- Адаптивный под размер экрана
|
||||||
|
|
||||||
### 2. Интерактивная карта серверов ✅
|
### 2. Интерактивная карта серверов ✅
|
||||||
- 25+ локаций серверов по всему миру
|
- 25+ локаций серверов по всему миру
|
||||||
- Всплывающие подсказки при наведении
|
- Всплывающие подсказки при наведении
|
||||||
- Статистика в реальном времени (пользователи, задержка)
|
- Статистика в реальном времени (пользователи, задержка)
|
||||||
- Анимированные пульсирующие эффекты
|
- Анимированные пульсирующие эффекты
|
||||||
- Линии соединений между серверами
|
- Линии соединений между серверами
|
||||||
- Индикаторы статуса (онлайн/обслуживание)
|
- Индикаторы статуса (онлайн/обслуживание)
|
||||||
|
|
||||||
## 🚀 Быстрый старт
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
### Установка зависимостей
|
### Установка зависимостей
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
# или
|
# или
|
||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Запуск dev-сервера
|
### Запуск dev-сервера
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
# или
|
# или
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Откройте http://localhost:5173 в браузере.
|
Откройте http://localhost:5173 в браузере.
|
||||||
|
|
||||||
### Сборка для продакшена
|
### Сборка для продакшена
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌍 Поддержка языков
|
## 🌍 Поддержка языков
|
||||||
|
|
||||||
Лендинг поддерживает 3 языка:
|
Лендинг поддерживает 3 языка:
|
||||||
- 🇬🇧 Английский (English)
|
- 🇬🇧 Английский (English)
|
||||||
- 🇷🇺 Русский
|
- 🇷🇺 Русский
|
||||||
- 🇨🇳 Китайский (中文)
|
- 🇨🇳 Китайский (中文)
|
||||||
|
|
||||||
Переключатель языков находится в навигации (правый верхний угол).
|
Переключатель языков находится в навигации (правый верхний угол).
|
||||||
|
|
||||||
## 📊 Структура проекта
|
## 📊 Структура проекта
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── components/
|
├── components/
|
||||||
│ ├── ParticlesBackground.tsx ← Анимированные частицы
|
│ ├── ParticlesBackground.tsx ← Анимированные частицы
|
||||||
│ ├── ServerMap.tsx ← Интерактивная карта
|
│ ├── ServerMap.tsx ← Интерактивная карта
|
||||||
│ ├── HeroSection.tsx ← Главный экран
|
│ ├── HeroSection.tsx ← Главный экран
|
||||||
│ ├── Navbar.tsx ← Навигация
|
│ ├── Navbar.tsx ← Навигация
|
||||||
│ ├── FeaturesSection.tsx ← Возможности
|
│ ├── FeaturesSection.tsx ← Возможности
|
||||||
│ ├── PricingSection.tsx ← Тарифы
|
│ ├── PricingSection.tsx ← Тарифы
|
||||||
│ └── ... другие компоненты
|
│ └── ... другие компоненты
|
||||||
├── i18n/
|
├── i18n/
|
||||||
│ ├── translations.ts ← Переводы
|
│ ├── translations.ts ← Переводы
|
||||||
│ └── context.tsx
|
│ └── context.tsx
|
||||||
└── routes/
|
└── routes/
|
||||||
└── index.tsx ← Главная страница
|
└── index.tsx ← Главная страница
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Кастомизация
|
## 🎨 Кастомизация
|
||||||
|
|
||||||
### Изменить локации серверов
|
### Изменить локации серверов
|
||||||
Отредактируйте `src/components/ServerMap.tsx`:
|
Отредактируйте `src/components/ServerMap.tsx`:
|
||||||
```typescript
|
```typescript
|
||||||
const servers: ServerLocation[] = [
|
const servers: ServerLocation[] = [
|
||||||
{
|
{
|
||||||
id: "moscow",
|
id: "moscow",
|
||||||
country: "Russia",
|
country: "Russia",
|
||||||
city: "Moscow",
|
city: "Moscow",
|
||||||
x: 55, // % слева
|
x: 55, // % слева
|
||||||
y: 30, // % сверху
|
y: 30, // % сверху
|
||||||
users: 2500,
|
users: 2500,
|
||||||
latency: 5,
|
latency: 5,
|
||||||
status: "online"
|
status: "online"
|
||||||
},
|
},
|
||||||
// ... больше серверов
|
// ... больше серверов
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
### Настроить количество частиц
|
### Настроить количество частиц
|
||||||
Отредактируйте `src/components/ParticlesBackground.tsx`:
|
Отредактируйте `src/components/ParticlesBackground.tsx`:
|
||||||
```typescript
|
```typescript
|
||||||
// Строка ~35
|
// Строка ~35
|
||||||
const particleCount = Math.min(100, Math.floor((canvas.width * canvas.height) / 15000));
|
const particleCount = Math.min(100, Math.floor((canvas.width * canvas.height) / 15000));
|
||||||
// Увеличьте делитель (15000) для меньшего количества частиц
|
// Увеличьте делитель (15000) для меньшего количества частиц
|
||||||
// Уменьшите для большего количества
|
// Уменьшите для большего количества
|
||||||
```
|
```
|
||||||
|
|
||||||
### Изменить цвета бренда
|
### Изменить цвета бренда
|
||||||
Отредактируйте `src/styles.css`:
|
Отредактируйте `src/styles.css`:
|
||||||
```css
|
```css
|
||||||
--gekon-green: oklch(0.72 0.19 160); /* Основной зелёный */
|
--gekon-green: oklch(0.72 0.19 160); /* Основной зелёный */
|
||||||
--gekon-cyan: oklch(0.7 0.14 200); /* Циан */
|
--gekon-cyan: oklch(0.7 0.14 200); /* Циан */
|
||||||
--gekon-purple: oklch(0.6 0.25 300); /* Фиолетовый */
|
--gekon-purple: oklch(0.6 0.25 300); /* Фиолетовый */
|
||||||
--gekon-neon: oklch(0.75 0.2 150); /* Неоновый зелёный */
|
--gekon-neon: oklch(0.75 0.2 150); /* Неоновый зелёный */
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔗 Интеграция с Sub-Bridge
|
## 🔗 Интеграция с Sub-Bridge
|
||||||
|
|
||||||
Чтобы подключить к вашему реальному VPN-сервису:
|
Чтобы подключить к вашему реальному VPN-сервису:
|
||||||
|
|
||||||
### 1. Создайте конфиг
|
### 1. Создайте конфиг
|
||||||
```typescript
|
```typescript
|
||||||
// src/config/subscription.ts
|
// src/config/subscription.ts
|
||||||
export const SUBSCRIPTION_CONFIG = {
|
export const SUBSCRIPTION_CONFIG = {
|
||||||
baseUrl: 'https://subb.ydns.eu',
|
baseUrl: 'https://subb.ydns.eu',
|
||||||
supportUrl: 'https://subb.ydns.eu/',
|
supportUrl: 'https://subb.ydns.eu/',
|
||||||
logoUrl: 'https://raw.githubusercontent.com/arpicme/Proxy-App-Icon-set/refs/heads/main/white_background/Karing.svg',
|
logoUrl: 'https://raw.githubusercontent.com/arpicme/Proxy-App-Icon-set/refs/heads/main/white_background/Karing.svg',
|
||||||
announcement: 'Спасибо за подписку! Ваша безопасность - наш приоритет.',
|
announcement: 'Спасибо за подписку! Ваша безопасность - наш приоритет.',
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Обновите CTA кнопки
|
### 2. Обновите CTA кнопки
|
||||||
```typescript
|
```typescript
|
||||||
// В компонентах (HeroSection, PricingSection и т.д.)
|
// В компонентах (HeroSection, PricingSection и т.д.)
|
||||||
import { SUBSCRIPTION_CONFIG } from '@/config/subscription';
|
import { SUBSCRIPTION_CONFIG } from '@/config/subscription';
|
||||||
|
|
||||||
<Button onClick={() => window.location.href = SUBSCRIPTION_CONFIG.baseUrl}>
|
<Button onClick={() => window.location.href = SUBSCRIPTION_CONFIG.baseUrl}>
|
||||||
Начать
|
Начать
|
||||||
</Button>
|
</Button>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Добавьте Telegram виджет
|
### 3. Добавьте Telegram виджет
|
||||||
```typescript
|
```typescript
|
||||||
// src/components/TelegramWidget.tsx
|
// src/components/TelegramWidget.tsx
|
||||||
export function TelegramWidget() {
|
export function TelegramWidget() {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href="https://t.me/your_support_bot"
|
href="https://t.me/your_support_bot"
|
||||||
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-gradient-to-br from-gekon-green to-gekon-cyan shadow-lg hover:scale-110 transition-transform"
|
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-gradient-to-br from-gekon-green to-gekon-cyan shadow-lg hover:scale-110 transition-transform"
|
||||||
>
|
>
|
||||||
<MessageCircle size={24} className="text-white" />
|
<MessageCircle size={24} className="text-white" />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌐 Локации серверов
|
## 🌐 Локации серверов
|
||||||
|
|
||||||
### Северная Америка
|
### Северная Америка
|
||||||
- 🇺🇸 США (Лос-Анджелес, Нью-Йорк)
|
- 🇺🇸 США (Лос-Анджелес, Нью-Йорк)
|
||||||
- 🇨🇦 Канада (Торонто)
|
- 🇨🇦 Канада (Торонто)
|
||||||
|
|
||||||
### Южная Америка
|
### Южная Америка
|
||||||
- 🇧🇷 Бразилия (Сан-Паулу)
|
- 🇧🇷 Бразилия (Сан-Паулу)
|
||||||
- 🇦🇷 Аргентина (Буэнос-Айрес)
|
- 🇦🇷 Аргентина (Буэнос-Айрес)
|
||||||
|
|
||||||
### Европа
|
### Европа
|
||||||
- 🇬🇧 Великобритания (Лондон)
|
- 🇬🇧 Великобритания (Лондон)
|
||||||
- 🇩🇪 Германия (Франкфурт)
|
- 🇩🇪 Германия (Франкфурт)
|
||||||
- 🇫🇷 Франция (Париж)
|
- 🇫🇷 Франция (Париж)
|
||||||
- 🇳🇱 Нидерланды (Амстердам)
|
- 🇳🇱 Нидерланды (Амстердам)
|
||||||
- 🇸🇪 Швеция (Стокгольм)
|
- 🇸🇪 Швеция (Стокгольм)
|
||||||
- 🇵🇱 Польша (Варшава)
|
- 🇵🇱 Польша (Варшава)
|
||||||
- 🇪🇸 Испания (Мадрид)
|
- 🇪🇸 Испания (Мадрид)
|
||||||
- 🇮🇹 Италия (Милан)
|
- 🇮🇹 Италия (Милан)
|
||||||
- 🇹🇷 Турция (Стамбул)
|
- 🇹🇷 Турция (Стамбул)
|
||||||
- 🇮🇱 Израиль (Тель-Авив)
|
- 🇮🇱 Израиль (Тель-Авив)
|
||||||
|
|
||||||
### Азия
|
### Азия
|
||||||
- 🇯🇵 Япония (Токио)
|
- 🇯🇵 Япония (Токио)
|
||||||
- 🇸🇬 Сингапур
|
- 🇸🇬 Сингапур
|
||||||
- 🇭🇰 Гонконг
|
- 🇭🇰 Гонконг
|
||||||
- 🇰🇷 Южная Корея (Сеул)
|
- 🇰🇷 Южная Корея (Сеул)
|
||||||
- 🇮🇳 Индия (Мумбаи)
|
- 🇮🇳 Индия (Мумбаи)
|
||||||
- 🇦🇪 ОАЭ (Дубай)
|
- 🇦🇪 ОАЭ (Дубай)
|
||||||
- 🇹🇭 Таиланд (Бангкок)
|
- 🇹🇭 Таиланд (Бангкок)
|
||||||
- 🇻🇳 Вьетнам (Хошимин)
|
- 🇻🇳 Вьетнам (Хошимин)
|
||||||
|
|
||||||
### Океания
|
### Океания
|
||||||
- 🇦🇺 Австралия (Сидней)
|
- 🇦🇺 Австралия (Сидней)
|
||||||
- 🇳🇿 Новая Зеландия (Окленд)
|
- 🇳🇿 Новая Зеландия (Окленд)
|
||||||
|
|
||||||
### Африка
|
### Африка
|
||||||
- 🇿🇦 ЮАР (Кейптаун)
|
- 🇿🇦 ЮАР (Кейптаун)
|
||||||
|
|
||||||
## 🛠️ Технологии
|
## 🛠️ Технологии
|
||||||
|
|
||||||
- **React 19** + TypeScript
|
- **React 19** + TypeScript
|
||||||
- **TanStack Router** - маршрутизация
|
- **TanStack Router** - маршрутизация
|
||||||
- **Tailwind CSS 4.2** - стилизация
|
- **Tailwind CSS 4.2** - стилизация
|
||||||
- **Radix UI** - UI компоненты
|
- **Radix UI** - UI компоненты
|
||||||
- **Lucide React** - иконки
|
- **Lucide React** - иконки
|
||||||
- **Vite** - сборка
|
- **Vite** - сборка
|
||||||
- **Canvas API** - анимация частиц
|
- **Canvas API** - анимация частиц
|
||||||
|
|
||||||
## 📦 Деплой
|
## 📦 Деплой
|
||||||
|
|
||||||
### Cloudflare Pages
|
### Cloudflare Pages
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
# Загрузите папку dist/ на Cloudflare Pages
|
# Загрузите папку dist/ на Cloudflare Pages
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vercel
|
### Vercel
|
||||||
```bash
|
```bash
|
||||||
vercel deploy
|
vercel deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
### Netlify
|
### Netlify
|
||||||
```bash
|
```bash
|
||||||
netlify deploy --prod
|
netlify deploy --prod
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 Следующие шаги
|
## 🎯 Следующие шаги
|
||||||
|
|
||||||
1. ✅ Добавлены анимированные частицы
|
1. ✅ Добавлены анимированные частицы
|
||||||
2. ✅ Создана интерактивная карта серверов
|
2. ✅ Создана интерактивная карта серверов
|
||||||
3. ⏳ Добавить реальные ссылки на подписку
|
3. ⏳ Добавить реальные ссылки на подписку
|
||||||
4. ⏳ Интегрировать Telegram бот
|
4. ⏳ Интегрировать Telegram бот
|
||||||
5. ⏳ Добавить аналитику (Google Analytics)
|
5. ⏳ Добавить аналитику (Google Analytics)
|
||||||
6. ⏳ Оптимизировать SEO
|
6. ⏳ Оптимизировать SEO
|
||||||
7. ⏳ Добавить Cookie Consent (GDPR)
|
7. ⏳ Добавить Cookie Consent (GDPR)
|
||||||
|
|
||||||
## 📝 Дополнительно
|
## 📝 Дополнительно
|
||||||
|
|
||||||
### Производительность
|
### Производительность
|
||||||
- Частицы автоматически масштабируются под размер экрана
|
- Частицы автоматически масштабируются под размер экрана
|
||||||
- Lazy loading для изображений
|
- Lazy loading для изображений
|
||||||
- Оптимизированная сборка с code splitting
|
- Оптимизированная сборка с code splitting
|
||||||
|
|
||||||
### Доступность
|
### Доступность
|
||||||
- ARIA метки
|
- ARIA метки
|
||||||
- Навигация с клавиатуры
|
- Навигация с клавиатуры
|
||||||
- Поддержка screen readers
|
- Поддержка screen readers
|
||||||
|
|
||||||
### SEO
|
### SEO
|
||||||
- Мета-теги для всех страниц
|
- Мета-теги для всех страниц
|
||||||
- Open Graph теги
|
- Open Graph теги
|
||||||
- Семантическая разметка HTML
|
- Семантическая разметка HTML
|
||||||
|
|
||||||
## 🐛 Решение проблем
|
## 🐛 Решение проблем
|
||||||
|
|
||||||
### Частицы не отображаются
|
### Частицы не отображаются
|
||||||
- Проверьте консоль браузера на ошибки
|
- Проверьте консоль браузера на ошибки
|
||||||
- Убедитесь, что Canvas поддерживается
|
- Убедитесь, что Canvas поддерживается
|
||||||
- Попробуйте уменьшить количество частиц
|
- Попробуйте уменьшить количество частиц
|
||||||
|
|
||||||
### Карта не работает
|
### Карта не работает
|
||||||
- Проверьте z-index конфликты
|
- Проверьте z-index конфликты
|
||||||
- Убедитесь, что hover события не блокируются
|
- Убедитесь, что hover события не блокируются
|
||||||
- Протестируйте в разных браузерах
|
- Протестируйте в разных браузерах
|
||||||
|
|
||||||
### Ошибки сборки
|
### Ошибки сборки
|
||||||
```bash
|
```bash
|
||||||
# Очистите кэш и переустановите
|
# Очистите кэш и переустановите
|
||||||
rm -rf node_modules package-lock.json
|
rm -rf node_modules package-lock.json
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📞 Поддержка
|
## 📞 Поддержка
|
||||||
|
|
||||||
Для вопросов и проблем:
|
Для вопросов и проблем:
|
||||||
- Проверьте документацию в `SETUP.md`
|
- Проверьте документацию в `SETUP.md`
|
||||||
- Изучите исходный код компонентов
|
- Изучите исходный код компонентов
|
||||||
- Протестируйте в разных браузерах
|
- Протестируйте в разных браузерах
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Версия:** 1.0.0
|
**Версия:** 1.0.0
|
||||||
**Обновлено:** 23.01.2026
|
**Обновлено:** 23.01.2026
|
||||||
**Создано с помощью:** React + TypeScript + Tailwind CSS
|
**Создано с помощью:** React + TypeScript + Tailwind CSS
|
||||||
|
|||||||
@@ -1,243 +1,243 @@
|
|||||||
# Gekon Landing Page - Setup Guide
|
# Gekon Landing Page - Setup Guide
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Node.js 18+ or Bun runtime
|
- Node.js 18+ or Bun runtime
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. **Install dependencies:**
|
1. **Install dependencies:**
|
||||||
```bash
|
```bash
|
||||||
# Using npm
|
# Using npm
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Or using bun (faster)
|
# Or using bun (faster)
|
||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run development server:**
|
2. **Run development server:**
|
||||||
```bash
|
```bash
|
||||||
# Using npm
|
# Using npm
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# Or using bun
|
# Or using bun
|
||||||
bun run dev
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Open in browser:**
|
3. **Open in browser:**
|
||||||
```
|
```
|
||||||
http://localhost:5173
|
http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📦 Build for Production
|
## 📦 Build for Production
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build
|
# Build
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# Preview production build
|
# Preview production build
|
||||||
npm run preview
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 New Features Added
|
## 🎨 New Features Added
|
||||||
|
|
||||||
### 1. Animated Particles Background
|
### 1. Animated Particles Background
|
||||||
- **File:** `src/components/ParticlesBackground.tsx`
|
- **File:** `src/components/ParticlesBackground.tsx`
|
||||||
- **Features:**
|
- **Features:**
|
||||||
- Canvas-based particle system
|
- Canvas-based particle system
|
||||||
- Animated connections between particles
|
- Animated connections between particles
|
||||||
- Gekon brand colors (green/cyan)
|
- Gekon brand colors (green/cyan)
|
||||||
- Performance optimized
|
- Performance optimized
|
||||||
- Responsive to screen size
|
- Responsive to screen size
|
||||||
|
|
||||||
### 2. Interactive Server Map
|
### 2. Interactive Server Map
|
||||||
- **File:** `src/components/ServerMap.tsx`
|
- **File:** `src/components/ServerMap.tsx`
|
||||||
- **Features:**
|
- **Features:**
|
||||||
- 25+ server locations worldwide
|
- 25+ server locations worldwide
|
||||||
- Hover tooltips with server details
|
- Hover tooltips with server details
|
||||||
- Real-time stats (users, latency)
|
- Real-time stats (users, latency)
|
||||||
- Animated pulse effects
|
- Animated pulse effects
|
||||||
- Connection lines between servers
|
- Connection lines between servers
|
||||||
- Status indicators (online/maintenance)
|
- Status indicators (online/maintenance)
|
||||||
|
|
||||||
### Server Locations Included:
|
### Server Locations Included:
|
||||||
- **North America:** USA (West/East), Canada
|
- **North America:** USA (West/East), Canada
|
||||||
- **South America:** Brazil, Argentina
|
- **South America:** Brazil, Argentina
|
||||||
- **Europe:** UK, Germany, France, Netherlands, Sweden, Poland, Spain, Italy, Turkey, Israel
|
- **Europe:** UK, Germany, France, Netherlands, Sweden, Poland, Spain, Italy, Turkey, Israel
|
||||||
- **Asia:** Japan, Singapore, Hong Kong, South Korea, India, UAE, Thailand, Vietnam
|
- **Asia:** Japan, Singapore, Hong Kong, South Korea, India, UAE, Thailand, Vietnam
|
||||||
- **Oceania:** Australia, New Zealand
|
- **Oceania:** Australia, New Zealand
|
||||||
- **Africa:** South Africa
|
- **Africa:** South Africa
|
||||||
|
|
||||||
## 🌍 Internationalization
|
## 🌍 Internationalization
|
||||||
|
|
||||||
The site supports 3 languages:
|
The site supports 3 languages:
|
||||||
- **English** (en)
|
- **English** (en)
|
||||||
- **Russian** (ru) - Русский
|
- **Russian** (ru) - Русский
|
||||||
- **Chinese** (zh) - 中文
|
- **Chinese** (zh) - 中文
|
||||||
|
|
||||||
Language switcher is in the navbar (top-right).
|
Language switcher is in the navbar (top-right).
|
||||||
|
|
||||||
## 🎨 Customization
|
## 🎨 Customization
|
||||||
|
|
||||||
### Update Server Locations
|
### Update Server Locations
|
||||||
Edit `src/components/ServerMap.tsx`:
|
Edit `src/components/ServerMap.tsx`:
|
||||||
```typescript
|
```typescript
|
||||||
const servers: ServerLocation[] = [
|
const servers: ServerLocation[] = [
|
||||||
{
|
{
|
||||||
id: "your-server",
|
id: "your-server",
|
||||||
country: "Country",
|
country: "Country",
|
||||||
city: "City",
|
city: "City",
|
||||||
x: 50, // % from left
|
x: 50, // % from left
|
||||||
y: 50, // % from top
|
y: 50, // % from top
|
||||||
users: 1000,
|
users: 1000,
|
||||||
latency: 10,
|
latency: 10,
|
||||||
status: "online"
|
status: "online"
|
||||||
},
|
},
|
||||||
// ... more servers
|
// ... more servers
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adjust Particle Count
|
### Adjust Particle Count
|
||||||
Edit `src/components/ParticlesBackground.tsx`:
|
Edit `src/components/ParticlesBackground.tsx`:
|
||||||
```typescript
|
```typescript
|
||||||
// Line ~35
|
// Line ~35
|
||||||
const particleCount = Math.min(100, Math.floor((canvas.width * canvas.height) / 15000));
|
const particleCount = Math.min(100, Math.floor((canvas.width * canvas.height) / 15000));
|
||||||
// Increase divisor (15000) for fewer particles
|
// Increase divisor (15000) for fewer particles
|
||||||
// Decrease for more particles
|
// Decrease for more particles
|
||||||
```
|
```
|
||||||
|
|
||||||
### Change Colors
|
### Change Colors
|
||||||
Edit `src/styles.css`:
|
Edit `src/styles.css`:
|
||||||
```css
|
```css
|
||||||
--gekon-green: oklch(0.72 0.19 160);
|
--gekon-green: oklch(0.72 0.19 160);
|
||||||
--gekon-cyan: oklch(0.7 0.14 200);
|
--gekon-cyan: oklch(0.7 0.14 200);
|
||||||
--gekon-purple: oklch(0.6 0.25 300);
|
--gekon-purple: oklch(0.6 0.25 300);
|
||||||
--gekon-neon: oklch(0.75 0.2 150);
|
--gekon-neon: oklch(0.75 0.2 150);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
gekon-speed-boost-3ba17197-main/
|
gekon-speed-boost-3ba17197-main/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── ParticlesBackground.tsx ← NEW: Animated particles
|
│ │ ├── ParticlesBackground.tsx ← NEW: Animated particles
|
||||||
│ │ ├── ServerMap.tsx ← NEW: Interactive map
|
│ │ ├── ServerMap.tsx ← NEW: Interactive map
|
||||||
│ │ ├── HeroSection.tsx
|
│ │ ├── HeroSection.tsx
|
||||||
│ │ ├── Navbar.tsx
|
│ │ ├── Navbar.tsx
|
||||||
│ │ ├── FeaturesSection.tsx
|
│ │ ├── FeaturesSection.tsx
|
||||||
│ │ ├── PricingSection.tsx
|
│ │ ├── PricingSection.tsx
|
||||||
│ │ └── ... other components
|
│ │ └── ... other components
|
||||||
│ ├── i18n/
|
│ ├── i18n/
|
||||||
│ │ ├── translations.ts ← Updated with new keys
|
│ │ ├── translations.ts ← Updated with new keys
|
||||||
│ │ └── context.tsx
|
│ │ └── context.tsx
|
||||||
│ ├── routes/
|
│ ├── routes/
|
||||||
│ │ └── index.tsx ← Updated with new components
|
│ │ └── index.tsx ← Updated with new components
|
||||||
│ └── styles.css
|
│ └── styles.css
|
||||||
├── package.json
|
├── package.json
|
||||||
└── vite.config.ts
|
└── vite.config.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Tech Stack
|
## 🔧 Tech Stack
|
||||||
|
|
||||||
- **Framework:** React 19 + TypeScript
|
- **Framework:** React 19 + TypeScript
|
||||||
- **Router:** TanStack Router
|
- **Router:** TanStack Router
|
||||||
- **Styling:** Tailwind CSS 4.2
|
- **Styling:** Tailwind CSS 4.2
|
||||||
- **UI Components:** Radix UI
|
- **UI Components:** Radix UI
|
||||||
- **Icons:** Lucide React
|
- **Icons:** Lucide React
|
||||||
- **Build Tool:** Vite
|
- **Build Tool:** Vite
|
||||||
- **Runtime:** Node.js / Bun
|
- **Runtime:** Node.js / Bun
|
||||||
|
|
||||||
## 🚀 Deployment
|
## 🚀 Deployment
|
||||||
|
|
||||||
### Cloudflare Pages (Recommended)
|
### Cloudflare Pages (Recommended)
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
# Upload dist/ folder to Cloudflare Pages
|
# Upload dist/ folder to Cloudflare Pages
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vercel
|
### Vercel
|
||||||
```bash
|
```bash
|
||||||
vercel deploy
|
vercel deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
### Netlify
|
### Netlify
|
||||||
```bash
|
```bash
|
||||||
netlify deploy --prod
|
netlify deploy --prod
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📊 Performance Tips
|
## 📊 Performance Tips
|
||||||
|
|
||||||
1. **Reduce particles on mobile:**
|
1. **Reduce particles on mobile:**
|
||||||
- Particles automatically scale based on screen size
|
- Particles automatically scale based on screen size
|
||||||
- Adjust in `ParticlesBackground.tsx`
|
- Adjust in `ParticlesBackground.tsx`
|
||||||
|
|
||||||
2. **Lazy load images:**
|
2. **Lazy load images:**
|
||||||
- Add `loading="lazy"` to img tags
|
- Add `loading="lazy"` to img tags
|
||||||
|
|
||||||
3. **Optimize bundle:**
|
3. **Optimize bundle:**
|
||||||
- Run `npm run build` to see bundle analysis
|
- Run `npm run build` to see bundle analysis
|
||||||
- Consider code splitting for large components
|
- Consider code splitting for large components
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
### Particles not showing
|
### Particles not showing
|
||||||
- Check browser console for errors
|
- Check browser console for errors
|
||||||
- Ensure canvas is supported
|
- Ensure canvas is supported
|
||||||
- Try reducing particle count
|
- Try reducing particle count
|
||||||
|
|
||||||
### Map tooltips not working
|
### Map tooltips not working
|
||||||
- Check z-index conflicts
|
- Check z-index conflicts
|
||||||
- Ensure hover events are not blocked
|
- Ensure hover events are not blocked
|
||||||
- Test on different browsers
|
- Test on different browsers
|
||||||
|
|
||||||
### Build errors
|
### Build errors
|
||||||
```bash
|
```bash
|
||||||
# Clear cache and reinstall
|
# Clear cache and reinstall
|
||||||
rm -rf node_modules package-lock.json
|
rm -rf node_modules package-lock.json
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📝 Next Steps
|
## 📝 Next Steps
|
||||||
|
|
||||||
1. **Add real subscription links** from your Sub-Bridge config
|
1. **Add real subscription links** from your Sub-Bridge config
|
||||||
2. **Integrate Telegram bot** for support
|
2. **Integrate Telegram bot** for support
|
||||||
3. **Add analytics** (Google Analytics, Plausible)
|
3. **Add analytics** (Google Analytics, Plausible)
|
||||||
4. **Optimize SEO** with meta tags
|
4. **Optimize SEO** with meta tags
|
||||||
5. **Add cookie consent** for GDPR
|
5. **Add cookie consent** for GDPR
|
||||||
|
|
||||||
## 🎯 Integration with Sub-Bridge
|
## 🎯 Integration with Sub-Bridge
|
||||||
|
|
||||||
To connect with your real VPN service:
|
To connect with your real VPN service:
|
||||||
|
|
||||||
1. **Update subscription URL:**
|
1. **Update subscription URL:**
|
||||||
```typescript
|
```typescript
|
||||||
// src/config/subscription.ts (create this file)
|
// src/config/subscription.ts (create this file)
|
||||||
export const SUBSCRIPTION_CONFIG = {
|
export const SUBSCRIPTION_CONFIG = {
|
||||||
baseUrl: 'https://subb.ydns.eu',
|
baseUrl: 'https://subb.ydns.eu',
|
||||||
supportUrl: 'https://subb.ydns.eu/',
|
supportUrl: 'https://subb.ydns.eu/',
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Update CTA buttons:**
|
2. **Update CTA buttons:**
|
||||||
```typescript
|
```typescript
|
||||||
// In components (HeroSection, PricingSection, etc.)
|
// In components (HeroSection, PricingSection, etc.)
|
||||||
<Button onClick={() => window.location.href = SUBSCRIPTION_CONFIG.baseUrl}>
|
<Button onClick={() => window.location.href = SUBSCRIPTION_CONFIG.baseUrl}>
|
||||||
Get Started
|
Get Started
|
||||||
</Button>
|
</Button>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📞 Support
|
## 📞 Support
|
||||||
|
|
||||||
For issues or questions:
|
For issues or questions:
|
||||||
- Check documentation in `/docs`
|
- Check documentation in `/docs`
|
||||||
- Review component source code
|
- Review component source code
|
||||||
- Test in different browsers
|
- Test in different browsers
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version:** 1.0.0
|
**Version:** 1.0.0
|
||||||
**Last Updated:** 2026-01-23
|
**Last Updated:** 2026-01-23
|
||||||
**Built with:** React + TypeScript + Tailwind CSS
|
**Built with:** React + TypeScript + Tailwind CSS
|
||||||
|
|||||||
+13
-13
@@ -1,13 +1,13 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
gekon-web:
|
gekon-web:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
command: npm run dev -- --host 0.0.0.0 --port 5173
|
command: npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
|||||||
Generated
+10031
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
@@ -23,23 +23,35 @@ export function FAQSection() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="faq" className="relative px-4 py-24 sm:py-32">
|
<section id="faq" className="relative px-4 pb-16 pt-5 sm:pb-20 sm:pt-5">
|
||||||
<div className="mx-auto max-w-3xl" ref={ref}>
|
<div className="mx-auto max-w-3xl" ref={ref}>
|
||||||
<h2 className={`mb-12 text-center text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
<div className="pointer-events-none absolute left-1/2 top-16 h-64 w-64 -translate-x-1/2 rounded-full bg-gekon-green/10 blur-3xl" />
|
||||||
|
|
||||||
|
<h2 className={`mb-8 text-center text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||||
{t("faq_title_1")} <span className="gradient-text">{t("faq_title_2")}</span>
|
{t("faq_title_1")} <span className="gradient-text">{t("faq_title_2")}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<Accordion type="single" collapsible className={`space-y-3 ${isVisible ? "animate-fade-up-delay-1" : "opacity-0"}`}>
|
<Accordion type="single" collapsible className="space-y-3.5">
|
||||||
{faqs.map((faq, i) => (
|
{faqs.map((faq, i) => (
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
key={i}
|
key={i}
|
||||||
value={`faq-${i}`}
|
value={`faq-${i}`}
|
||||||
className="glass-card rounded-xl border-none px-6"
|
className={`group relative overflow-hidden !border-none rounded-2xl border border-gekon-green/20 bg-gradient-to-br from-background/92 via-background/82 to-background/92 px-5 shadow-[0_0_0_1px_rgba(16,185,129,0.06)] transition-all duration-300 hover:-translate-y-0.5 hover:border-gekon-green/35 hover:shadow-[0_12px_34px_rgba(16,185,129,0.14)] data-[state=open]:border-gekon-green/45 data-[state=open]:shadow-[0_14px_38px_rgba(16,185,129,0.2)] sm:px-6 ${
|
||||||
|
isVisible ? "animate-fade-up" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
style={{ animationDelay: `${0.08 + i * 0.08}s` }}
|
||||||
>
|
>
|
||||||
<AccordionTrigger className="text-left text-sm font-semibold hover:no-underline sm:text-base">
|
<div className="pointer-events-none absolute -right-12 -top-12 h-28 w-28 rounded-full bg-gekon-green/15 blur-2xl opacity-60 transition-opacity duration-300 group-hover:opacity-100 group-data-[state=open]:opacity-100" />
|
||||||
{t(faq.qKey)}
|
|
||||||
|
<AccordionTrigger className="gap-4 py-4 text-left text-base font-semibold text-foreground transition-colors hover:no-underline sm:text-lg [&>svg]:text-gekon-green/80 [&>svg]:transition-colors group-hover:[&>svg]:text-gekon-green data-[state=open]:text-gekon-green">
|
||||||
|
<span className="flex items-center gap-3">
|
||||||
|
<span className="inline-flex h-7 min-w-7 items-center justify-center rounded-full border border-gekon-green/35 bg-gekon-green/12 px-1.5 text-[11px] font-mono tracking-wide text-gekon-green">
|
||||||
|
{(i + 1).toString().padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
<span>{t(faq.qKey)}</span>
|
||||||
|
</span>
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="text-sm leading-relaxed text-muted-foreground">
|
<AccordionContent className="pb-5 pl-10 pr-1 text-sm leading-relaxed text-muted-foreground sm:text-[15px]">
|
||||||
{t(faq.aKey)}
|
{t(faq.aKey)}
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ export function FeaturesSection() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="technology" className="relative px-4 py-24 sm:py-32">
|
<section id="technology" className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||||
<div className="mx-auto max-w-7xl" ref={ref}>
|
<div className="mx-auto max-w-7xl" ref={ref}>
|
||||||
<div className="mb-16 text-center">
|
<div className="mb-7 text-center">
|
||||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
<h2 className={`mb-3 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||||
{t("features_title_1")} <span className="gradient-text">{t("features_title_2")}</span>
|
{t("features_title_1")} <span className="gradient-text">{t("features_title_2")}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className={`mx-auto max-w-2xl text-muted-foreground ${isVisible ? "animate-fade-up-delay-1" : "opacity-0"}`}>
|
<p className={`mx-auto max-w-2xl text-muted-foreground ${isVisible ? "animate-fade-up-delay-1" : "opacity-0"}`}>
|
||||||
@@ -28,20 +28,20 @@ export function FeaturesSection() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{features.map((feature, i) => (
|
{features.map((feature, i) => (
|
||||||
<div
|
<div
|
||||||
key={feature.titleKey}
|
key={feature.titleKey}
|
||||||
className={`glass-card group rounded-xl p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:shadow-gekon-green/5 ${
|
className={`glass-card group rounded-lg p-4 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:shadow-gekon-green/5 ${
|
||||||
isVisible ? "animate-fade-up" : "opacity-0"
|
isVisible ? "animate-fade-up" : "opacity-0"
|
||||||
}`}
|
}`}
|
||||||
style={{ animationDelay: `${i * 0.1}s` }}
|
style={{ animationDelay: `${i * 0.1}s` }}
|
||||||
>
|
>
|
||||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br from-gekon-green/20 to-gekon-cyan/20 transition-all group-hover:from-gekon-green/30 group-hover:to-gekon-cyan/30">
|
<div className="mb-3 flex h-9 w-9 items-center justify-center rounded-md bg-gradient-to-br from-gekon-green/20 to-gekon-cyan/20 transition-all group-hover:from-gekon-green/30 group-hover:to-gekon-cyan/30">
|
||||||
<feature.icon size={24} className="text-gekon-green" />
|
<feature.icon size={19} className="text-gekon-green" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-2 text-lg font-semibold text-foreground">{t(feature.titleKey)}</h3>
|
<h3 className="mb-1.5 text-base font-semibold text-foreground">{t(feature.titleKey)}</h3>
|
||||||
<p className="text-sm leading-relaxed text-muted-foreground">{t(feature.descKey)}</p>
|
<p className="text-sm leading-snug text-muted-foreground">{t(feature.descKey)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function FinalCTASection() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden px-4 py-24 sm:py-32">
|
<section className="relative overflow-hidden px-4 py-16 sm:py-20">
|
||||||
<div className="pointer-events-none absolute inset-0 bg-radial-glow" />
|
<div className="pointer-events-none absolute inset-0 bg-radial-glow" />
|
||||||
<div className="pointer-events-none absolute inset-0 bg-grid-pattern opacity-50" />
|
<div className="pointer-events-none absolute inset-0 bg-grid-pattern opacity-50" />
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,13 @@ export function Footer() {
|
|||||||
<div className="mx-auto max-w-7xl">
|
<div className="mx-auto max-w-7xl">
|
||||||
<div className="grid gap-8 sm:grid-cols-2 md:grid-cols-5">
|
<div className="grid gap-8 sm:grid-cols-2 md:grid-cols-5">
|
||||||
<div className="md:col-span-1">
|
<div className="md:col-span-1">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-gekon-green to-gekon-cyan">
|
<img
|
||||||
<span className="text-sm font-bold text-primary-foreground">G</span>
|
src="/gekon-logo.png"
|
||||||
</div>
|
alt="Gekon Fast logo"
|
||||||
|
className="h-8 w-8 rounded-md object-contain"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
<span className="text-lg font-bold">Gekon</span>
|
<span className="text-lg font-bold">Gekon</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
|||||||
+113
-70
@@ -1,88 +1,131 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Zap, ArrowRight } from "lucide-react";
|
import { ArrowRight, Github, Instagram, MessageCircle, Send, Youtube, Zap } from "lucide-react";
|
||||||
import { useI18n } from "@/i18n/context";
|
import { useI18n } from "@/i18n/context";
|
||||||
|
|
||||||
export function HeroSection() {
|
export function HeroSection() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const motionClasses = [
|
||||||
|
"tag-cloud-motion-a",
|
||||||
|
"tag-cloud-motion-b",
|
||||||
|
"tag-cloud-motion-c",
|
||||||
|
"tag-cloud-motion-d",
|
||||||
|
];
|
||||||
|
const cloudTags = [
|
||||||
|
{ label: "YouTube", x: 14, y: 18, style: "service" },
|
||||||
|
{ label: "Discord", x: 44, y: 12, style: "service" },
|
||||||
|
{ label: "Telegram", x: 70, y: 20, style: "service" },
|
||||||
|
{ label: "ChatGPT", x: 24, y: 34, style: "service" },
|
||||||
|
{ label: "Spotify", x: 56, y: 30, style: "service" },
|
||||||
|
{ label: "TikTok", x: 80, y: 34, style: "service" },
|
||||||
|
{ label: "Instagram", x: 12, y: 50, style: "service" },
|
||||||
|
{ label: "X (Twitter)", x: 40, y: 48, style: "service" },
|
||||||
|
{ label: "Facebook", x: 68, y: 48, style: "service" },
|
||||||
|
{ label: "Threads", x: 20, y: 64, style: "service" },
|
||||||
|
{ label: "Twitch", x: 52, y: 66, style: "service" },
|
||||||
|
{ label: "Reddit", x: 82, y: 62, style: "service" },
|
||||||
|
{ label: "Windows", x: 8, y: 78, style: "platform" },
|
||||||
|
{ label: "macOS", x: 30, y: 78, style: "platform" },
|
||||||
|
{ label: "iOS", x: 48, y: 80, style: "platform" },
|
||||||
|
{ label: "Android", x: 66, y: 78, style: "platform" },
|
||||||
|
{ label: "Linux", x: 86, y: 78, style: "platform" },
|
||||||
|
{ label: "Wi‑Fi", x: 18, y: 90, style: "platform" },
|
||||||
|
{ label: "Smart TV", x: 42, y: 92, style: "platform" },
|
||||||
|
{ label: "Chrome", x: 66, y: 92, style: "platform" },
|
||||||
|
{ label: "Firefox", x: 86, y: 90, style: "platform" },
|
||||||
|
];
|
||||||
|
const socialLinks = [
|
||||||
|
{ name: "Telegram", href: "#", icon: Send },
|
||||||
|
{ name: "Instagram", href: "#", icon: Instagram },
|
||||||
|
{ name: "ВКонтакте", href: "#", icon: MessageCircle },
|
||||||
|
{ name: "GitHub", href: "#", icon: Github },
|
||||||
|
{ name: "YouTube", href: "#", icon: Youtube },
|
||||||
|
{ name: "Threads", href: "#", icon: MessageCircle },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative flex min-h-screen items-center justify-center overflow-hidden bg-grid-pattern bg-radial-glow px-4 pt-16">
|
<section className="relative flex items-start overflow-hidden bg-grid-pattern bg-radial-glow px-4 pb-4 pt-16 sm:pb-5">
|
||||||
<div className="pointer-events-none absolute top-1/4 left-1/4 h-96 w-96 rounded-full bg-gekon-green/5 blur-[128px]" />
|
<div className="pointer-events-none absolute top-1/4 left-1/4 h-96 w-96 rounded-full bg-gekon-green/5 blur-[128px]" />
|
||||||
<div className="pointer-events-none absolute bottom-1/4 right-1/4 h-96 w-96 rounded-full bg-gekon-cyan/5 blur-[128px]" />
|
<div className="pointer-events-none absolute bottom-1/4 right-1/4 h-96 w-96 rounded-full bg-gekon-cyan/5 blur-[128px]" />
|
||||||
|
|
||||||
<div className="relative z-10 mx-auto max-w-5xl text-center">
|
<div className="relative z-10 mx-auto w-full max-w-7xl">
|
||||||
<div className="animate-fade-up mb-6 inline-flex items-center gap-2 rounded-full border border-gekon-green/20 bg-gekon-green/5 px-4 py-1.5 text-sm text-gekon-green">
|
<div className="grid items-start gap-6 lg:grid-cols-[1.1fr_0.9fr] lg:gap-4">
|
||||||
<Zap size={14} />
|
<div className="text-center lg:text-left">
|
||||||
<span>{t("hero_badge")}</span>
|
<div className="animate-fade-up mb-6 inline-flex items-center gap-2 rounded-full border border-gekon-green/20 bg-gekon-green/5 px-4 py-1.5 text-sm text-gekon-green">
|
||||||
</div>
|
<Zap size={14} />
|
||||||
|
<span>{t("hero_badge")}</span>
|
||||||
<h1 className="animate-fade-up-delay-1 mb-6 text-4xl font-extrabold leading-tight tracking-tight sm:text-5xl md:text-7xl">
|
|
||||||
{t("hero_title_1")}{" "}
|
|
||||||
<span className="gradient-text">{t("hero_title_2")}</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="animate-fade-up-delay-2 mx-auto mb-10 max-w-2xl text-lg text-muted-foreground sm:text-xl">
|
|
||||||
{t("hero_subtitle")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="animate-fade-up-delay-3 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
|
||||||
<Button variant="glow" size="xl">
|
|
||||||
{t("hero_cta_primary")}
|
|
||||||
<ArrowRight size={18} />
|
|
||||||
</Button>
|
|
||||||
<Button variant="glow-outline" size="xl">
|
|
||||||
{t("hero_cta_secondary")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="animate-fade-up-delay-3 mx-auto mt-16 max-w-3xl">
|
|
||||||
<div className="glass-card rounded-2xl p-6 sm:p-8">
|
|
||||||
<div className="mb-4 flex items-center justify-between text-sm text-muted-foreground">
|
|
||||||
<span>{t("hero_speed_label")}</span>
|
|
||||||
<span className="font-mono text-gekon-green">{t("hero_speed_optimized")}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
|
||||||
<SpeedBar label={t("hero_speed_download")} before="50 Mbps" after="150 Mbps" percent={90} />
|
<h1 className="animate-fade-up-delay-1 mb-5 text-4xl font-extrabold leading-tight tracking-tight sm:text-5xl md:text-6xl">
|
||||||
<SpeedBar label={t("hero_speed_upload")} before="20 Mbps" after="60 Mbps" percent={75} />
|
{t("hero_title_1")}{" "}
|
||||||
<SpeedBar label={t("hero_speed_latency")} before="120ms" after="40ms" percent={95} isReduced />
|
<span className="gradient-text">{t("hero_title_2")}</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="animate-fade-up-delay-2 mx-auto mb-8 max-w-2xl text-lg text-muted-foreground sm:text-xl lg:mx-0">
|
||||||
|
{t("hero_subtitle")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="animate-fade-up-delay-3 flex flex-col items-center gap-4 sm:flex-row sm:justify-center lg:justify-start">
|
||||||
|
<Button variant="glow" size="xl">
|
||||||
|
{t("hero_cta_primary")}
|
||||||
|
<ArrowRight size={18} />
|
||||||
|
</Button>
|
||||||
|
<Button variant="glow-outline" size="xl">
|
||||||
|
{t("hero_cta_secondary")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 w-full max-w-[540px]">
|
||||||
|
<div className="flex items-center justify-center gap-3 lg:justify-start">
|
||||||
|
{socialLinks.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
title={item.name}
|
||||||
|
aria-label={item.name}
|
||||||
|
className="inline-flex h-11 w-11 items-center justify-center rounded-full border border-gekon-green/60 bg-gekon-green/15 text-gekon-green shadow-[0_0_16px_rgba(16,185,129,0.25)] transition-all hover:-translate-y-0.5 hover:border-gekon-green hover:bg-gekon-green/25 hover:shadow-[0_0_24px_rgba(16,185,129,0.45)]"
|
||||||
|
>
|
||||||
|
<item.icon size={19} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center lg:justify-start">
|
||||||
|
<div className="flex w-full max-w-[430px] flex-col items-center lg:items-start">
|
||||||
|
<div className="relative h-[320px] w-[320px] shrink-0 sm:h-[370px] sm:w-[370px] lg:h-[400px] lg:w-[400px]">
|
||||||
|
{cloudTags.map((tag, index) => (
|
||||||
|
<div
|
||||||
|
key={tag.label}
|
||||||
|
className="absolute -translate-x-1/2 -translate-y-1/2"
|
||||||
|
style={{ left: `${tag.x}%`, top: `${tag.y}%` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={motionClasses[index % motionClasses.length]}
|
||||||
|
style={{ animationDelay: `${(index % 7) * 0.42}s` }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={tag.label}
|
||||||
|
className={`inline-flex rounded-full border px-3 py-1.5 text-sm font-semibold backdrop-blur-sm ${
|
||||||
|
tag.style === "service"
|
||||||
|
? "tag-cloud-item border-gekon-green/35 bg-background/80 text-gekon-green shadow-lg shadow-gekon-green/10 hover:border-gekon-green/60 hover:shadow-gekon-green/30"
|
||||||
|
: "tag-cloud-item border-gekon-cyan/35 bg-background/85 text-gekon-cyan shadow-md shadow-gekon-cyan/10 hover:border-gekon-cyan/60 hover:shadow-gekon-cyan/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SpeedBar({
|
|
||||||
label,
|
|
||||||
before,
|
|
||||||
after,
|
|
||||||
percent,
|
|
||||||
isReduced,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
before: string;
|
|
||||||
after: string;
|
|
||||||
percent: number;
|
|
||||||
isReduced?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 flex items-center justify-between text-xs">
|
|
||||||
<span className="text-muted-foreground">{label}</span>
|
|
||||||
<span className="font-mono">
|
|
||||||
<span className="text-muted-foreground line-through">{before}</span>
|
|
||||||
{" → "}
|
|
||||||
<span className="text-gekon-green font-semibold">{after}</span>
|
|
||||||
{isReduced && <span className="ml-1 text-gekon-green">↓</span>}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-2 overflow-hidden rounded-full bg-secondary">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full bg-gradient-to-r from-gekon-green to-gekon-cyan transition-all duration-1000"
|
|
||||||
style={{ width: `${percent}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ export function HowItWorksSection() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative px-4 py-24 sm:py-32">
|
<section className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||||
<div className="mx-auto max-w-5xl" ref={ref}>
|
<div className="mx-auto max-w-5xl" ref={ref}>
|
||||||
<div className="mb-16 text-center">
|
<div className="mb-10 text-center">
|
||||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||||
{t("how_title_1")} <span className="gradient-text">{t("how_title_2")}</span>
|
{t("how_title_1")} <span className="gradient-text">{t("how_title_2")}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -34,10 +34,13 @@ export function Navbar() {
|
|||||||
>
|
>
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex h-16 items-center justify-between">
|
<div className="flex h-16 items-center justify-between">
|
||||||
<a href="#" className="flex items-center gap-2">
|
<a href="#" className="flex items-center gap-3">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-gekon-green to-gekon-cyan">
|
<img
|
||||||
<span className="text-sm font-bold text-primary-foreground">G</span>
|
src="/gekon-logo.png"
|
||||||
</div>
|
alt="Gekon Fast logo"
|
||||||
|
className="h-9 w-9 rounded-md object-contain"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
<span className="text-xl font-bold tracking-tight text-foreground">
|
<span className="text-xl font-bold tracking-tight text-foreground">
|
||||||
Gekon
|
Gekon
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,105 +1,105 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
interface Particle {
|
interface Particle {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
vx: number;
|
vx: number;
|
||||||
vy: number;
|
vy: number;
|
||||||
size: number;
|
size: number;
|
||||||
opacity: number;
|
opacity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ParticlesBackground() {
|
export function ParticlesBackground() {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const particlesRef = useRef<Particle[]>([]);
|
const particlesRef = useRef<Particle[]>([]);
|
||||||
const animationFrameRef = useRef<number>();
|
const animationFrameRef = useRef<number>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
// Set canvas size
|
// Set canvas size
|
||||||
const resizeCanvas = () => {
|
const resizeCanvas = () => {
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight;
|
||||||
};
|
};
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
window.addEventListener("resize", resizeCanvas);
|
window.addEventListener("resize", resizeCanvas);
|
||||||
|
|
||||||
// Initialize particles
|
// Initialize particles
|
||||||
const particleCount = Math.min(150, Math.floor((canvas.width * canvas.height) / 12000));
|
const particleCount = Math.min(150, Math.floor((canvas.width * canvas.height) / 12000));
|
||||||
particlesRef.current = Array.from({ length: particleCount }, () => ({
|
particlesRef.current = Array.from({ length: particleCount }, () => ({
|
||||||
x: Math.random() * canvas.width,
|
x: Math.random() * canvas.width,
|
||||||
y: Math.random() * canvas.height,
|
y: Math.random() * canvas.height,
|
||||||
vx: (Math.random() - 0.5) * 0.8,
|
vx: (Math.random() - 0.5) * 0.8,
|
||||||
vy: (Math.random() - 0.5) * 0.8,
|
vy: (Math.random() - 0.5) * 0.8,
|
||||||
size: Math.random() * 3 + 1.5,
|
size: Math.random() * 3 + 1.5,
|
||||||
opacity: Math.random() * 0.6 + 0.3,
|
opacity: Math.random() * 0.6 + 0.3,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Animation loop
|
// Animation loop
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// Update and draw particles
|
// Update and draw particles
|
||||||
particlesRef.current.forEach((particle) => {
|
particlesRef.current.forEach((particle) => {
|
||||||
// Update position
|
// Update position
|
||||||
particle.x += particle.vx;
|
particle.x += particle.vx;
|
||||||
particle.y += particle.vy;
|
particle.y += particle.vy;
|
||||||
|
|
||||||
// Wrap around edges
|
// Wrap around edges
|
||||||
if (particle.x < 0) particle.x = canvas.width;
|
if (particle.x < 0) particle.x = canvas.width;
|
||||||
if (particle.x > canvas.width) particle.x = 0;
|
if (particle.x > canvas.width) particle.x = 0;
|
||||||
if (particle.y < 0) particle.y = canvas.height;
|
if (particle.y < 0) particle.y = canvas.height;
|
||||||
if (particle.y > canvas.height) particle.y = 0;
|
if (particle.y > canvas.height) particle.y = 0;
|
||||||
|
|
||||||
// Draw particle
|
// Draw particle
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||||
ctx.fillStyle = `rgba(16, 185, 129, ${particle.opacity})`; // gekon-green
|
ctx.fillStyle = `rgba(16, 185, 129, ${particle.opacity})`; // gekon-green
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Draw connections
|
// Draw connections
|
||||||
particlesRef.current.forEach((p1, i) => {
|
particlesRef.current.forEach((p1, i) => {
|
||||||
particlesRef.current.slice(i + 1).forEach((p2) => {
|
particlesRef.current.slice(i + 1).forEach((p2) => {
|
||||||
const dx = p1.x - p2.x;
|
const dx = p1.x - p2.x;
|
||||||
const dy = p1.y - p2.y;
|
const dy = p1.y - p2.y;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
if (distance < 150) {
|
if (distance < 150) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(p1.x, p1.y);
|
ctx.moveTo(p1.x, p1.y);
|
||||||
ctx.lineTo(p2.x, p2.y);
|
ctx.lineTo(p2.x, p2.y);
|
||||||
const opacity = (1 - distance / 150) * 0.2;
|
const opacity = (1 - distance / 150) * 0.2;
|
||||||
ctx.strokeStyle = `rgba(6, 182, 212, ${opacity})`; // gekon-cyan
|
ctx.strokeStyle = `rgba(6, 182, 212, ${opacity})`; // gekon-cyan
|
||||||
ctx.lineWidth = 0.5;
|
ctx.lineWidth = 0.5;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
animationFrameRef.current = requestAnimationFrame(animate);
|
animationFrameRef.current = requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
|
|
||||||
animate();
|
animate();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("resize", resizeCanvas);
|
window.removeEventListener("resize", resizeCanvas);
|
||||||
if (animationFrameRef.current) {
|
if (animationFrameRef.current) {
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={canvasRef}
|
||||||
className="pointer-events-none absolute inset-0 opacity-60"
|
className="pointer-events-none absolute inset-0 opacity-60"
|
||||||
style={{ mixBlendMode: "screen", zIndex: 1 }}
|
style={{ mixBlendMode: "screen", zIndex: 1 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,86 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
||||||
import { useI18n } from "@/i18n/context";
|
import { useI18n } from "@/i18n/context";
|
||||||
import type { TranslationKeys } from "@/i18n/translations";
|
import type { TranslationKeys } from "@/i18n/translations";
|
||||||
|
|
||||||
const metrics: { labelKey: TranslationKeys; before: string; after: string; improvement: string }[] = [
|
type AnimatedValue = {
|
||||||
{ labelKey: "perf_download", before: "50 Mbps", after: "150 Mbps", improvement: "3x" },
|
from: number;
|
||||||
{ labelKey: "perf_latency", before: "120ms", after: "40ms", improvement: "67% ↓" },
|
to: number;
|
||||||
{ labelKey: "perf_streaming", before: "720p", after: "4K", improvement: "UHD" },
|
unit: string;
|
||||||
{ labelKey: "perf_gaming", before: "80ms", after: "25ms", improvement: "69% ↓" },
|
spaceBeforeUnit?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const metrics: {
|
||||||
|
labelKey: TranslationKeys;
|
||||||
|
before: AnimatedValue;
|
||||||
|
after: AnimatedValue;
|
||||||
|
improvement: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
labelKey: "perf_download",
|
||||||
|
before: { from: 0, to: 50, unit: "Mbps", spaceBeforeUnit: true },
|
||||||
|
after: { from: 0, to: 150, unit: "Mbps", spaceBeforeUnit: true },
|
||||||
|
improvement: "3x",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "perf_latency",
|
||||||
|
before: { from: 0, to: 120, unit: "ms" },
|
||||||
|
after: { from: 0, to: 40, unit: "ms" },
|
||||||
|
improvement: "67% ↓",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "perf_streaming",
|
||||||
|
before: { from: 0, to: 720, unit: "p" },
|
||||||
|
after: { from: 0, to: 4, unit: "K" },
|
||||||
|
improvement: "UHD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelKey: "perf_gaming",
|
||||||
|
before: { from: 0, to: 80, unit: "ms" },
|
||||||
|
after: { from: 0, to: 25, unit: "ms" },
|
||||||
|
improvement: "69% ↓",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function PerformanceSection() {
|
export function PerformanceSection() {
|
||||||
const { ref, isVisible } = useScrollAnimation();
|
const { ref, isVisible } = useScrollAnimation();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVisible) {
|
||||||
|
setProgress(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationFrame = 0;
|
||||||
|
let startTime: number | null = null;
|
||||||
|
const duration = 1500;
|
||||||
|
|
||||||
|
const animate = (timestamp: number) => {
|
||||||
|
if (startTime === null) startTime = timestamp;
|
||||||
|
const linearProgress = Math.min((timestamp - startTime) / duration, 1);
|
||||||
|
const easedProgress = 1 - Math.pow(1 - linearProgress, 3);
|
||||||
|
setProgress(easedProgress);
|
||||||
|
|
||||||
|
if (linearProgress < 1) {
|
||||||
|
animationFrame = window.requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
animationFrame = window.requestAnimationFrame(animate);
|
||||||
|
return () => window.cancelAnimationFrame(animationFrame);
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
const formatAnimated = (value: AnimatedValue) => {
|
||||||
|
const current = Math.round(value.from + (value.to - value.from) * progress);
|
||||||
|
return `${current}${value.spaceBeforeUnit ? " " : ""}${value.unit}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="performance" className="relative px-4 py-24 sm:py-32">
|
<section id="performance" className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||||
<div className="mx-auto max-w-5xl" ref={ref}>
|
<div className="mx-auto max-w-5xl" ref={ref}>
|
||||||
<div className="mb-16 text-center">
|
<div className="mb-10 text-center">
|
||||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||||
{t("perf_title_1")} <span className="gradient-text">{t("perf_title_2")}</span>
|
{t("perf_title_1")} <span className="gradient-text">{t("perf_title_2")}</span>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -37,14 +101,20 @@ export function PerformanceSection() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-sm text-muted-foreground mb-1">{t("perf_before")}</div>
|
<div className="text-sm text-muted-foreground mb-1">{t("perf_before")}</div>
|
||||||
<div className="font-mono text-xl text-muted-foreground">{m.before}</div>
|
<div className={`font-mono text-xl text-muted-foreground ${isVisible ? "animate-counter" : ""}`}>
|
||||||
|
{formatAnimated(m.before)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx-4 text-2xl text-gekon-green/90 transition-transform duration-500 group-hover:translate-x-0.5">
|
||||||
|
→
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-4 text-gekon-green text-2xl">→</div>
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-sm text-gekon-green mb-1">{t("perf_after")}</div>
|
<div className="text-sm text-gekon-green mb-1">{t("perf_after")}</div>
|
||||||
<div className="font-mono text-xl text-foreground font-bold">{m.after}</div>
|
<div className={`font-mono text-xl font-bold text-foreground ${isVisible ? "animate-counter" : ""}`}>
|
||||||
|
{formatAnimated(m.after)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 rounded-full bg-gekon-green/10 px-3 py-1 text-sm font-bold text-gekon-green">
|
<div className="ml-4 rounded-full border border-gekon-green/20 bg-gekon-green/10 px-3 py-1 text-sm font-bold text-gekon-green transition-all duration-300 group-hover:bg-gekon-green/15 group-hover:shadow-[0_0_18px_rgba(16,185,129,0.2)]">
|
||||||
{m.improvement}
|
{m.improvement}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function PricingSection() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="pricing" className="relative px-4 py-24 sm:py-32">
|
<section id="pricing" className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||||
<div className="mx-auto max-w-6xl" ref={ref}>
|
<div className="mx-auto max-w-6xl" ref={ref}>
|
||||||
<div className="mb-12 text-center">
|
<div className="mb-12 text-center">
|
||||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||||
|
|||||||
+263
-223
@@ -1,223 +1,263 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
import { useScrollAnimation } from "@/hooks/useScrollAnimation";
|
||||||
import { useI18n } from "@/i18n/context";
|
import { useI18n } from "@/i18n/context";
|
||||||
import { Server, Zap, Users } from "lucide-react";
|
import { Server, Zap, Users } from "lucide-react";
|
||||||
|
import worldMapOutline from "@/world-states.svg?url";
|
||||||
interface ServerLocation {
|
|
||||||
id: string;
|
interface ServerLocation {
|
||||||
country: string;
|
id: string;
|
||||||
city: string;
|
country: string;
|
||||||
x: number; // percentage from left
|
city: string;
|
||||||
y: number; // percentage from top
|
x: number; // percentage from left
|
||||||
users: number;
|
y: number; // percentage from top
|
||||||
latency: number;
|
users: number;
|
||||||
status: "online" | "maintenance";
|
latency: number;
|
||||||
}
|
status: "online" | "maintenance";
|
||||||
|
}
|
||||||
const servers: ServerLocation[] = [
|
|
||||||
// North America
|
const servers: ServerLocation[] = [
|
||||||
{ id: "us-west", country: "USA", city: "Los Angeles", x: 15, y: 35, users: 1250, latency: 12, status: "online" },
|
// North America
|
||||||
{ id: "us-east", country: "USA", city: "New York", x: 22, y: 32, users: 1840, latency: 8, status: "online" },
|
{ id: "us-west", country: "USA", city: "Los Angeles", x: 13, y: 35, users: 1250, latency: 12, status: "online" },
|
||||||
{ id: "canada", country: "Canada", city: "Toronto", x: 21, y: 28, users: 680, latency: 15, status: "online" },
|
{ id: "us-east", country: "USA", city: "New York", x: 20, y: 31, users: 1840, latency: 8, status: "online" },
|
||||||
|
{ id: "canada", country: "Canada", city: "Toronto", x: 20, y: 27, users: 680, latency: 15, status: "online" },
|
||||||
// South America
|
|
||||||
{ id: "brazil", country: "Brazil", city: "São Paulo", x: 30, y: 65, users: 520, latency: 45, status: "online" },
|
// South America
|
||||||
{ id: "argentina", country: "Argentina", city: "Buenos Aires", x: 28, y: 72, users: 280, latency: 52, status: "online" },
|
{ id: "brazil", country: "Brazil", city: "São Paulo", x: 30, y: 65, users: 520, latency: 45, status: "online" },
|
||||||
|
{ id: "argentina", country: "Argentina", city: "Buenos Aires", x: 28, y: 72, users: 280, latency: 52, status: "online" },
|
||||||
// Europe
|
|
||||||
{ id: "uk", country: "UK", city: "London", x: 48, y: 28, users: 2100, latency: 5, status: "online" },
|
// Europe
|
||||||
{ id: "germany", country: "Germany", city: "Frankfurt", x: 51, y: 30, users: 1650, latency: 7, status: "online" },
|
{ id: "uk", country: "UK", city: "London", x: 46.5, y: 27, users: 2100, latency: 5, status: "online" },
|
||||||
{ id: "france", country: "France", city: "Paris", x: 49, y: 32, users: 980, latency: 9, status: "online" },
|
{ id: "germany", country: "Germany", city: "Frankfurt", x: 49, y: 29, users: 1650, latency: 7, status: "online" },
|
||||||
{ id: "netherlands", country: "Netherlands", city: "Amsterdam", x: 50, y: 29, users: 1420, latency: 6, status: "online" },
|
{ id: "france", country: "France", city: "Paris", x: 47.8, y: 30, users: 980, latency: 9, status: "online" },
|
||||||
{ id: "sweden", country: "Sweden", city: "Stockholm", x: 53, y: 24, users: 540, latency: 12, status: "online" },
|
{ id: "netherlands", country: "Netherlands", city: "Amsterdam", x: 48.2, y: 28, users: 1420, latency: 6, status: "online" },
|
||||||
{ id: "poland", country: "Poland", city: "Warsaw", x: 54, y: 30, users: 720, latency: 14, status: "online" },
|
{ id: "sweden", country: "Sweden", city: "Stockholm", x: 51, y: 23, users: 540, latency: 12, status: "online" },
|
||||||
{ id: "spain", country: "Spain", city: "Madrid", x: 47, y: 35, users: 650, latency: 18, status: "online" },
|
{ id: "poland", country: "Poland", city: "Warsaw", x: 51.5, y: 28.5, users: 720, latency: 14, status: "online" },
|
||||||
{ id: "italy", country: "Italy", city: "Milan", x: 51, y: 34, users: 580, latency: 16, status: "online" },
|
{ id: "spain", country: "Spain", city: "Madrid", x: 46, y: 33, users: 650, latency: 18, status: "online" },
|
||||||
|
{ id: "italy", country: "Italy", city: "Milan", x: 49.5, y: 31.5, users: 580, latency: 16, status: "online" },
|
||||||
// Asia
|
|
||||||
{ id: "japan", country: "Japan", city: "Tokyo", x: 82, y: 35, users: 1950, latency: 8, status: "online" },
|
// Asia
|
||||||
{ id: "singapore", country: "Singapore", city: "Singapore", x: 75, y: 52, users: 1680, latency: 10, status: "online" },
|
{ id: "japan", country: "Japan", city: "Tokyo", x: 80.5, y: 35, users: 1950, latency: 8, status: "online" },
|
||||||
{ id: "hong-kong", country: "Hong Kong", city: "Hong Kong", x: 77, y: 42, users: 1420, latency: 12, status: "online" },
|
{ id: "singapore", country: "Singapore", city: "Singapore", x: 73, y: 52, users: 1680, latency: 10, status: "online" },
|
||||||
{ id: "south-korea", country: "South Korea", city: "Seoul", x: 80, y: 34, users: 1280, latency: 9, status: "online" },
|
{ id: "hong-kong", country: "Hong Kong", city: "Hong Kong", x: 76, y: 43, users: 1420, latency: 12, status: "online" },
|
||||||
{ id: "india", country: "India", city: "Mumbai", x: 68, y: 45, users: 890, latency: 25, status: "online" },
|
{ id: "south-korea", country: "South Korea", city: "Seoul", x: 79.5, y: 33, users: 1280, latency: 9, status: "online" },
|
||||||
{ id: "uae", country: "UAE", city: "Dubai", x: 60, y: 42, users: 740, latency: 22, status: "online" },
|
{ id: "india", country: "India", city: "Mumbai", x: 67, y: 45.5, users: 890, latency: 25, status: "online" },
|
||||||
{ id: "thailand", country: "Thailand", city: "Bangkok", x: 74, y: 48, users: 620, latency: 28, status: "online" },
|
{ id: "uae", country: "UAE", city: "Dubai", x: 59.5, y: 39.5, users: 740, latency: 22, status: "online" },
|
||||||
{ id: "vietnam", country: "Vietnam", city: "Ho Chi Minh", x: 76, y: 50, users: 480, latency: 32, status: "online" },
|
{ id: "thailand", country: "Thailand", city: "Bangkok", x: 74, y: 48.5, users: 620, latency: 28, status: "online" },
|
||||||
|
{ id: "vietnam", country: "Vietnam", city: "Ho Chi Minh", x: 75.5, y: 50.5, users: 480, latency: 32, status: "online" },
|
||||||
// Oceania
|
|
||||||
{ id: "australia", country: "Australia", city: "Sydney", x: 85, y: 72, users: 920, latency: 18, status: "online" },
|
// Oceania
|
||||||
{ id: "new-zealand", country: "New Zealand", city: "Auckland", x: 90, y: 75, users: 340, latency: 24, status: "online" },
|
{ id: "australia", country: "Australia", city: "Sydney", x: 84, y: 73, users: 920, latency: 18, status: "online" },
|
||||||
|
{ id: "new-zealand", country: "New Zealand", city: "Auckland", x: 89, y: 76, users: 340, latency: 24, status: "online" },
|
||||||
// Africa
|
|
||||||
{ id: "south-africa", country: "South Africa", city: "Cape Town", x: 52, y: 72, users: 380, latency: 48, status: "online" },
|
// Africa
|
||||||
|
{ id: "south-africa", country: "South Africa", city: "Cape Town", x: 51, y: 74, users: 380, latency: 48, status: "online" },
|
||||||
// Middle East
|
|
||||||
{ id: "turkey", country: "Turkey", city: "Istanbul", x: 56, y: 35, users: 680, latency: 20, status: "online" },
|
// Middle East
|
||||||
{ id: "israel", country: "Israel", city: "Tel Aviv", x: 57, y: 38, users: 420, latency: 26, status: "online" },
|
{ id: "turkey", country: "Turkey", city: "Istanbul", x: 54.5, y: 33, users: 680, latency: 20, status: "online" },
|
||||||
];
|
{ id: "israel", country: "Israel", city: "Tel Aviv", x: 56, y: 36, users: 420, latency: 26, status: "online" },
|
||||||
|
];
|
||||||
export function ServerMap() {
|
|
||||||
const [hoveredServer, setHoveredServer] = useState<ServerLocation | null>(null);
|
const networkLinks: Array<[string, string]> = [
|
||||||
const { ref, isVisible } = useScrollAnimation();
|
["us-west", "us-east"],
|
||||||
const { t } = useI18n();
|
["us-east", "canada"],
|
||||||
|
["us-east", "uk"],
|
||||||
const totalUsers = servers.reduce((sum, s) => sum + s.users, 0);
|
["us-east", "brazil"],
|
||||||
const avgLatency = Math.round(servers.reduce((sum, s) => sum + s.latency, 0) / servers.length);
|
["brazil", "argentina"],
|
||||||
|
["uk", "france"],
|
||||||
return (
|
["france", "spain"],
|
||||||
<section id="technology" className="relative px-4 py-24 sm:py-32 overflow-hidden">
|
["france", "germany"],
|
||||||
<div className="mx-auto max-w-7xl" ref={ref}>
|
["germany", "netherlands"],
|
||||||
<div className="mb-16 text-center">
|
["germany", "poland"],
|
||||||
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
["germany", "italy"],
|
||||||
{t("tech_title_1")} <span className="gradient-text">{t("tech_title_2")}</span>
|
["poland", "sweden"],
|
||||||
</h2>
|
["italy", "turkey"],
|
||||||
<p className={`mx-auto max-w-2xl text-lg text-muted-foreground ${isVisible ? "animate-fade-up-delay-1" : "opacity-0"}`}>
|
["turkey", "israel"],
|
||||||
{t("tech_subtitle")}
|
["turkey", "uae"],
|
||||||
</p>
|
["uae", "india"],
|
||||||
</div>
|
["india", "thailand"],
|
||||||
|
["thailand", "vietnam"],
|
||||||
{/* Stats */}
|
["vietnam", "hong-kong"],
|
||||||
<div className={`mb-12 grid gap-6 sm:grid-cols-3 ${isVisible ? "animate-fade-up-delay-2" : "opacity-0"}`}>
|
["hong-kong", "singapore"],
|
||||||
<div className="glass-card rounded-xl p-6 text-center">
|
["hong-kong", "south-korea"],
|
||||||
<Server className="mx-auto mb-3 text-gekon-green" size={32} />
|
["south-korea", "japan"],
|
||||||
<div className="text-3xl font-bold gradient-text">{servers.length}+</div>
|
["vietnam", "australia"],
|
||||||
<div className="text-sm text-muted-foreground">Global Servers</div>
|
["australia", "new-zealand"],
|
||||||
</div>
|
["uae", "south-africa"],
|
||||||
<div className="glass-card rounded-xl p-6 text-center">
|
];
|
||||||
<Users className="mx-auto mb-3 text-gekon-cyan" size={32} />
|
|
||||||
<div className="text-3xl font-bold gradient-text">{totalUsers}+</div>
|
export function ServerMap() {
|
||||||
<div className="text-sm text-muted-foreground">Active Users</div>
|
const [hoveredServer, setHoveredServer] = useState<ServerLocation | null>(null);
|
||||||
</div>
|
const { ref, isVisible } = useScrollAnimation();
|
||||||
<div className="glass-card rounded-xl p-6 text-center">
|
const { t } = useI18n();
|
||||||
<Zap className="mx-auto mb-3 text-gekon-green" size={32} />
|
|
||||||
<div className="text-3xl font-bold gradient-text">{avgLatency}ms</div>
|
const totalUsers = servers.reduce((sum, s) => sum + s.users, 0);
|
||||||
<div className="text-sm text-muted-foreground">Avg Latency</div>
|
const avgLatency = Math.round(servers.reduce((sum, s) => sum + s.latency, 0) / servers.length);
|
||||||
</div>
|
const serverById = new Map(servers.map((server) => [server.id, server]));
|
||||||
</div>
|
|
||||||
|
return (
|
||||||
{/* Interactive Map */}
|
<section id="technology" className="relative overflow-hidden px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||||
<div className={`glass-card relative overflow-hidden rounded-2xl p-8 ${isVisible ? "animate-fade-up-delay-3" : "opacity-0"}`}>
|
<div className="mx-auto max-w-7xl" ref={ref}>
|
||||||
{/* World Map SVG Background */}
|
<div className="mb-10 text-center">
|
||||||
<div className="relative aspect-[2/1] w-full">
|
<h2 className={`mb-4 text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||||
{/* Simplified world map outline */}
|
{t("tech_title_1")} <span className="gradient-text">{t("tech_title_2")}</span>
|
||||||
<svg
|
</h2>
|
||||||
viewBox="0 0 1000 500"
|
<p className={`mx-auto max-w-2xl text-lg text-muted-foreground ${isVisible ? "animate-fade-up-delay-1" : "opacity-0"}`}>
|
||||||
className="absolute inset-0 h-full w-full opacity-10"
|
{t("tech_subtitle")}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</p>
|
||||||
>
|
</div>
|
||||||
{/* Continents outlines (simplified) */}
|
|
||||||
<path
|
{/* Stats */}
|
||||||
d="M 150 150 Q 200 120 250 150 L 280 180 L 250 220 L 200 200 Z"
|
<div className={`mb-10 grid gap-4 sm:grid-cols-3 ${isVisible ? "animate-fade-up-delay-2" : "opacity-0"}`}>
|
||||||
fill="currentColor"
|
<div className="glass-card rounded-xl p-4 text-center">
|
||||||
className="text-gekon-green"
|
<Server className="mx-auto mb-2 text-gekon-green" size={26} />
|
||||||
/>
|
<div className="text-2xl font-bold gradient-text">{servers.length}+</div>
|
||||||
<path
|
<div className="text-xs text-muted-foreground">Global Servers</div>
|
||||||
d="M 450 120 Q 550 100 650 140 L 680 180 L 650 220 L 550 200 L 450 180 Z"
|
</div>
|
||||||
fill="currentColor"
|
<div className="glass-card rounded-xl p-4 text-center">
|
||||||
className="text-gekon-cyan"
|
<Users className="mx-auto mb-2 text-gekon-cyan" size={26} />
|
||||||
/>
|
<div className="text-2xl font-bold gradient-text">{totalUsers}+</div>
|
||||||
<path
|
<div className="text-xs text-muted-foreground">Active Users</div>
|
||||||
d="M 700 150 Q 800 130 900 180 L 920 250 L 880 300 L 750 280 L 700 220 Z"
|
</div>
|
||||||
fill="currentColor"
|
<div className="glass-card rounded-xl p-4 text-center">
|
||||||
className="text-gekon-green"
|
<Zap className="mx-auto mb-2 text-gekon-green" size={26} />
|
||||||
/>
|
<div className="text-2xl font-bold gradient-text">{avgLatency}ms</div>
|
||||||
</svg>
|
<div className="text-xs text-muted-foreground">Avg Latency</div>
|
||||||
|
</div>
|
||||||
{/* Connection lines */}
|
</div>
|
||||||
<svg className="absolute inset-0 h-full w-full pointer-events-none" viewBox="0 0 100 100" preserveAspectRatio="none">
|
|
||||||
{servers.map((server, i) => {
|
{/* Interactive Map */}
|
||||||
if (i === 0) return null;
|
<div className={`relative ${isVisible ? "animate-fade-up-delay-3" : "opacity-0"}`}>
|
||||||
const prev = servers[i - 1];
|
{/* World Map SVG Background */}
|
||||||
return (
|
<div className="relative aspect-[2/1] w-full overflow-hidden">
|
||||||
<line
|
<div
|
||||||
key={`line-${server.id}`}
|
className="absolute inset-0"
|
||||||
x1={prev.x}
|
style={{
|
||||||
y1={prev.y}
|
WebkitMaskImage:
|
||||||
x2={server.x}
|
"radial-gradient(120% 85% at 50% 50%, black 62%, rgba(0,0,0,0.72) 78%, transparent 100%)",
|
||||||
y2={server.y}
|
maskImage:
|
||||||
stroke="url(#gradient)"
|
"radial-gradient(120% 85% at 50% 50%, black 62%, rgba(0,0,0,0.72) 78%, transparent 100%)",
|
||||||
strokeWidth="0.1"
|
}}
|
||||||
opacity="0.2"
|
>
|
||||||
className="animate-pulse"
|
<div className="absolute inset-0 bg-[#020617]" />
|
||||||
style={{ animationDelay: `${i * 0.1}s` }}
|
<img
|
||||||
/>
|
src={worldMapOutline}
|
||||||
);
|
alt="World contour map"
|
||||||
})}
|
className="h-full w-full object-contain opacity-82"
|
||||||
<defs>
|
draggable={false}
|
||||||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
/>
|
||||||
<stop offset="0%" stopColor="rgb(16, 185, 129)" />
|
<div className="absolute inset-0 bg-gradient-to-b from-background/28 via-transparent to-background/34" />
|
||||||
<stop offset="100%" stopColor="rgb(6, 182, 212)" />
|
</div>
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
{/* Connection lines */}
|
||||||
</svg>
|
<svg className="absolute inset-0 h-full w-full pointer-events-none" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||||
|
{networkLinks.map(([fromId, toId], i) => {
|
||||||
{/* Server nodes */}
|
const from = serverById.get(fromId);
|
||||||
{servers.map((server, i) => (
|
const to = serverById.get(toId);
|
||||||
<div
|
if (!from || !to) return null;
|
||||||
key={server.id}
|
return (
|
||||||
className="absolute -translate-x-1/2 -translate-y-1/2 cursor-pointer transition-transform hover:scale-150"
|
<line
|
||||||
style={{
|
key={`line-${fromId}-${toId}`}
|
||||||
left: `${server.x}%`,
|
x1={from.x}
|
||||||
top: `${server.y}%`,
|
y1={from.y}
|
||||||
animationDelay: `${i * 0.05}s`,
|
x2={to.x}
|
||||||
}}
|
y2={to.y}
|
||||||
onMouseEnter={() => setHoveredServer(server)}
|
stroke="url(#gradient)"
|
||||||
onMouseLeave={() => setHoveredServer(null)}
|
strokeWidth="0.14"
|
||||||
>
|
opacity="0.3"
|
||||||
{/* Pulse ring */}
|
className="animate-pulse"
|
||||||
<div className="absolute inset-0 -m-2 animate-ping rounded-full bg-gekon-green/30" />
|
style={{ animationDelay: `${i * 0.1}s` }}
|
||||||
|
/>
|
||||||
{/* Node dot */}
|
);
|
||||||
<div
|
})}
|
||||||
className={`relative h-3 w-3 rounded-full transition-all ${
|
<defs>
|
||||||
server.status === "online"
|
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
? "bg-gekon-green shadow-lg shadow-gekon-green/50"
|
<stop offset="0%" stopColor="rgb(16, 185, 129)" />
|
||||||
: "bg-yellow-500 shadow-lg shadow-yellow-500/50"
|
<stop offset="100%" stopColor="rgb(6, 182, 212)" />
|
||||||
}`}
|
</linearGradient>
|
||||||
/>
|
</defs>
|
||||||
|
</svg>
|
||||||
{/* Tooltip */}
|
|
||||||
{hoveredServer?.id === server.id && (
|
{/* Server nodes */}
|
||||||
<div className="absolute left-1/2 top-full z-10 mt-2 -translate-x-1/2 whitespace-nowrap rounded-lg bg-background/95 px-3 py-2 text-xs shadow-xl backdrop-blur-sm border border-gekon-green/20">
|
{servers.map((server, i) => (
|
||||||
<div className="font-semibold text-foreground">{server.city}, {server.country}</div>
|
<div
|
||||||
<div className="mt-1 flex items-center gap-3 text-muted-foreground">
|
key={server.id}
|
||||||
<span className="flex items-center gap-1">
|
className="absolute -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
||||||
<Users size={10} />
|
style={{
|
||||||
{server.users}
|
left: `${server.x}%`,
|
||||||
</span>
|
top: `${server.y}%`,
|
||||||
<span className="flex items-center gap-1">
|
}}
|
||||||
<Zap size={10} />
|
>
|
||||||
{server.latency}ms
|
{/* Pulse ring */}
|
||||||
</span>
|
<div className="absolute inset-0 -m-1 animate-ping rounded-full bg-gekon-green/30 pointer-events-none" />
|
||||||
</div>
|
|
||||||
</div>
|
{/* Node dot */}
|
||||||
)}
|
<button
|
||||||
</div>
|
type="button"
|
||||||
))}
|
aria-label={`${server.city}, ${server.country}`}
|
||||||
</div>
|
className="pointer-events-auto relative block h-2.5 w-2.5 rounded-full transition-transform duration-150 hover:scale-125 focus-visible:outline-none"
|
||||||
|
onMouseEnter={() => setHoveredServer(server)}
|
||||||
{/* Legend */}
|
onMouseLeave={() => setHoveredServer(null)}
|
||||||
<div className="mt-6 flex flex-wrap items-center justify-center gap-6 text-xs text-muted-foreground">
|
>
|
||||||
<div className="flex items-center gap-2">
|
<span
|
||||||
<div className="h-2 w-2 rounded-full bg-gekon-green shadow-lg shadow-gekon-green/50" />
|
className={`absolute inset-0 rounded-full transition-all ${
|
||||||
<span>Online</span>
|
server.status === "online"
|
||||||
</div>
|
? "bg-gekon-green shadow-lg shadow-gekon-green/50"
|
||||||
<div className="flex items-center gap-2">
|
: "bg-yellow-500 shadow-lg shadow-yellow-500/50"
|
||||||
<div className="h-2 w-2 rounded-full bg-yellow-500 shadow-lg shadow-yellow-500/50" />
|
}`}
|
||||||
<span>Maintenance</span>
|
/>
|
||||||
</div>
|
</button>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-px w-8 bg-gradient-to-r from-gekon-green to-gekon-cyan" />
|
</div>
|
||||||
<span>Network Connection</span>
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
{/* Global tooltip layer (always above points) */}
|
||||||
</div>
|
{hoveredServer && (
|
||||||
|
<div
|
||||||
{/* Hover instruction */}
|
className="pointer-events-none absolute z-40 -translate-x-1/2 whitespace-nowrap rounded-lg border border-gekon-green/25 bg-background/98 px-3 py-2 text-xs shadow-2xl shadow-black/50 backdrop-blur-md"
|
||||||
<p className="mt-4 text-center text-sm text-muted-foreground">
|
style={{
|
||||||
Hover over nodes to see server details
|
left: `${hoveredServer.x}%`,
|
||||||
</p>
|
top: `calc(${hoveredServer.y}% + 14px)`,
|
||||||
</div>
|
}}
|
||||||
</section>
|
>
|
||||||
);
|
<div className="font-semibold text-foreground">
|
||||||
}
|
{hoveredServer.city}, {hoveredServer.country}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 flex items-center gap-3 text-muted-foreground">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Users size={10} />
|
||||||
|
{hoveredServer.users}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Zap size={10} />
|
||||||
|
{hoveredServer.latency}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="mt-5 flex flex-wrap items-center justify-center gap-6 text-xs text-muted-foreground/90">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-gekon-green shadow-lg shadow-gekon-green/50" />
|
||||||
|
<span>Online</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-yellow-500 shadow-lg shadow-yellow-500/50" />
|
||||||
|
<span>Maintenance</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-px w-8 bg-gradient-to-r from-gekon-green to-gekon-cyan" />
|
||||||
|
<span>Network Connection</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover instruction */}
|
||||||
|
<p className="mt-4 text-center text-sm text-muted-foreground">
|
||||||
|
Hover over nodes to see server details
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Headset, Languages, Settings } from "lucide-react";
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
title: "Отвечаем 24/7",
|
||||||
|
desc: "Мы всегда готовы прийти на помощь",
|
||||||
|
icon: Headset,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Говорим понятным языком",
|
||||||
|
desc: "Делаем сложные вещи простыми — никаких технических терминов",
|
||||||
|
icon: Languages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Помогаем настроить сервис",
|
||||||
|
desc: "От смартфонов до телевизоров — позаботимся обо всем!",
|
||||||
|
icon: Settings,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SupportHighlightsSection() {
|
||||||
|
return (
|
||||||
|
<section className="px-4 pb-0 pt-5 sm:pb-0">
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{cards.map((card) => (
|
||||||
|
<div
|
||||||
|
key={card.title}
|
||||||
|
className="group relative overflow-hidden rounded-2xl border border-gekon-green/20 bg-gradient-to-br from-background/85 via-background/70 to-background/85 p-5 shadow-[0_0_0_1px_rgba(16,185,129,0.08)] transition-all hover:-translate-y-1 hover:border-gekon-green/45 hover:shadow-[0_8px_30px_rgba(16,185,129,0.18)]"
|
||||||
|
>
|
||||||
|
<div className="pointer-events-none absolute -right-10 -top-10 h-28 w-28 rounded-full bg-gekon-green/15 blur-2xl transition-opacity group-hover:opacity-100" />
|
||||||
|
<div className="mb-3 inline-flex h-10 w-10 items-center justify-center rounded-xl border border-gekon-green/30 bg-gekon-green/10 text-gekon-green">
|
||||||
|
<card.icon size={18} />
|
||||||
|
</div>
|
||||||
|
<h3 className="mb-1.5 text-base font-semibold text-foreground">{card.title}</h3>
|
||||||
|
<p className="text-sm leading-relaxed text-muted-foreground">{card.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,9 +16,9 @@ export function UseCasesSection() {
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative px-4 py-24 sm:py-32">
|
<section className="relative px-4 pb-0 pt-5 sm:pb-0 sm:pt-5">
|
||||||
<div className="mx-auto max-w-5xl" ref={ref}>
|
<div className="mx-auto max-w-5xl" ref={ref}>
|
||||||
<h2 className={`mb-12 text-center text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
<h2 className={`mb-8 text-center text-3xl font-bold sm:text-4xl ${isVisible ? "animate-fade-up" : "opacity-0"}`}>
|
||||||
{t("cases_title_1")} <span className="gradient-text">{t("cases_title_2")}</span>
|
{t("cases_title_1")} <span className="gradient-text">{t("cases_title_2")}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -26,14 +26,23 @@ export function UseCasesSection() {
|
|||||||
{cases.map((c, i) => (
|
{cases.map((c, i) => (
|
||||||
<div
|
<div
|
||||||
key={c.titleKey}
|
key={c.titleKey}
|
||||||
className={`glass-card flex items-center gap-3 rounded-full px-6 py-3 transition-all hover:scale-105 hover:shadow-lg hover:shadow-gekon-green/5 ${
|
className={`glass-card group flex items-center gap-3 rounded-full px-6 py-3 transition-all duration-500 hover:scale-[1.03] hover:shadow-lg hover:shadow-gekon-green/10 ${
|
||||||
isVisible ? "animate-fade-up" : "opacity-0"
|
isVisible ? "animate-fade-up" : "opacity-0"
|
||||||
}`}
|
}`}
|
||||||
style={{ animationDelay: `${i * 0.08}s` }}
|
style={{
|
||||||
|
animationName: isVisible ? "fade-up, float" : undefined,
|
||||||
|
animationDuration: isVisible ? "0.6s, 5.4s" : undefined,
|
||||||
|
animationTimingFunction: isVisible ? "ease-out, ease-in-out" : undefined,
|
||||||
|
animationIterationCount: isVisible ? "1, infinite" : undefined,
|
||||||
|
animationFillMode: isVisible ? "both, both" : undefined,
|
||||||
|
animationDelay: isVisible ? `${i * 0.08}s, ${0.2 + i * 0.16}s` : `${i * 0.08}s`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<c.icon size={20} className="text-gekon-green" />
|
<div className="rounded-full bg-gekon-green/10 p-1.5 transition-all duration-300 group-hover:bg-gekon-green/20 group-hover:shadow-[0_0_18px_rgba(16,185,129,0.25)]">
|
||||||
|
<c.icon size={19} className="text-gekon-green transition-transform duration-300 group-hover:scale-110" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold text-foreground">{t(c.titleKey)}</span>
|
<span className="font-semibold text-foreground transition-colors group-hover:text-gekon-green">{t(c.titleKey)}</span>
|
||||||
<span className="ml-2 text-sm text-muted-foreground">{t(c.descKey)}</span>
|
<span className="ml-2 text-sm text-muted-foreground">{t(c.descKey)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -179,8 +179,8 @@ const ru: typeof en = {
|
|||||||
hero_title_1: "Ускорь свой интернет.",
|
hero_title_1: "Ускорь свой интернет.",
|
||||||
hero_title_2: "Раскрой настоящую скорость.",
|
hero_title_2: "Раскрой настоящую скорость.",
|
||||||
hero_subtitle: "Передовая технология оптимизации сети. Используйте интернет на полную мощность благодаря интеллектуальной маршрутизации и оптимизации трафика.",
|
hero_subtitle: "Передовая технология оптимизации сети. Используйте интернет на полную мощность благодаря интеллектуальной маршрутизации и оптимизации трафика.",
|
||||||
hero_cta_primary: "Попробовать бесплатно",
|
hero_cta_primary: "Оформить подписку",
|
||||||
hero_cta_secondary: "Смотреть результаты",
|
hero_cta_secondary: "Попробовать бесплатно",
|
||||||
hero_speed_label: "Скорость соединения",
|
hero_speed_label: "Скорость соединения",
|
||||||
hero_speed_optimized: "Оптимизировано",
|
hero_speed_optimized: "Оптимизировано",
|
||||||
hero_speed_download: "Загрузка",
|
hero_speed_download: "Загрузка",
|
||||||
@@ -297,7 +297,7 @@ const ru: typeof en = {
|
|||||||
cta_title_2: "настоящую скорость интернета?",
|
cta_title_2: "настоящую скорость интернета?",
|
||||||
cta_subtitle: "Присоединяйтесь к 10 000+ пользователям, которые ускорили свои соединения.",
|
cta_subtitle: "Присоединяйтесь к 10 000+ пользователям, которые ускорили свои соединения.",
|
||||||
cta_button: "Попробовать бесплатно",
|
cta_button: "Попробовать бесплатно",
|
||||||
cta_note: "Без кредитной карты · 7-дневный пробный период",
|
cta_note: "Бесплатный тест 3 дня, за суточный репост - 24 часа сервиса в подарок!",
|
||||||
|
|
||||||
footer_desc: "Передовая технология оптимизации сети для более быстрого и надёжного интернета.",
|
footer_desc: "Передовая технология оптимизации сети для более быстрого и надёжного интернета.",
|
||||||
footer_product: "Продукт",
|
footer_product: "Продукт",
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ export const Route = createRootRoute({
|
|||||||
rel: "stylesheet",
|
rel: "stylesheet",
|
||||||
href: appCss,
|
href: appCss,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rel: "icon",
|
||||||
|
type: "image/png",
|
||||||
|
href: "/gekon-logo.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: "apple-touch-icon",
|
||||||
|
href: "/gekon-logo.png",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
shellComponent: RootShell,
|
shellComponent: RootShell,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { HeroSection } from "@/components/HeroSection";
|
|||||||
import { SocialProofBar } from "@/components/SocialProofBar";
|
import { SocialProofBar } from "@/components/SocialProofBar";
|
||||||
import { FeaturesSection } from "@/components/FeaturesSection";
|
import { FeaturesSection } from "@/components/FeaturesSection";
|
||||||
import { ServerMap } from "@/components/ServerMap";
|
import { ServerMap } from "@/components/ServerMap";
|
||||||
|
import { SupportHighlightsSection } from "@/components/SupportHighlightsSection";
|
||||||
import { HowItWorksSection } from "@/components/HowItWorksSection";
|
import { HowItWorksSection } from "@/components/HowItWorksSection";
|
||||||
import { PricingSection } from "@/components/PricingSection";
|
import { PricingSection } from "@/components/PricingSection";
|
||||||
import { PerformanceSection } from "@/components/PerformanceSection";
|
import { PerformanceSection } from "@/components/PerformanceSection";
|
||||||
@@ -42,8 +43,9 @@ function Index() {
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<SocialProofBar />
|
<SocialProofBar />
|
||||||
<FeaturesSection />
|
|
||||||
<ServerMap />
|
<ServerMap />
|
||||||
|
<SupportHighlightsSection />
|
||||||
|
<FeaturesSection />
|
||||||
<HowItWorksSection />
|
<HowItWorksSection />
|
||||||
<PerformanceSection />
|
<PerformanceSection />
|
||||||
<UseCasesSection />
|
<UseCasesSection />
|
||||||
|
|||||||
@@ -150,6 +150,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
@keyframes tag-cloud-path-a {
|
||||||
|
0% { transform: translate3d(0, 0, 0); }
|
||||||
|
20% { transform: translate3d(8px, -10px, 0); }
|
||||||
|
45% { transform: translate3d(16px, 2px, 0); }
|
||||||
|
70% { transform: translate3d(6px, 12px, 0); }
|
||||||
|
100% { transform: translate3d(0, 0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tag-cloud-path-b {
|
||||||
|
0% { transform: translate3d(0, 0, 0); }
|
||||||
|
25% { transform: translate3d(-10px, -6px, 0); }
|
||||||
|
50% { transform: translate3d(-16px, 8px, 0); }
|
||||||
|
75% { transform: translate3d(-4px, 14px, 0); }
|
||||||
|
100% { transform: translate3d(0, 0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tag-cloud-path-c {
|
||||||
|
0% { transform: translate3d(0, 0, 0); }
|
||||||
|
25% { transform: translate3d(12px, 6px, 0); }
|
||||||
|
50% { transform: translate3d(4px, -14px, 0); }
|
||||||
|
75% { transform: translate3d(-10px, -8px, 0); }
|
||||||
|
100% { transform: translate3d(0, 0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tag-cloud-path-d {
|
||||||
|
0% { transform: translate3d(0, 0, 0); }
|
||||||
|
20% { transform: translate3d(-6px, 10px, 0); }
|
||||||
|
45% { transform: translate3d(10px, 16px, 0); }
|
||||||
|
70% { transform: translate3d(14px, -4px, 0); }
|
||||||
|
100% { transform: translate3d(0, 0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud-motion-a { animation: tag-cloud-path-a 11s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
||||||
|
.tag-cloud-motion-b { animation: tag-cloud-path-b 13s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
||||||
|
.tag-cloud-motion-c { animation: tag-cloud-path-c 12s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
||||||
|
.tag-cloud-motion-d { animation: tag-cloud-path-d 14s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
|
||||||
|
|
||||||
|
.tag-cloud-item {
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, filter 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
will-change: transform, filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud-item:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-cloud-item:focus-visible {
|
||||||
|
outline: 2px solid var(--gekon-green);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.glass-card {
|
.glass-card {
|
||||||
background: var(--glass);
|
background: var(--glass);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.2 MiB |
+9
-9
@@ -1,9 +1,9 @@
|
|||||||
// @lovable.dev/vite-tanstack-config already includes the following — do NOT add them manually
|
// @lovable.dev/vite-tanstack-config already includes the following — do NOT add them manually
|
||||||
// or the app will break with duplicate plugins:
|
// or the app will break with duplicate plugins:
|
||||||
// - tanstackStart, viteReact, tailwindcss, tsConfigPaths, cloudflare (build-only),
|
// - tanstackStart, viteReact, tailwindcss, tsConfigPaths, cloudflare (build-only),
|
||||||
// componentTagger (dev-only), VITE_* env injection, @ path alias, React/TanStack dedupe,
|
// componentTagger (dev-only), VITE_* env injection, @ path alias, React/TanStack dedupe,
|
||||||
// error logger plugins, and sandbox detection (port/host/strictPort).
|
// error logger plugins, and sandbox detection (port/host/strictPort).
|
||||||
// You can pass additional config via defineConfig({ vite: { ... } }) if needed.
|
// You can pass additional config via defineConfig({ vite: { ... } }) if needed.
|
||||||
import { defineConfig } from "@lovable.dev/vite-tanstack-config";
|
import { defineConfig } from "@lovable.dev/vite-tanstack-config";
|
||||||
|
|
||||||
export default defineConfig();
|
export default defineConfig();
|
||||||
|
|||||||
Reference in New Issue
Block a user