Initial commit: HotelSync PMS v0.1.0

- React 18 + TypeScript + Vite + Tailwind CSS
- Шахматка бронирований (drag-to-book)
- Страницы: Calendar, Bookings, Rooms, Housekeeping, Channels, API Docs, Settings
- Роли: super_admin, hotel_manager, housekeeper
- Светлая/тёмная тема
- Docker + Nginx конфигурация
- Лендинг hotelsync.ru
This commit is contained in:
2026-03-10 20:38:32 +03:00
commit 420d55d57e
45 changed files with 7274 additions and 0 deletions

449
deploy/landing/index.html Normal file
View File

@@ -0,0 +1,449 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="HotelSync — современная SaaS PMS система для управления отелем. Шахматка бронирований, менеджер каналов, уборка." />
<title>HotelSync — PMS система для отелей</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--brand: #4f46e5;
--brand-light: #6366f1;
--brand-dark: #3730a3;
--text: #0f172a;
--text-muted: #64748b;
--bg: #f8fafc;
--white: #ffffff;
--border: #e2e8f0;
}
html { scroll-behavior: smooth; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
/* ── Nav ─────────────────────────────────────── */
nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 0 1.5rem;
height: 64px;
display: flex; align-items: center; justify-content: space-between;
}
.logo {
display: flex; align-items: center; gap: 10px;
font-size: 1.25rem; font-weight: 700; color: var(--text); text-decoration: none;
}
.logo-icon {
width: 36px; height: 36px; border-radius: 10px;
background: var(--brand); color: white;
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem; font-weight: 800;
}
.logo span { color: var(--brand); }
.nav-cta {
display: inline-flex; align-items: center; gap: 8px;
background: var(--brand); color: white;
padding: 0.5rem 1.25rem; border-radius: 8px;
font-size: 0.875rem; font-weight: 600; text-decoration: none;
transition: background 0.15s, transform 0.15s;
}
.nav-cta:hover { background: var(--brand-dark); transform: translateY(-1px); }
/* ── Hero ────────────────────────────────────── */
.hero {
min-height: 100vh;
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 6rem 1.5rem 4rem;
text-align: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute; inset: 0;
background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(99,102,241,0.12) 0%, transparent 70%);
pointer-events: none;
}
.badge {
display: inline-flex; align-items: center; gap: 6px;
background: rgba(99,102,241,0.1); color: var(--brand);
border: 1px solid rgba(99,102,241,0.25);
padding: 0.35rem 0.9rem; border-radius: 999px;
font-size: 0.8rem; font-weight: 600; margin-bottom: 1.5rem;
}
.badge-dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--brand); animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
}
h1 {
font-size: clamp(2.5rem, 7vw, 4.5rem);
font-weight: 800; line-height: 1.1;
letter-spacing: -0.03em;
max-width: 900px;
margin-bottom: 1.25rem;
}
h1 em { font-style: normal; color: var(--brand); }
.hero-sub {
font-size: clamp(1rem, 2.5vw, 1.25rem);
color: var(--text-muted); line-height: 1.7;
max-width: 600px; margin-bottom: 2.5rem;
}
.hero-actions {
display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; justify-content: center;
margin-bottom: 4rem;
}
.btn-primary {
display: inline-flex; align-items: center; gap: 8px;
background: var(--brand); color: white;
padding: 0.875rem 2rem; border-radius: 12px;
font-size: 1rem; font-weight: 600; text-decoration: none;
transition: all 0.15s; box-shadow: 0 4px 16px rgba(99,102,241,0.35);
}
.btn-primary:hover {
background: var(--brand-dark); transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(99,102,241,0.4);
}
.btn-secondary {
display: inline-flex; align-items: center; gap: 8px;
background: white; color: var(--text);
border: 1px solid var(--border);
padding: 0.875rem 2rem; border-radius: 12px;
font-size: 1rem; font-weight: 600; text-decoration: none;
transition: all 0.15s;
}
.btn-secondary:hover { background: var(--bg); transform: translateY(-2px); }
/* ── App preview card ─────────────────────────── */
.preview-card {
width: min(860px, 90vw);
background: white; border-radius: 20px;
box-shadow: 0 32px 80px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.05);
overflow: hidden; position: relative;
}
.preview-topbar {
background: white; border-bottom: 1px solid var(--border);
padding: 0.75rem 1rem;
display: flex; align-items: center; gap: 8px;
}
.preview-dot { width: 10px; height: 10px; border-radius: 50%; }
.preview-inner {
display: grid; grid-template-columns: 180px 1fr;
height: 320px;
}
.preview-sidebar {
background: #f8fafc; border-right: 1px solid var(--border);
padding: 1rem 0.75rem;
}
.preview-sidebar-item {
display: flex; align-items: center; gap: 8px;
padding: 0.5rem 0.75rem; border-radius: 8px;
font-size: 0.75rem; font-weight: 500; color: #64748b;
margin-bottom: 2px;
}
.preview-sidebar-item.active {
background: rgba(99,102,241,0.1); color: var(--brand);
}
.preview-sidebar-icon {
width: 16px; height: 16px; border-radius: 4px;
background: currentColor; opacity: 0.4;
}
.preview-content { padding: 1rem; overflow: hidden; }
.preview-row {
display: flex; gap: 6px; margin-bottom: 6px; align-items: center;
}
.preview-label {
width: 60px; font-size: 0.65rem; font-weight: 600;
color: #94a3b8; shrink: 0;
}
.preview-block {
height: 28px; border-radius: 6px;
display: flex; align-items: center; padding: 0 8px;
font-size: 0.6rem; font-weight: 600; color: white; white-space: nowrap; overflow: hidden;
}
/* ── Features ────────────────────────────────── */
section { padding: 5rem 1.5rem; }
.section-title {
text-align: center; margin-bottom: 3rem;
}
.section-title h2 {
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-weight: 800; margin-bottom: 0.75rem; letter-spacing: -0.02em;
}
.section-title p { color: var(--text-muted); font-size: 1.1rem; }
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem; max-width: 1100px; margin: 0 auto;
}
.feature-card {
background: white; border: 1px solid var(--border);
border-radius: 16px; padding: 1.75rem;
transition: all 0.2s;
}
.feature-card:hover {
box-shadow: 0 8px 30px rgba(0,0,0,0.08);
transform: translateY(-4px);
}
.feature-icon {
width: 48px; height: 48px; border-radius: 12px;
background: rgba(99,102,241,0.1);
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; margin-bottom: 1rem;
}
.feature-card h3 {
font-size: 1rem; font-weight: 700; margin-bottom: 0.5rem;
}
.feature-card p { color: var(--text-muted); font-size: 0.9rem; line-height: 1.6; }
/* ── Coming soon ─────────────────────────────── */
.coming-soon {
background: linear-gradient(135deg, var(--brand) 0%, #7c3aed 100%);
border-radius: 24px; max-width: 800px; margin: 0 auto;
padding: 4rem 2rem; text-align: center; color: white;
}
.coming-soon h2 { font-size: 2rem; font-weight: 800; margin-bottom: 0.75rem; }
.coming-soon p { opacity: 0.85; font-size: 1rem; margin-bottom: 2rem; }
.notify-form {
display: flex; gap: 8px; max-width: 420px; margin: 0 auto; flex-wrap: wrap; justify-content: center;
}
.notify-form input {
flex: 1; min-width: 220px;
padding: 0.75rem 1rem; border-radius: 10px; border: none;
font-size: 0.9rem; font-family: inherit;
background: rgba(255,255,255,0.15); color: white;
}
.notify-form input::placeholder { color: rgba(255,255,255,0.6); }
.notify-form input:focus { outline: 2px solid rgba(255,255,255,0.5); }
.notify-form button {
padding: 0.75rem 1.5rem; border-radius: 10px; border: none;
background: white; color: var(--brand);
font-size: 0.9rem; font-weight: 700; cursor: pointer;
font-family: inherit; transition: all 0.15s;
}
.notify-form button:hover { background: #f1f5f9; transform: scale(1.02); }
/* ── Footer ──────────────────────────────────── */
footer {
text-align: center; padding: 2rem 1.5rem;
border-top: 1px solid var(--border);
color: var(--text-muted); font-size: 0.85rem;
}
@media (max-width: 640px) {
.preview-inner { grid-template-columns: 1fr; height: auto; }
.preview-sidebar { display: none; }
.hero-actions { flex-direction: column; }
}
</style>
</head>
<body>
<!-- Nav -->
<nav>
<a href="/" class="logo">
<div class="logo-icon">H</div>
Hotel<span>Sync</span>
</a>
<a href="https://app.hotelsync.ru" class="nav-cta">
Войти в систему →
</a>
</nav>
<!-- Hero -->
<section class="hero">
<div class="badge">
<div class="badge-dot"></div>
Скоро запуск · Уже в разработке
</div>
<h1>
Управляйте отелем<br/>
как <em>профессионал</em>
</h1>
<p class="hero-sub">
HotelSync — современная PMS система с шахматкой бронирований,
синхронизацией с Booking.com и Airbnb, управлением уборкой
и полным API для интеграций.
</p>
<div class="hero-actions">
<a href="https://app.hotelsync.ru" class="btn-primary">
✦ Открыть систему
</a>
<a href="#features" class="btn-secondary">
Узнать больше
</a>
</div>
<!-- App preview -->
<div class="preview-card">
<div class="preview-topbar">
<div class="preview-dot" style="background:#ff5f57"></div>
<div class="preview-dot" style="background:#febc2e"></div>
<div class="preview-dot" style="background:#28c840"></div>
<div style="flex:1;height:22px;background:#f1f5f9;border-radius:6px;margin-left:8px"></div>
</div>
<div class="preview-inner">
<div class="preview-sidebar">
<div style="display:flex;align-items:center;gap:8px;padding:0.5rem 0.75rem;margin-bottom:12px">
<div style="width:24px;height:24px;border-radius:6px;background:#4f46e5"></div>
<span style="font-size:0.75rem;font-weight:700;color:#0f172a">HotelSync</span>
</div>
<div class="preview-sidebar-item active">
<div class="preview-sidebar-icon"></div> Шахматка
</div>
<div class="preview-sidebar-item">
<div class="preview-sidebar-icon"></div> Бронирования
</div>
<div class="preview-sidebar-item">
<div class="preview-sidebar-icon"></div> Номера
</div>
<div class="preview-sidebar-item">
<div class="preview-sidebar-icon"></div> Уборка
</div>
<div class="preview-sidebar-item">
<div class="preview-sidebar-icon"></div> Каналы
</div>
</div>
<div class="preview-content">
<div style="display:flex;gap:6px;margin-bottom:12px">
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
<div style="height:6px;flex:1;background:#e2e8f0;border-radius:3px"></div>
</div>
<div class="preview-row">
<div class="preview-label">101</div>
<div class="preview-block" style="background:#4f46e5;width:22%">Волков Д.</div>
<div style="flex:1"></div>
</div>
<div class="preview-row">
<div class="preview-label">102</div>
<div style="flex:0.3"></div>
<div class="preview-block" style="background:#4f46e5;width:28%">Козлова А.</div>
<div style="flex:1"></div>
</div>
<div class="preview-row">
<div class="preview-label">201 ★</div>
<div class="preview-block" style="background:#10b981;width:18%">Соколов</div>
<div style="flex:0.2"></div>
<div class="preview-block" style="background:#4f46e5;width:25%">Белова Т.</div>
</div>
<div class="preview-row">
<div class="preview-label">301</div>
<div class="preview-block" style="background:#10b981;width:35%">Александрова Н.</div>
</div>
<div class="preview-row">
<div class="preview-label">401</div>
<div style="flex:0.4"></div>
<div class="preview-block" style="background:#4f46e5;width:30%">Орлов М.</div>
</div>
<div class="preview-row">
<div class="preview-label">Пентхаус</div>
<div style="flex:0.5"></div>
<div class="preview-block" style="background:#f59e0b;width:38%">Громов В.</div>
</div>
<div class="preview-row">
<div class="preview-label">102A</div>
<div class="preview-block" style="background:#10b981;width:42%">Лебедева Е.</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features -->
<section id="features" style="background:white">
<div class="section-title">
<h2>Всё что нужно отелю</h2>
<p>Один инструмент вместо десяти</p>
</div>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">📅</div>
<h3>Шахматка бронирований</h3>
<p>Наглядный календарь с номерами и датами. Перетаскивайте блоки, создавайте брони в один клик.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🌐</div>
<h3>Менеджер каналов</h3>
<p>Автоматическая синхронизация с Booking.com, Airbnb, Expedia и другими OTA-платформами.</p>
</div>
<div class="feature-card">
<div class="feature-icon"></div>
<h3>Управление уборкой</h3>
<p>Канбан-доска для горничных. Задачи, статусы, приоритеты — всё в одном месте.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<h3>Аналитика и отчёты</h3>
<p>Загрузка отеля, RevPAR, ADR и другие ключевые метрики в реальном времени.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔌</div>
<h3>REST API</h3>
<p>Полное API для интеграций с вашими системами. Webhooks, документация, SDK.</p>
</div>
<div class="feature-card">
<div class="feature-icon">🏨</div>
<h3>Мультиотельность</h3>
<p>Управляйте сетью отелей из одного аккаунта. Разные роли для каждого объекта.</p>
</div>
</div>
</section>
<!-- Coming soon / notify -->
<section>
<div class="coming-soon">
<h2>🚀 Скоро полноценный запуск</h2>
<p>Оставьте email — сообщим первыми о старте и дадим скидку 30% на первый год</p>
<form class="notify-form" onsubmit="handleSubmit(event)">
<input type="email" placeholder="ваш@email.ru" required id="emailInput" />
<button type="submit">Уведомить меня</button>
</form>
<p id="formSuccess" style="display:none;margin-top:1rem;font-weight:600;">✓ Отлично! Мы свяжемся с вами при запуске.</p>
</div>
</section>
<!-- Footer -->
<footer>
<p>© 2026 HotelSync · SaaS PMS Platform · <a href="https://app.hotelsync.ru" style="color:var(--brand)">Войти в систему</a></p>
</footer>
<script>
function handleSubmit(e) {
e.preventDefault();
document.getElementById('emailInput').style.display = 'none';
e.target.querySelector('button').style.display = 'none';
document.getElementById('formSuccess').style.display = 'block';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,67 @@
# ── Редирект HTTP → HTTPS ─────────────────────────────────────────────────
server {
listen 80;
server_name hotelsync.ru www.hotelsync.ru app.hotelsync.ru api.hotelsync.ru;
# Certbot challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# ── app.hotelsync.ru — Панель управления (React SPA) ─────────────────────
server {
listen 443 ssl;
server_name app.hotelsync.ru;
ssl_certificate /etc/letsencrypt/live/app.hotelsync.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.hotelsync.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# ── hotelsync.ru — Лендинг (редирект на app пока нет лендинга) ───────────
server {
listen 443 ssl;
server_name hotelsync.ru www.hotelsync.ru;
ssl_certificate /etc/letsencrypt/live/hotelsync.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hotelsync.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
return 302 https://app.hotelsync.ru;
}
}
# ── api.hotelsync.ru — API Backend ────────────────────────────────────────
server {
listen 443 ssl;
server_name api.hotelsync.ru;
ssl_certificate /etc/letsencrypt/live/api.hotelsync.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.hotelsync.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://api:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

32
deploy/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,32 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
client_max_body_size 20M;
# Логи
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log main;
# Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml text/javascript image/svg+xml;
include /etc/nginx/conf.d/*.conf;
}

86
deploy/setup.sh Normal file
View File

@@ -0,0 +1,86 @@
#!/bin/bash
# HotelSync — первоначальная настройка сервера
set -e
echo "🚀 HotelSync Server Setup"
echo "========================="
# ── 1. Обновление системы ─────────────────────────────────────────────────
echo "→ Обновление пакетов..."
apt-get update -qq && apt-get upgrade -y -qq
# ── 2. Установка Docker ───────────────────────────────────────────────────
echo "→ Установка Docker..."
curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker
# Docker Compose plugin
apt-get install -y docker-compose-plugin
echo "Docker версия: $(docker --version)"
echo "Docker Compose: $(docker compose version)"
# ── 3. Установка Git ──────────────────────────────────────────────────────
echo "→ Установка Git..."
apt-get install -y git
# ── 4. Настройка проекта ──────────────────────────────────────────────────
echo "→ Создание директории проекта..."
mkdir -p /opt/hotelsync
cd /opt/hotelsync
# ── 5. Настройка Gitea (self-hosted Git) ──────────────────────────────────
echo "→ Настройка Gitea..."
mkdir -p /opt/gitea/data
cat > /opt/gitea/docker-compose.yml << 'EOF'
version: '3'
services:
gitea:
image: gitea/gitea:latest
container_name: hotelsync-gitea
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=sqlite3
- GITEA__server__DOMAIN=git.hotelsync.ru
- GITEA__server__ROOT_URL=https://git.hotelsync.ru
- GITEA__server__HTTP_PORT=3000
ports:
- "3001:3000"
- "222:22"
volumes:
- /opt/gitea/data:/data
EOF
docker compose -f /opt/gitea/docker-compose.yml up -d
echo "✓ Gitea запущен на порту 3001"
# ── 6. Настройка bare git репозитория для деплоя ──────────────────────────
echo "→ Настройка git deploy hook..."
mkdir -p /opt/hotelsync/repo.git
git init --bare /opt/hotelsync/repo.git
cat > /opt/hotelsync/repo.git/hooks/post-receive << 'HOOK'
#!/bin/bash
echo "→ Получен push, запускаем деплой..."
cd /opt/hotelsync/app
git --work-tree=/opt/hotelsync/app --git-dir=/opt/hotelsync/repo.git checkout -f main
docker compose build frontend
docker compose up -d --no-deps frontend
echo "✓ Деплой завершён!"
HOOK
chmod +x /opt/hotelsync/repo.git/hooks/post-receive
mkdir -p /opt/hotelsync/app
git init /opt/hotelsync/app
echo "✓ Git deploy hook настроен"
echo ""
echo "=============================="
echo "✅ Базовая настройка завершена"
echo "=============================="
echo ""
echo "Следующий шаг: запустить deploy/ssl.sh для SSL сертификатов"

31
deploy/ssl.sh Normal file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Получение SSL сертификатов Let's Encrypt
set -e
EMAIL="admin@hotelsync.ru"
DOMAINS="hotelsync.ru,www.hotelsync.ru,app.hotelsync.ru,api.hotelsync.ru,git.hotelsync.ru"
echo "→ Получение SSL сертификатов..."
# Временный nginx для certbot challenge
docker run --rm -p 80:80 -v /opt/hotelsync/certbot/www:/var/www/certbot nginx:alpine &
NGINX_PID=$!
sleep 2
for domain in hotelsync.ru app.hotelsync.ru api.hotelsync.ru git.hotelsync.ru; do
docker run --rm \
-v /opt/hotelsync/certbot/certs:/etc/letsencrypt \
-v /opt/hotelsync/certbot/www:/var/www/certbot \
certbot/certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email "$EMAIL" \
--agree-tos \
--no-eff-email \
-d "$domain" \
$([ "$domain" = "hotelsync.ru" ] && echo "-d www.hotelsync.ru" || echo "")
echo "✓ SSL для $domain получен"
done
kill $NGINX_PID 2>/dev/null || true
echo "✅ Все SSL сертификаты получены"