feat(sharing): implement room link sharing functionality and update sharing guide

This commit is contained in:
ur002 2026-02-01 19:31:50 +03:00
parent 79267cb3d2
commit ca4e1fd087
8 changed files with 309 additions and 5 deletions

76
SHARING_GUIDE.md Normal file
View File

@ -0,0 +1,76 @@
# 🔗 Руководство по шарингу комнат
## Как пригласить друзей в игру
### Способ 1: Копирование ссылки
1. Создайте комнату или зайдите в существующую
2. В лобби комнаты нажмите на кнопку **📋** рядом со ссылкой
3. Отправьте скопированную ссылку друзьям любым удобным способом
### Способ 2: Прямой шаринг в Telegram
1. В лобби комнаты нажмите кнопку **📱 Telegram**
2. Выберите чат или контакт для отправки
3. Ваш друг получит ссылку с приглашением
### Способ 3: Прямой шаринг в WhatsApp
1. В лобби комнаты нажмите кнопку **💬 WhatsApp**
2. Выберите чат или контакт для отправки
3. Ваш друг получит ссылку с приглашением
### Способ 4: Универсальный шаринг (на мобильных устройствах)
1. В лобби комнаты нажмите кнопку **🔗 Поделиться**
2. Выберите приложение для отправки (доступно на Android/iOS)
3. Отправьте ссылку через выбранное приложение
## Как присоединиться по ссылке
### Вариант A: Автоматическое присоединение
1. Перейдите по полученной ссылке
2. Войдите в игру или зарегистрируйтесь
3. Вы автоматически присоединитесь к комнате
### Вариант B: Ручное присоединение
1. Войдите в игру
2. Перейдите в раздел "Мультиплеер"
3. Найдите комнату в списке и присоединитесь
## Формат ссылки
Ссылка на комнату имеет вид:
```
http://localhost:3000/?room=room_1234567890_abc123
```
Где `room_1234567890_abc123` - уникальный ID комнаты.
## Особенности
- ✅ Ссылка работает до тех пор, пока комната активна
- ✅ По одной ссылке может присоединиться несколько игроков
- ✅ Комната закрывается, когда все игроки выходят
- ✅ Максимальное количество игроков устанавливается при создании комнаты
## Советы
1. **Быстрое копирование**: Кликните на поле со ссылкой - текст выделится автоматически
2. **Проверка ссылки**: Ссылка обновляется автоматически при изменении комнаты
3. **Мобильные устройства**: Используйте кнопку "Поделиться" для быстрого шаринга
4. **Telegram/WhatsApp**: Работает как на десктопе, так и на мобильных устройствах
## Техническая информация
### Поддерживаемые браузеры
- Chrome/Edge: все функции
- Firefox: все функции
- Safari (iOS): все функции включая Web Share API
- Мобильные браузеры: полная поддержка
### API
- **Clipboard API** - для копирования ссылки
- **Web Share API** - для универсального шаринга (мобильные устройства)
- **URL Parameters** - для передачи ID комнаты
### Безопасность
- ID комнаты генерируется на сервере
- Невозможно подделать или угадать ID
- Комната доступна только активным пользователям

View File

@ -40,6 +40,9 @@ async function initDatabase() {
// Создаём админа по умолчанию
await createDefaultAdmin();
// Всегда проверяем и инициализируем админские настройки
initDefaultAdminSettings();
return db;
}
@ -150,9 +153,6 @@ async function createDefaultAdmin() {
saveDatabase();
console.log('👑 Создан администратор по умолчанию: admin / admin123');
}
// Инициализируем дефолтные админские настройки
initDefaultAdminSettings();
}
/**

BIN
poker.db

Binary file not shown.

View File

@ -188,6 +188,11 @@ function onAuthSuccess() {
if (!isGuest && authToken) {
loadUserSettingsFromServer();
}
// Проверяем наличие комнаты в URL для автоматического присоединения
if (typeof checkRoomInUrl === 'function') {
checkRoomInUrl();
}
}
/**
@ -198,6 +203,7 @@ function updateUserDisplay() {
const badgeEl = document.getElementById('user-role-badge');
const adminBtn = document.getElementById('admin-btn');
const serverUrlGroup = document.getElementById('server-url-group');
const adminLlmButtonGroup = document.getElementById('admin-llm-button-group');
if (currentUser) {
nameEl.textContent = currentUser.username;
@ -207,10 +213,12 @@ function updateUserDisplay() {
badgeEl.textContent = 'ADMIN';
if (adminBtn) adminBtn.style.display = 'block';
if (serverUrlGroup) serverUrlGroup.style.display = 'block';
if (adminLlmButtonGroup) adminLlmButtonGroup.style.display = 'block';
} else {
badgeEl.style.display = 'none';
if (adminBtn) adminBtn.style.display = 'none';
if (serverUrlGroup) serverUrlGroup.style.display = 'none';
if (adminLlmButtonGroup) adminLlmButtonGroup.style.display = 'none';
}
// Обновляем поля имени в формах
@ -363,7 +371,12 @@ function switchAdminTab(tab) {
* Загрузить админские настройки
*/
async function loadAdminSettings() {
if (!authToken || currentUser?.role !== 'admin') return;
if (!authToken || currentUser?.role !== 'admin') {
console.log('❌ Нет прав для загрузки админ настроек');
return;
}
console.log('📥 Загрузка админских настроек...');
try {
const response = await fetch('/api/settings/admin', {
@ -374,14 +387,18 @@ async function loadAdminSettings() {
const data = await response.json();
const s = data.settings;
console.log('✅ Получены настройки:', s);
document.getElementById('admin-llm-enabled').checked = s.llmEnabled;
document.getElementById('admin-llm-provider').value = s.llmProvider || 'ollama';
document.getElementById('admin-llm-url').value = s.llmApiUrl || 'http://localhost:11434';
document.getElementById('admin-llm-model').value = s.llmModel || 'llama3.2';
document.getElementById('admin-llm-apikey').value = s.llmApiKey || '';
} else {
console.error('❌ Ошибка ответа сервера:', response.status);
}
} catch (error) {
console.error('Ошибка загрузки админ настроек:', error);
console.error('Ошибка загрузки админ настроек:', error);
}
}
@ -399,6 +416,8 @@ async function saveAdminSettings() {
llmApiKey: document.getElementById('admin-llm-apikey').value
};
console.log('💾 Сохранение админских настроек:', settings);
try {
const response = await fetch('/api/settings/admin', {
method: 'POST',
@ -411,9 +430,14 @@ async function saveAdminSettings() {
if (response.ok) {
showNotification('Настройки сохранены', 'success');
// Обновляем статус LLM для отображения в настройках
loadLLMStatus();
} else {
showNotification('Ошибка сохранения', 'error');
}
} catch (error) {
showNotification('Ошибка сохранения', 'error');
console.error('Ошибка:', error);
}
}

View File

@ -221,6 +221,27 @@
<button class="btn-back" onclick="leaveRoom()">← Выйти</button>
<h2 id="lobby-room-name">Комната</h2>
<!-- Блок для шаринга ссылки на комнату -->
<div class="room-share-section">
<div class="room-link-display">
<input type="text" id="room-link-input" class="input" readonly onclick="this.select()">
<button class="btn btn-secondary btn-icon-only" onclick="copyRoomLink()" title="Копировать ссылку">
📋
</button>
</div>
<div class="share-buttons">
<button class="btn btn-outline btn-small" onclick="shareToTelegram()">
<span>📱 Telegram</span>
</button>
<button class="btn btn-outline btn-small" onclick="shareToWhatsApp()">
<span>💬 WhatsApp</span>
</button>
<button class="btn btn-outline btn-small" onclick="shareGeneric()" id="share-generic-btn">
<span>🔗 Поделиться</span>
</button>
</div>
</div>
<div class="lobby-players" id="lobby-players">
<!-- Игроки будут добавлены динамически -->
</div>
@ -461,6 +482,13 @@
<span id="llm-status-text">LLM чат: загрузка...</span>
</div>
<!-- Кнопка настройки LLM для админа -->
<div class="admin-only" id="admin-llm-button-group" style="display: none; margin-top: 12px;">
<button class="btn btn-primary" onclick="showScreen('admin-screen')">
👑 Настроить LLM (Админ-панель)
</button>
</div>
<div style="margin-top: 16px;"></div>
<button class="btn btn-outline" onclick="resetSettings()">Сбросить настройки</button>
</div>

View File

@ -495,6 +495,9 @@ function showRoomLobby(room) {
document.getElementById('lobby-room-name').textContent = room.name;
document.getElementById('lobby-blinds').textContent = `${room.smallBlind}/${room.bigBlind}`;
// Генерируем и отображаем ссылку на комнату
updateRoomLink(room.id);
updateLobbyPlayers(room);
}
@ -519,6 +522,11 @@ function updateLobbyPlayers(room) {
const startBtn = document.getElementById('start-game-btn');
startBtn.style.display = room.players[0]?.id === currentPlayerId ? 'block' : 'none';
startBtn.disabled = room.players.length < 2;
// Обновляем ссылку на комнату (если ID изменился)
if (room.id) {
updateRoomLink(room.id);
}
}
/**
@ -2000,3 +2008,136 @@ function showNotification(message, type = 'info') {
notification.remove();
}, 3000);
}
// =============================================================================
// ШАРИНГ КОМНАТЫ
// =============================================================================
/**
* Обновить ссылку на комнату
*/
function updateRoomLink(roomId) {
const baseUrl = window.location.origin + window.location.pathname;
const roomUrl = `${baseUrl}?room=${roomId}`;
const input = document.getElementById('room-link-input');
if (input) {
input.value = roomUrl;
}
// Проверяем поддержку Web Share API
const shareBtn = document.getElementById('share-generic-btn');
if (shareBtn) {
if (navigator.share) {
shareBtn.style.display = 'inline-flex';
} else {
shareBtn.style.display = 'none';
}
}
}
/**
* Копировать ссылку на комнату
*/
async function copyRoomLink() {
const input = document.getElementById('room-link-input');
const url = input.value;
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(url);
showNotification('Ссылка скопирована!', 'success');
} else {
// Запасной вариант для старых браузеров
input.select();
document.execCommand('copy');
showNotification('Ссылка скопирована!', 'success');
}
} catch (error) {
console.error('Ошибка копирования:', error);
showNotification('Не удалось скопировать ссылку', 'error');
}
}
/**
* Поделиться в Telegram
*/
function shareToTelegram() {
const input = document.getElementById('room-link-input');
const url = input.value;
const roomName = document.getElementById('lobby-room-name').textContent;
const text = `Присоединяйся к игре в покер "${roomName}"!`;
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
window.open(telegramUrl, '_blank');
}
/**
* Поделиться в WhatsApp
*/
function shareToWhatsApp() {
const input = document.getElementById('room-link-input');
const url = input.value;
const roomName = document.getElementById('lobby-room-name').textContent;
const text = `Присоединяйся к игре в покер "${roomName}"!\n${url}`;
const whatsappUrl = `https://wa.me/?text=${encodeURIComponent(text)}`;
window.open(whatsappUrl, '_blank');
}
/**
* Поделиться через Web Share API
*/
async function shareGeneric() {
const input = document.getElementById('room-link-input');
const url = input.value;
const roomName = document.getElementById('lobby-room-name').textContent;
if (navigator.share) {
try {
await navigator.share({
title: 'Texas Hold\'em Poker',
text: `Присоединяйся к игре в покер "${roomName}"!`,
url: url
});
showNotification('Ссылка отправлена!', 'success');
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Ошибка шаринга:', error);
showNotification('Не удалось поделиться', 'error');
}
}
} else {
showNotification('Ваш браузер не поддерживает эту функцию', 'info');
}
}
/**
* Проверить и присоединиться к комнате из URL
*/
function checkRoomInUrl() {
const urlParams = new URLSearchParams(window.location.search);
const roomId = urlParams.get('room');
if (roomId && currentPlayerId) {
// Если есть ID комнаты в URL, пытаемся присоединиться
showNotification('Подключение к комнате...', 'info');
// Переходим в мультиплеер и подключаемся
setTimeout(() => {
showScreen('multiplayer-menu');
// Ждём подключения WebSocket
if (ws && ws.readyState === WebSocket.OPEN) {
joinRoom(roomId);
} else {
connectWebSocket().then(() => {
setTimeout(() => joinRoom(roomId), 500);
});
}
}, 500);
}
}

View File

@ -568,6 +568,40 @@ body::before {
font-size: 14px;
}
/* Блок шаринга комнаты */
.room-share-section {
margin-bottom: 24px;
padding: 16px;
background: var(--bg-secondary);
border-radius: var(--border-radius);
border: 1px solid var(--glass-border);
}
.room-link-display {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.room-link-display .input {
flex: 1;
font-size: 13px;
font-family: monospace;
cursor: pointer;
}
.share-buttons {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.share-buttons .btn {
flex: 1;
min-width: 100px;
}
.lobby-chat {
margin-top: 24px;
border-top: 1px solid var(--glass-border);

View File

@ -122,6 +122,7 @@ app.post('/api/settings/user', authMiddleware, (req, res) => {
// Получить админские настройки
app.get('/api/settings/admin', authMiddleware, adminMiddleware, (req, res) => {
const settings = database.getAdminSettings();
console.log('📤 Отправка админских настроек:', settings);
res.json({ settings });
});