montana/Русский/Логистика/miniapp.html

1209 lines
52 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>SeaFare Montana</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/supercluster@8.0.1/dist/supercluster.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: var(--tg-theme-bg-color, #0a0e17);
--bg2: var(--tg-theme-secondary-bg-color, #111827);
--text: var(--tg-theme-text-color, #e0e6ed);
--hint: var(--tg-theme-hint-color, #7b8a9e);
--link: var(--tg-theme-link-color, #5eaaef);
--btn: var(--tg-theme-button-color, #00bfff);
--btn-text: var(--tg-theme-button-text-color, #ffffff);
--accent: #00bfff;
--card: rgba(17, 24, 39, 0.95);
--border: rgba(255,255,255,0.08);
--safe-top: var(--tg-content-safe-area-inset-top, 0px);
--safe-bottom: var(--tg-safe-area-inset-bottom, 0px);
}
html, body { height: 100%; overflow: hidden; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg); color: var(--text);
}
/* Tabs */
.app { display: flex; flex-direction: column; height: 100vh; }
.tab-content { flex: 1; position: relative; overflow: hidden; }
.tab-pane { display: none; position: absolute; inset: 0; }
.tab-pane.active { display: flex; flex-direction: column; }
.bottom-nav {
display: flex; background: var(--bg2); border-top: 1px solid var(--border);
padding-bottom: var(--safe-bottom); z-index: 1000;
}
.nav-item {
flex: 1; text-align: center; padding: 8px 0 6px; cursor: pointer;
color: var(--hint); font-size: 11px; transition: color 0.2s;
}
.nav-item.active { color: var(--accent); }
.nav-item svg { display: block; margin: 0 auto 2px; width: 24px; height: 24px; }
/* Map */
#map { flex: 1; z-index: 1; }
.map-search {
position: absolute; top: calc(8px + var(--safe-top)); left: 10px; right: 10px;
z-index: 1001;
}
.map-search input {
width: 100%; padding: 10px 14px; border-radius: 12px; border: none;
background: var(--card); color: var(--text); font-size: 14px;
backdrop-filter: blur(10px); outline: none;
}
.map-search input::placeholder { color: var(--hint); }
/* Vessel popup (compact Leaflet popup near marker) */
.leaflet-popup-content-wrapper { background: var(--card) !important; color: var(--text) !important; border-radius: 10px !important; box-shadow: 0 4px 20px rgba(0,0,0,0.5) !important; padding: 0 !important; }
.leaflet-popup-content { margin: 0 !important; min-width: 210px; max-width: 260px; }
.leaflet-popup-tip { background: var(--card) !important; }
.leaflet-popup-close-button { color: var(--hint) !important; font-size: 18px !important; top: 4px !important; right: 6px !important; }
.vp { padding: 12px 14px; font-size: 14px; line-height: 1.5; }
.vp-name { font-size: 15px; font-weight: 700; color: var(--accent); margin-bottom: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
.vp-row { display: flex; justify-content: space-between; gap: 10px; padding: 2px 0; }
.vp-lbl { color: var(--hint); font-size: 13px; white-space: nowrap; }
.vp-val { color: #fff; font-size: 13px; font-weight: 600; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
.vp-btn { display: block; width: 100%; margin-top: 10px; padding: 8px; border: 1px solid var(--accent); border-radius: 8px; background: transparent; color: var(--accent); font-size: 13px; font-weight: 600; cursor: pointer; text-align: center; }
.vp-btn:active { background: var(--accent); color: var(--bg); }
/* Chat */
.chat-container { display: flex; flex-direction: column; height: 100%; padding-top: var(--safe-top); }
.chat-messages {
flex: 1; overflow-y: auto; padding: 12px; display: flex;
flex-direction: column; gap: 8px;
}
.msg { max-width: 85%; padding: 10px 14px; border-radius: 16px; font-size: 14px; line-height: 1.5; word-wrap: break-word; }
.msg-user { align-self: flex-end; background: var(--btn); color: var(--btn-text); border-bottom-right-radius: 4px; }
.msg-bot { align-self: flex-start; background: var(--bg2); color: var(--text); border-bottom-left-radius: 4px; }
.msg-bot pre { background: rgba(0,0,0,0.3); padding: 8px; border-radius: 8px; overflow-x: auto; font-size: 12px; margin: 4px 0; }
.msg-typing { align-self: flex-start; background: var(--bg2); padding: 12px 18px; border-radius: 16px; }
.msg-typing span { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--hint); margin: 0 2px; animation: typing 1.2s infinite; }
.msg-typing span:nth-child(2) { animation-delay: 0.2s; }
.msg-typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing { 0%,60%,100% { opacity: 0.3; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-4px); } }
.chat-chips { display: flex; gap: 6px; padding: 8px 12px; overflow-x: auto; flex-shrink: 0; }
.chat-chip {
white-space: nowrap; padding: 6px 12px; border-radius: 16px; font-size: 12px;
background: var(--bg2); color: var(--accent); border: 1px solid var(--border);
cursor: pointer; flex-shrink: 0;
}
.chat-input-area {
display: flex; gap: 8px; padding: 8px 12px;
padding-bottom: calc(8px + var(--safe-bottom));
background: var(--bg); border-top: 1px solid var(--border);
}
.chat-input-area textarea {
flex: 1; resize: none; border: none; border-radius: 20px; padding: 10px 14px;
background: var(--bg2); color: var(--text); font-size: 14px;
font-family: inherit; outline: none; max-height: 100px;
}
.chat-input-area textarea::placeholder { color: var(--hint); }
.chat-send {
width: 40px; height: 40px; border-radius: 50%; border: none;
background: var(--btn); color: var(--btn-text); font-size: 18px;
cursor: pointer; display: flex; align-items: center; justify-content: center;
flex-shrink: 0; align-self: flex-end;
}
/* Profile */
.profile-container {
height: 100%; overflow-y: auto; padding: 16px;
padding-top: calc(16px + var(--safe-top));
padding-bottom: calc(16px + var(--safe-bottom));
}
.profile-header {
text-align: center; padding: 20px 0;
}
.profile-avatar {
width: 64px; height: 64px; border-radius: 50%; background: var(--btn);
display: flex; align-items: center; justify-content: center;
font-size: 24px; font-weight: 700; color: var(--btn-text); margin: 0 auto 10px;
}
.profile-name { font-size: 18px; font-weight: 600; }
.profile-role { font-size: 13px; color: var(--hint); margin-top: 2px; text-transform: capitalize; }
.profile-card {
background: var(--bg2); border-radius: 12px; padding: 16px; margin-top: 16px;
}
.profile-card-title { font-size: 12px; color: var(--hint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.profile-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--border); }
.profile-row:last-child { border-bottom: none; }
.profile-row-label { color: var(--hint); font-size: 14px; }
.profile-row-val { color: var(--text); font-size: 14px; font-weight: 500; }
.profile-watches { margin-top: 8px; }
.watch-item {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid var(--border);
}
.watch-item:last-child { border-bottom: none; }
.watch-name { font-size: 14px; font-weight: 500; }
.watch-dest { font-size: 12px; color: var(--hint); }
.watch-remove { background: none; border: none; color: #ef4444; font-size: 12px; cursor: pointer; }
.profile-btn {
display: block; width: 100%; padding: 12px; margin-top: 16px; border: none;
border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer;
text-align: center;
}
.profile-btn-primary { background: var(--btn); color: var(--btn-text); }
.profile-btn-outline { background: transparent; color: var(--accent); border: 1px solid var(--accent); }
/* Not linked screen */
.not-linked {
display: flex; flex-direction: column; align-items: center; justify-content: center;
height: 100%; padding: 40px 24px; text-align: center;
}
.not-linked-icon { font-size: 64px; margin-bottom: 20px; }
.not-linked h2 { font-size: 20px; margin-bottom: 10px; }
.not-linked p { color: var(--hint); font-size: 14px; line-height: 1.6; margin-bottom: 24px; }
.not-linked a {
display: inline-block; padding: 12px 32px; background: var(--btn); color: var(--btn-text);
border-radius: 10px; text-decoration: none; font-weight: 600; font-size: 15px;
}
/* Loading */
.loading-screen {
display: flex; flex-direction: column; align-items: center; justify-content: center;
height: 100vh; gap: 16px;
}
.spinner { width: 40px; height: 40px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Map filters */
.map-filters {
position: absolute; top: calc(48px + var(--safe-top)); left: 10px; right: 10px;
z-index: 1001; display: flex; gap: 6px; overflow-x: auto; padding: 4px 0;
-webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.map-filters::-webkit-scrollbar { display: none; }
.filter-chip {
white-space: nowrap; padding: 4px 8px; border-radius: 12px; font-size: 10px;
font-weight: 600; cursor: pointer; flex-shrink: 0; border: 1.5px solid;
transition: opacity 0.2s, transform 0.1s; user-select: none;
}
.filter-chip.active { opacity: 1; }
.filter-chip.inactive { opacity: 0.35; }
.filter-chip:active { transform: scale(0.95); }
.filter-count {
position: absolute; top: calc(74px + var(--safe-top)); left: 14px;
z-index: 1001; font-size: 10px; color: var(--hint);
background: rgba(10,14,23,0.7); padding: 2px 6px; border-radius: 6px;
}
/* Vessel arrow markers */
.vessel-marker { background: none !important; border: none !important; }
.cluster-marker {
display: flex; align-items: center; justify-content: center;
border-radius: 50%; color: #fff; font-weight: 700;
text-shadow: 0 1px 2px rgba(0,0,0,0.6); cursor: pointer;
border: 2px solid rgba(255,255,255,0.5);
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
transition: transform 0.15s ease;
}
.cluster-marker:hover { transform: scale(1.15); border-color: #fff; }
/* Leaflet overrides */
.leaflet-popup-content-wrapper { background: var(--card) !important; color: var(--text) !important; border-radius: 12px !important; }
.leaflet-popup-tip { background: var(--card) !important; }
.leaflet-control-zoom a { background: var(--card) !important; color: var(--text) !important; border-color: var(--border) !important; }
.leaflet-control-attribution { display: none !important; }
</style>
</head>
<body>
<!-- Loading -->
<div id="loadingScreen" class="loading-screen">
<div class="spinner"></div>
<div style="color:var(--hint); font-size:14px">SeaFare Montana</div>
</div>
<!-- Not linked -->
<div id="notLinkedScreen" class="not-linked" style="display:none">
<div class="not-linked-icon">&#x2693;</div>
<h2 id="nlTitle">Register First</h2>
<p id="nlText">To use the SeaFare bot, please register on our website and link your Telegram account in the profile settings.</p>
<a id="nlLink" href="https://seafare.efir.org" target="_blank" id="nlBtn">Open Website</a>
</div>
<!-- Main App -->
<div id="mainApp" class="app" style="display:none">
<!-- Map Tab -->
<div class="tab-content">
<div id="tabMap" class="tab-pane active">
<div class="map-search">
<input type="text" id="mapSearch" placeholder="Search vessel or port...">
</div>
<div class="map-filters" id="mapFilters"></div>
<div class="filter-count" id="filterCount"></div>
<div id="map"></div>
</div>
<!-- Chat Tab -->
<div id="tabChat" class="tab-pane">
<div class="chat-container">
<div class="chat-messages" id="chatMessages">
<div class="msg msg-bot">Hello! I'm your maritime AI assistant. Ask me about vessels, routes, contacts, or anything shipping-related.</div>
</div>
<div class="chat-chips" id="chatChips"></div>
<div class="chat-input-area">
<textarea id="chatInput" rows="1" placeholder="Ask anything..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat()}"></textarea>
<button class="chat-send" onclick="sendChat()">&#x27A4;</button>
</div>
</div>
</div>
<!-- Profile Tab -->
<div id="tabProfile" class="tab-pane">
<div class="profile-container">
<div class="profile-header">
<div class="profile-avatar" id="profAvatar">?</div>
<div class="profile-name" id="profName"></div>
<div class="profile-role" id="profRole"></div>
</div>
<div class="profile-card">
<div class="profile-card-title">Wallet</div>
<div class="profile-row">
<span class="profile-row-label">Balance</span>
<span class="profile-row-val" id="profBalance">$0.00</span>
</div>
</div>
<div class="profile-card">
<div class="profile-card-title">Vessel Watches</div>
<div class="profile-watches" id="profWatches">
<div style="color:var(--hint); font-size:13px; padding:8px 0">No active watches</div>
</div>
</div>
<div class="profile-card">
<div class="profile-card-title">Trade Routes</div>
<div class="profile-row-val" id="profRoutes" style="font-size:13px; line-height:1.8"></div>
</div>
<div class="profile-card">
<div class="profile-card-title">Cargo Types</div>
<div class="profile-row-val" id="profCargo" style="font-size:13px; line-height:1.8"></div>
</div>
<button class="profile-btn profile-btn-outline" onclick="openFullSite()">Open Full Website</button>
</div>
</div>
</div>
<!-- Bottom Nav -->
<div class="bottom-nav">
<div class="nav-item active" onclick="switchTab('map')" id="navMap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 6v16l7-4 8 4 7-4V2l-7 4-8-4-7 4z"/><path d="M8 2v16"/><path d="M16 6v16"/></svg>
Map
</div>
<div class="nav-item" onclick="switchTab('chat')" id="navChat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
Chat
</div>
<div class="nav-item" onclick="switchTab('profile')" id="navProfile">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
Profile
</div>
</div>
</div>
<script>
window.onerror = function(msg, url, line, col, err) {
console.error("JS Error:", msg, "at", url, line);
var ls = document.getElementById("loadingScreen");
if (ls && ls.style.display !== "none") {
ls.innerHTML = "<div style=\"color:#ff6b6b;padding:20px;text-align:center;font-size:14px;\">Error: " + msg + "<br>Line: " + line + "</div>";
}
return false;
};
// =============================================================================
// Telegram WebApp Init
// =============================================================================
let tg;
try {
tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
if (tg.isVersionAtLeast && tg.isVersionAtLeast('8.0')) {
try { tg.requestFullscreen(); } catch(e) {}
}
if (tg.setHeaderColor) tg.setHeaderColor(tg.themeParams?.secondary_bg_color || '#111827');
if (tg.setBackgroundColor) tg.setBackgroundColor(tg.themeParams?.bg_color || '#0a0e17');
} catch(e) {
console.error('TG SDK init error:', e);
tg = { initData: '', initDataUnsafe: {}, themeParams: {}, ready: function(){}, expand: function(){}, close: function(){}, MainButton: { show:function(){}, hide:function(){}, onClick:function(){}, setText:function(){} }, BackButton: { show:function(){}, hide:function(){}, onClick:function(){} }, isVersionAtLeast: function(){return false;}, setHeaderColor:function(){}, setBackgroundColor:function(){} };
}
const API_BASE = '';
let authToken = null;
let currentUser = null;
let currentTab = 'map';
let leafletMap = null;
let vesselMarkers = {};
let selectedVessel = null;
let vesselPopup = null;
// Supercluster state
let _supercluster = null;
let _allVesselsGeoJSON = [];
let _clusterMarkers = [];
let _bulkLoaded = false;
let _bulkRefreshTimer = null;
const CLUSTER_ZOOM = 10;
const TYPE_CAT_NAMES = {0:'tanker',1:'bulk',2:'container',3:'cargo',4:'passenger',5:'roro',6:'fishing',7:'tug',8:'offshore',9:'other'};
let _mapLoadEpoch = 0;
let _clusterMarkersById = {};
let _lastClusterZoom = -1;
let chatHistory = [];
let mapRefreshTimer = null;
// =============================================================================
// i18n
// =============================================================================
const _i18n = {
en: {
tab_map: 'Map', tab_chat: 'Chat', tab_profile: 'Profile',
search_placeholder: 'Search vessel or port...',
chat_welcome: "Hello! I'm your maritime AI assistant. Ask me about vessels, routes, contacts, or anything shipping-related.",
chip1: 'Vessels near Istanbul', chip1_q: 'Vessels near Istanbul',
chip2: 'Freight rates', chip2_q: 'Freight rate Black Sea',
chip3: 'Track vessel', chip3_q: 'Track vessel',
chip4: 'Contacts', chip4_q: 'Find contacts for MAERSK',
ask_placeholder: 'Ask anything...',
wallet: 'Wallet', balance: 'Balance',
watches: 'Vessel Watches', no_watches: 'No active watches',
trade_routes: 'Trade Routes', cargo_types: 'Cargo Types',
open_website: 'Open Full Website',
ai_details: 'AI Details', track_btn: 'Track',
nl_title: 'Register First',
nl_text: 'To use the SeaFare bot, register on our website and link your Telegram in profile settings.',
nl_link: 'Open Website',
type: 'Type', flag: 'Flag', destination: 'Destination',
speed: 'Speed', course: 'Course',
tell_about: 'Tell me about vessel ',
track_vessel: 'Track vessel ',
track_notify: ' and notify me when it arrives',
search_vessel: 'Search vessel ',
error: 'Error: ', member: 'member',
},
ru: {
tab_map: 'Карта', tab_chat: 'Чат', tab_profile: 'Профиль',
search_placeholder: 'Поиск судна или порта...',
chat_welcome: 'Привет! Я морской AI-помощник. Спросите о судах, маршрутах, фрахтовых ставках или чём-либо в судоходстве.',
chip1: 'Суда у Стамбула', chip1_q: 'Суда рядом со Стамбулом',
chip2: 'Фрахтовые ставки', chip2_q: 'Фрахтовые ставки Чёрное море',
chip3: 'Отследить судно', chip3_q: 'Отследить судно',
chip4: 'Загруженность порта', chip4_q: 'Загруженность порта Новороссийск',
ask_placeholder: 'Спросите что угодно...',
wallet: 'Кошелёк', balance: 'Баланс',
watches: 'Отслеживания', no_watches: 'Нет активных отслеживаний',
trade_routes: 'Торговые маршруты', cargo_types: 'Типы грузов',
open_website: 'Открыть сайт',
ai_details: 'Подробнее', track_btn: 'Отследить',
nl_title: 'Сначала регистрация',
nl_text: 'Чтобы пользоваться ботом SeaFare, зарегистрируйтесь на сайте и привяжите Telegram в настройках профиля.',
nl_link: 'Открыть сайт',
type: 'Тип', flag: 'Флаг', destination: 'Назначение',
speed: 'Скорость', course: 'Курс',
tell_about: 'Расскажи о судне ',
track_vessel: 'Отследить судно ',
track_notify: ' и уведомить о прибытии',
search_vessel: 'Найти судно ',
error: 'Ошибка: ', member: 'участник',
},
es: {
tab_map: 'Mapa', tab_chat: 'Chat', tab_profile: 'Perfil',
search_placeholder: 'Buscar buque o puerto...',
chat_welcome: '¡Hola! Soy tu asistente marítimo de IA. Pregúntame sobre buques, rutas, fletes o cualquier tema naviero.',
chip1: 'Buques en Estambul', chip1_q: 'Buques cerca de Estambul',
chip2: 'Tarifas de flete', chip2_q: 'Tarifas de flete Mar Negro',
chip3: 'Rastrear buque', chip3_q: 'Rastrear buque',
chip4: 'Congestión portuaria', chip4_q: 'Congestión portuaria Novorossiysk',
ask_placeholder: 'Pregunta lo que sea...',
wallet: 'Billetera', balance: 'Saldo',
watches: 'Seguimientos', no_watches: 'Sin seguimientos activos',
trade_routes: 'Rutas comerciales', cargo_types: 'Tipos de carga',
open_website: 'Abrir sitio web',
ai_details: 'Detalles IA', track_btn: 'Rastrear',
nl_title: 'Regístrese primero',
nl_text: 'Para usar el bot SeaFare, regístrese en nuestro sitio web y vincule su Telegram en la configuración del perfil.',
nl_link: 'Abrir sitio web',
type: 'Tipo', flag: 'Bandera', destination: 'Destino',
speed: 'Velocidad', course: 'Rumbo',
tell_about: 'Cuéntame sobre el buque ',
track_vessel: 'Rastrear buque ',
track_notify: ' y notificarme cuando llegue',
search_vessel: 'Buscar buque ',
error: 'Error: ', member: 'miembro',
}
};
let _lang = 'en';
function t(k) { return (_i18n[_lang] && _i18n[_lang][k]) || _i18n.en[k] || k; }
function applyTranslations() {
// Bottom nav
document.getElementById('navMap').lastChild.textContent = t('tab_map');
document.getElementById('navChat').lastChild.textContent = t('tab_chat');
document.getElementById('navProfile').lastChild.textContent = t('tab_profile');
// Map
document.getElementById('mapSearch').placeholder = t('search_placeholder');
// Chat
const welcomeMsg = document.querySelector('#chatMessages .msg-bot');
if (welcomeMsg) welcomeMsg.textContent = t('chat_welcome');
document.getElementById('chatInput').placeholder = t('ask_placeholder');
// Chat chips (rebuild with translated text + queries)
const chipsEl = document.getElementById('chatChips');
const chipData = [
{ label: t('chip1'), query: t('chip1_q') },
{ label: t('chip2'), query: t('chip2_q') },
{ label: t('chip3'), query: t('chip3_q') },
{ label: t('chip4'), query: t('chip4_q') },
];
chipsEl.innerHTML = chipData.map(c =>
`<div class="chat-chip" onclick="quickChat('${c.query.replace(/'/g, "\\'")}')">${c.label}</div>`
).join('');
// Vessel card buttons
var _bd = document.querySelector('.btn-details'); if(_bd) _bd.textContent = t('ai_details');
var _bt = document.querySelector('.btn-track'); if(_bt) _bt.textContent = t('track_btn');
// Profile
document.querySelectorAll('.profile-card-title').forEach(el => {
const txt = el.textContent.trim();
if (txt === 'Wallet') el.textContent = t('wallet');
else if (txt === 'Vessel Watches') el.textContent = t('watches');
else if (txt === 'Trade Routes') el.textContent = t('trade_routes');
else if (txt === 'Cargo Types') el.textContent = t('cargo_types');
});
var _bl = document.querySelector('.profile-row-label'); if(_bl) _bl.textContent = t('balance');
const watchesEmpty = document.querySelector('#profWatches > div');
if (watchesEmpty) watchesEmpty.textContent = t('no_watches');
var _po = document.querySelector('.profile-btn-outline'); if(_po) _po.textContent = t('open_website');
// Not linked
document.getElementById('nlTitle').textContent = t('nl_title');
document.getElementById('nlText').textContent = t('nl_text');
document.getElementById('nlLink').textContent = t('nl_link');
}
// =============================================================================
// Auth
// =============================================================================
async function authenticate() {
// 1. Try Telegram WebApp auth (inside Telegram)
if (tg.initData) {
try {
const resp = await fetch(API_BASE + '/api/v1/auth/telegram-webapp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ initData: tg.initData })
});
const data = await resp.json();
if (data.success) {
authToken = data.token;
currentUser = data.user;
return true;
}
window._authError = data.error || 'unknown';
return false;
} catch (e) {
window._authError = e.message;
return false;
}
}
// 2. Fallback: try web session token (opened in browser, not Telegram)
var webToken = localStorage.getItem('authToken');
if (webToken) {
try {
const resp = await fetch(API_BASE + '/api/v1/profile', {
headers: { 'Authorization': 'Bearer ' + webToken }
});
if (resp.ok) {
authToken = webToken;
const pd = await resp.json();
currentUser = pd.profile || {};
return true;
}
} catch (e) {}
}
window._authError = 'not_in_telegram';
return false;
}
async function init() {
// Detect language from Telegram
const tgLang = (tg.initDataUnsafe?.user?.language_code || 'en').substring(0, 2);
_lang = ['ru', 'es'].includes(tgLang) ? tgLang : 'en';
try { applyTranslations(); } catch(e) { console.warn('applyTranslations:', e.message); }
const ok = await authenticate();
document.getElementById('loadingScreen').style.display = 'none';
if (!ok) {
// If opened in browser (not Telegram) and no web session — redirect to main site
if (window._authError === 'not_in_telegram') {
window.location.href = '/';
return;
}
document.getElementById('notLinkedScreen').style.display = 'flex';
return;
}
document.getElementById('mainApp').style.display = 'flex';
await loadChatHistory();
initMap();
loadProfile();
// BackButton navigation
tg.BackButton.onClick(function() {
if (currentTab !== 'map') {
switchTab('map');
} else {
tg.close();
}
});
}
// =============================================================================
// Tab Navigation
// =============================================================================
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('tab' + tab.charAt(0).toUpperCase() + tab.slice(1)).classList.add('active');
document.getElementById('nav' + tab.charAt(0).toUpperCase() + tab.slice(1)).classList.add('active');
if (tab === 'map') {
tg.BackButton.hide();
if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 100);
startMapRefresh();
} else {
tg.BackButton.show();
stopMapRefresh();
}
if (tab === 'profile') loadProfile();
if (tg.HapticFeedback) tg.HapticFeedback.selectionChanged();
}
// =============================================================================
// Map
// =============================================================================
function initMap() {
leafletMap = L.map('map', {
zoomControl: false,
attributionControl: false,
}).setView([37.5, 24.0], 6);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 18,
}).addTo(leafletMap);
L.control.zoom({ position: 'bottomright' }).addTo(leafletMap);
// Init Supercluster
if (typeof Supercluster !== 'undefined') {
_supercluster = new Supercluster({
radius: 60, maxZoom: CLUSTER_ZOOM - 1, minZoom: 2,
map: (props) => {
const counts = {};
for (let i = 0; i < 10; i++) counts['c' + i] = 0;
counts['c' + props.type_cat] = 1;
return counts;
},
reduce: (acc, props) => {
for (let i = 0; i < 10; i++) acc['c' + i] += props['c' + i];
}
});
}
let _mvTimeout = null;
leafletMap.on('moveend', () => {
clearTimeout(_mvTimeout);
_mvTimeout = setTimeout(() => {
if (!leafletMap) return;
const zoom = leafletMap.getZoom();
if (zoom >= CLUSTER_ZOOM) {
loadMapVessels().then(() => {
if (leafletMap && leafletMap.getZoom() >= CLUSTER_ZOOM) clearClusters();
});
} else if (_bulkLoaded) {
renderClusters();
clearIndividualMarkers();
}
}, 300);
});
initFilters();
// Handle clicks inside vessel popup (standard Leaflet pattern)
leafletMap.on('popupopen', function() {
var btn = document.querySelector('.vp-btn');
if (btn) {
btn.onclick = function() { askAboutVessel(); };
}
});
// Search
document.getElementById('mapSearch').addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
searchOnMap(this.value.trim());
}
});
loadBulkVessels();
}
function clearClusters() {
_clusterMarkers.forEach(m => leafletMap.removeLayer(m));
_clusterMarkers = [];
_clusterMarkersById = {};
_lastClusterZoom = -1;
}
function clearIndividualMarkers() {
Object.values(vesselMarkers).forEach(m => { if (leafletMap.hasLayer(m)) leafletMap.removeLayer(m); });
vesselMarkers = {};
}
async function loadBulkVessels() {
if (!leafletMap) return;
try {
const resp = await fetch(API_BASE + '/api/v1/map/vessels/all', {
headers: authToken ? { 'Authorization': 'Bearer ' + authToken } : {}
});
if (!resp.ok) { loadMapVessels(); return; }
const data = await resp.json();
if (!data.success || !data.vessels) { loadMapVessels(); return; }
_allVesselsGeoJSON = [];
for (const v of data.vessels) {
if (v[1] == null || v[2] == null) continue;
_allVesselsGeoJSON.push({
type: 'Feature',
geometry: { type: 'Point', coordinates: [v[2], v[1]] },
properties: {
mmsi: v[0], type_cat: v[3] != null ? v[3] : 9,
heading: v[4], speed: v[5], course: v[6], name: v[7] || ''
}
});
}
if (_supercluster && _allVesselsGeoJSON.length > 0) {
_supercluster.load(_allVesselsGeoJSON);
_bulkLoaded = true;
if (leafletMap.getZoom() < CLUSTER_ZOOM) {
clearIndividualMarkers();
renderClusters();
}
}
if (_bulkRefreshTimer) clearInterval(_bulkRefreshTimer);
_bulkRefreshTimer = setInterval(loadBulkVessels, 120000);
if (leafletMap.getZoom() >= CLUSTER_ZOOM) loadMapVessels();
} catch (e) {
console.warn('Bulk load failed:', e);
loadMapVessels();
}
}
function renderClusters() {
if (!_supercluster || !_bulkLoaded || !leafletMap) return;
const b = leafletMap.getBounds();
const bbox = [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()];
const zoom = leafletMap.getZoom();
// Zoom changed → full rebuild
if (zoom !== _lastClusterZoom) {
clearClusters();
_lastClusterZoom = zoom;
}
const clusters = _supercluster.getClusters(bbox, zoom);
const desiredKeys = new Set();
let totalShown = 0;
clusters.forEach(c => {
const coords = c.geometry.coordinates;
const props = c.properties;
if (props.cluster) {
const key = 'c_' + props.cluster_id;
let filteredCount = 0;
for (let i = 0; i < 10; i++) {
const catName = TYPE_CAT_NAMES[i];
const filterCat = VESSEL_FILTER_LABELS[catName] ? catName : 'other';
if (activeFilters.has(filterCat)) filteredCount += (props['c' + i] || 0);
}
if (filteredCount === 0) return;
desiredKeys.add(key);
totalShown += filteredCount;
if (_clusterMarkersById[key]) return; // diff: skip existing
let maxCat = 9, maxCount = 0;
for (let i = 0; i < 10; i++) {
const catName = TYPE_CAT_NAMES[i];
const filterCat = VESSEL_FILTER_LABELS[catName] ? catName : 'other';
if (activeFilters.has(filterCat) && (props['c' + i] || 0) > maxCount) {
maxCount = props['c' + i]; maxCat = i;
}
}
const color = VESSEL_COLORS[TYPE_CAT_NAMES[maxCat]] || VESSEL_COLORS.other;
const size = Math.min(56, Math.max(22, 18 + Math.sqrt(filteredCount) * 3));
const label = filteredCount >= 1000 ? (filteredCount / 1000).toFixed(1) + 'K' : String(filteredCount);
const fontSize = filteredCount >= 1000 ? 9 : (filteredCount >= 100 ? 10 : 11);
const icon = L.divIcon({
html: `<div class="cluster-marker" style="width:${size}px;height:${size}px;background:radial-gradient(circle at 35% 35%,${color}ee,${color}99);font-size:${fontSize}px">${label}</div>`,
className: '', iconSize: [size, size], iconAnchor: [size/2, size/2]
});
const m = L.marker([coords[1], coords[0]], { icon: icon });
const clusterId = props.cluster_id;
m.on('click', () => {
const expZoom = _supercluster.getClusterExpansionZoom(clusterId);
leafletMap.setView([coords[1], coords[0]], Math.min(expZoom, CLUSTER_ZOOM));
});
m.addTo(leafletMap);
_clusterMarkers.push(m);
_clusterMarkersById[key] = m;
} else {
const key = 's_' + (props.mmsi || coords.join(','));
const catName = TYPE_CAT_NAMES[props.type_cat] || 'other';
const filterCat = VESSEL_FILTER_LABELS[catName] ? catName : 'other';
if (!activeFilters.has(filterCat)) return;
desiredKeys.add(key);
totalShown++;
if (_clusterMarkersById[key]) return; // diff: skip existing
const v = { _cat: catName, heading: props.heading, course: props.course, name: props.name, mmsi: props.mmsi };
const m = L.marker([coords[1], coords[0]], { icon: createVesselIcon(v) });
m.on('click', () => { leafletMap.setView([coords[1], coords[0]], CLUSTER_ZOOM); });
m.addTo(leafletMap);
_clusterMarkers.push(m);
_clusterMarkersById[key] = m;
}
});
// Remove markers no longer in viewport
for (const key of Object.keys(_clusterMarkersById)) {
if (!desiredKeys.has(key)) {
leafletMap.removeLayer(_clusterMarkersById[key]);
const idx = _clusterMarkers.indexOf(_clusterMarkersById[key]);
if (idx !== -1) _clusterMarkers.splice(idx, 1);
delete _clusterMarkersById[key];
}
}
document.getElementById('filterCount').textContent = totalShown + ' / ' + _allVesselsGeoJSON.length;
}
function startMapRefresh() {
if (_bulkLoaded && !_bulkRefreshTimer) {
_bulkRefreshTimer = setInterval(loadBulkVessels, 120000);
}
}
function stopMapRefresh() {
if (_bulkRefreshTimer) { clearInterval(_bulkRefreshTimer); _bulkRefreshTimer = null; }
if (mapRefreshTimer) { clearInterval(mapRefreshTimer); mapRefreshTimer = null; }
}
async function loadMapVessels() {
if (!leafletMap) return;
const epoch = ++_mapLoadEpoch;
const b = leafletMap.getBounds();
try {
const resp = await fetch(API_BASE + '/api/v1/map/vessels?' +
`lat_min=${b.getSouth().toFixed(4)}&lat_max=${b.getNorth().toFixed(4)}` +
`&lon_min=${b.getWest().toFixed(4)}&lon_max=${b.getEast().toFixed(4)}&limit=500`, {
headers: authToken ? { 'Authorization': 'Bearer ' + authToken } : {}
});
if (!resp.ok) return;
if (epoch !== _mapLoadEpoch) return;
const data = await resp.json();
if (!data.success || epoch !== _mapLoadEpoch) return;
allVesselsCache = data.vessels || [];
updateMarkers(allVesselsCache);
} catch (e) { console.error('Map load error:', e); }
}
const VESSEL_COLORS = {
tanker:'#d32f2f', bulk:'#ef6c00', container:'#388e3c', cargo:'#f9a825',
general:'#f9a825', passenger:'#1565c0', roro:'#00838f', offshore:'#ad1457',
tug:'#00acc1', fishing:'#6a1b9a', highspeed:'#fdd835', pleasure:'#7b1fa2',
military:'#546e7a', sailing:'#ec407a', other:'#78909c'
};
const VESSEL_FILTER_LABELS = {
tanker: {en:'Tanker', ru:'Танкер', es:'Tanque'},
bulk: {en:'Bulk', ru:'Балкер', es:'Granelero'},
container: {en:'Container', ru:'Контейнер', es:'Contenedor'},
cargo: {en:'Cargo', ru:'Карго', es:'Carga'},
passenger: {en:'Passenger', ru:'Пассаж.', es:'Pasaje'},
roro: {en:'Ro-Ro', ru:'Ро-Ро', es:'Ro-Ro'},
fishing: {en:'Fishing', ru:'Рыба', es:'Pesca'},
tug: {en:'Tug', ru:'Буксир', es:'Remolc.'},
offshore: {en:'Offshore', ru:'Офшор', es:'Offshore'},
other: {en:'Other', ru:'Другие', es:'Otros'},
};
let activeFilters = new Set(Object.keys(VESSEL_FILTER_LABELS)); // all active by default
let allVesselsCache = []; // last loaded vessels for re-filtering
function initFilters() {
const bar = document.getElementById('mapFilters');
bar.innerHTML = '';
for (const [cat, labels] of Object.entries(VESSEL_FILTER_LABELS)) {
const chip = document.createElement('div');
chip.className = 'filter-chip active';
chip.dataset.cat = cat;
chip.textContent = labels[_lang] || labels.en;
const color = VESSEL_COLORS[cat] || VESSEL_COLORS.other;
chip.style.background = color + '22';
chip.style.borderColor = color;
chip.style.color = color;
chip.onclick = () => toggleFilter(cat, chip);
bar.appendChild(chip);
}
}
function toggleFilter(cat, chip) {
if (activeFilters.has(cat)) {
activeFilters.delete(cat);
chip.classList.remove('active');
chip.classList.add('inactive');
} else {
activeFilters.add(cat);
chip.classList.remove('inactive');
chip.classList.add('active');
}
applyFilters();
if (tg.HapticFeedback) tg.HapticFeedback.selectionChanged();
}
function applyFilters() {
if (leafletMap && leafletMap.getZoom() < CLUSTER_ZOOM && _bulkLoaded) {
renderClusters();
return;
}
updateMarkers(allVesselsCache);
}
function vesselTypeCategory(code, cat) {
// Prefer server-side type_category
if (cat && cat !== 'other' && VESSEL_COLORS[cat]) return cat;
code = parseInt(code) || 0;
if (code >= 70 && code < 80) return 'cargo';
if (code >= 80 && code < 90) return 'tanker';
if (code >= 60 && code < 70) return 'passenger';
if (code >= 30 && code < 40) return 'fishing';
if (code >= 50 && code < 60) return 'tug';
if (code >= 40 && code < 50) return 'highspeed';
return 'other';
}
function createVesselIcon(v) {
const cat = v._cat || 'other';
const color = VESSEL_COLORS[cat] || VESSEL_COLORS.other;
const h = (v.heading != null && v.heading !== 511) ? v.heading : null;
const rot = h || v.course || 0;
const svg = `<svg width="18" height="22" viewBox="0 0 20 24" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(${rot}deg)"><path d="M10 0L18 20L10 16L2 20Z" fill="${color}" stroke="#000" stroke-width="0.8" opacity="0.9"/></svg>`;
return L.divIcon({ html: svg, className: 'vessel-marker', iconSize: [18,22], iconAnchor: [9,11] });
}
function updateMarkers(vessels) {
const seen = new Set();
let visCount = 0;
for (const v of vessels) {
const key = v.mmsi || v.name;
if (!key) continue;
const lat = parseFloat(v.lat || v.latitude);
const lon = parseFloat(v.lon || v.longitude);
if (isNaN(lat) || isNaN(lon)) continue;
const cat = vesselTypeCategory(v.type_code || v.ship_type, v.type_category);
v._cat = cat;
// Normalize category for filter matching
const filterCat = VESSEL_FILTER_LABELS[cat] ? cat : 'other';
const visible = activeFilters.has(filterCat);
seen.add(key);
if (vesselMarkers[key]) {
if (visible) {
vesselMarkers[key].setLatLng([lat, lon]);
vesselMarkers[key].setIcon(createVesselIcon(v));
if (!leafletMap.hasLayer(vesselMarkers[key])) vesselMarkers[key].addTo(leafletMap);
visCount++;
} else {
if (leafletMap.hasLayer(vesselMarkers[key])) leafletMap.removeLayer(vesselMarkers[key]);
}
} else if (visible) {
const m = L.marker([lat, lon], { icon: createVesselIcon(v) });
m.vesselData = v;
m.on('click', function() { showVesselCard(this.vesselData); });
m.addTo(leafletMap);
vesselMarkers[key] = m;
visCount++;
}
if (vesselMarkers[key]) vesselMarkers[key].vesselData = v;
}
// Remove stale markers
for (const key of Object.keys(vesselMarkers)) {
if (!seen.has(key)) {
leafletMap.removeLayer(vesselMarkers[key]);
delete vesselMarkers[key];
}
}
// Update count
document.getElementById('filterCount').textContent = visCount + ' / ' + vessels.length;
}
function showVesselCard(v) {
selectedVessel = v;
if (vesselPopup) { leafletMap.closePopup(vesselPopup); vesselPopup = null; }
const name = v.name || v.mmsi || '\u2014';
const rows = [];
const typeName = v.type_name || v.ship_type_name || v.type_category || v._cat || '';
if (typeName) rows.push([t('type'), typeName]);
const flag = (v.flag || v.country || '').trim();
if (flag) rows.push([t('flag'), flag]);
const dwt = v.dwt || v.deadweight;
if (dwt) rows.push(['DWT', Number(dwt).toLocaleString()]);
const dest = (v.destination || '').trim();
if (dest) rows.push([t('destination'), dest]);
if (v.speed != null && v.speed !== '') rows.push([t('speed'), v.speed + ' kn']);
if (v.course != null && v.course !== '') rows.push([t('course'), v.course + '\u00b0']);
if (v.mmsi) rows.push(['MMSI', v.mmsi]);
if (v.imo) rows.push(['IMO', v.imo]);
const rowsHtml = rows.map(function(r) {
return '<div class="vp-row"><span class="vp-lbl">' + r[0] + '</span><span class="vp-val">' + r[1] + '</span></div>';
}).join('');
const html = '<div class="vp"><div class="vp-name">' + name + '</div>' + rowsHtml +
'<div class="vp-btn">AI Details</div></div>';
const lat = parseFloat(v.lat || v.latitude);
const lon = parseFloat(v.lon || v.longitude);
vesselPopup = L.popup({ closeButton: true, maxWidth: 260, minWidth: 210, offset: [0, -8] })
.setLatLng([lat, lon])
.setContent(html)
.openOn(leafletMap);
if (tg.HapticFeedback) tg.HapticFeedback.impactOccurred('light');
}
function closeVesselCard() {
if (vesselPopup) { leafletMap.closePopup(vesselPopup); vesselPopup = null; }
selectedVessel = null;
}
function askAboutVessel() {
if (!selectedVessel) return;
var name = selectedVessel.name || selectedVessel.mmsi;
var imo = selectedVessel.imo;
var mmsi = selectedVessel.mmsi;
var query = 'Vessel details: ' + name;
if (imo) query += ' IMO ' + imo;
else if (mmsi) query += ' MMSI ' + mmsi;
// Close popup and clear selection
if (vesselPopup) { leafletMap.closePopup(vesselPopup); vesselPopup = null; }
selectedVessel = null;
// Switch to chat and send
switchTab('chat');
sendChatMessage(query);
}
function trackVessel() {
if (!selectedVessel) return;
const name = selectedVessel.name || selectedVessel.mmsi;
closeVesselCard();
switchTab('chat');
sendChatMessage(t('track_vessel') + name + t('track_notify'));
}
async function searchOnMap(query) {
if (!query) return;
// Use chat API to search, then parse coordinates from response
switchTab('chat');
sendChatMessage(t('search_vessel') + query);
}
// =============================================================================
// Chat
// =============================================================================
async function loadChatHistory() {
try {
const resp = await fetch(API_BASE + '/api/v1/chat/history?limit=30', {
headers: { 'Authorization': 'Bearer ' + authToken }
});
if (!resp.ok) return;
const data = await resp.json();
if (!data.messages || !data.messages.length) return;
const messagesEl = document.getElementById('chatMessages');
messagesEl.innerHTML = '';
for (const msg of data.messages) {
const div = document.createElement('div');
div.className = msg.role === 'user' ? 'msg msg-user' : 'msg msg-bot';
if (msg.role === 'user') {
div.textContent = msg.message;
} else {
div.innerHTML = formatResponse(msg.message);
}
messagesEl.appendChild(div);
chatHistory.push({ role: msg.role, content: msg.message });
}
messagesEl.scrollTop = messagesEl.scrollHeight;
document.getElementById('chatChips').style.display = 'none';
} catch (e) { console.error('History load error:', e); }
}
let chatBusy = false;
function sendChat() {
const input = document.getElementById('chatInput');
const text = input.value.trim();
if (!text || chatBusy) return;
input.value = '';
input.style.height = 'auto';
sendChatMessage(text);
}
function quickChat(text) {
sendChatMessage(text);
if (tg.HapticFeedback) tg.HapticFeedback.impactOccurred('light');
}
async function sendChatMessage(text) {
if (chatBusy) return;
chatBusy = true;
document.getElementById('chatChips').style.display = 'none';
const messagesEl = document.getElementById('chatMessages');
// User message
const userDiv = document.createElement('div');
userDiv.className = 'msg msg-user';
userDiv.textContent = text;
messagesEl.appendChild(userDiv);
// Typing indicator
const typingDiv = document.createElement('div');
typingDiv.className = 'msg-typing';
typingDiv.innerHTML = '<span></span><span></span><span></span>';
messagesEl.appendChild(typingDiv);
messagesEl.scrollTop = messagesEl.scrollHeight;
chatHistory.push({ role: 'user', content: text });
try {
const lang = (tg.initDataUnsafe?.user?.language_code || 'en').substring(0, 2);
const resp = await fetch(API_BASE + '/api/v1/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + authToken
},
body: JSON.stringify({ message: text, lang: lang })
});
if (!resp.ok) {
let errMsg = 'HTTP ' + resp.status;
try { const ej = await resp.json(); errMsg = ej.error || ej.response || errMsg; } catch(_) {}
throw new Error(errMsg);
}
const data = await resp.json();
typingDiv.remove();
const botDiv = document.createElement('div');
botDiv.className = 'msg msg-bot';
botDiv.innerHTML = formatResponse(data.response || 'No response');
messagesEl.appendChild(botDiv);
chatHistory.push({ role: 'assistant', content: data.response || '' });
if (tg.HapticFeedback) tg.HapticFeedback.notificationOccurred('success');
} catch (e) {
typingDiv.remove();
const errDiv = document.createElement('div');
errDiv.className = 'msg msg-bot';
errDiv.textContent = t('error') + e.message;
messagesEl.appendChild(errDiv);
}
chatBusy = false;
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function formatResponse(text) {
// Handle SHOWMAP markers → buttons
text = text.replace(/\{\{SHOWMAP~([^~]+)~([^~]+)~([^~]+)~([^}]+)\}\}/g,
(_, lat, lon, zoom, label) => {
return `<button onclick="flyToMap(${lat},${lon},${zoom})" style="display:inline-block;padding:6px 12px;margin:4px 0;background:var(--btn);color:var(--btn-text);border:none;border-radius:8px;font-size:12px;cursor:pointer">${label} &#x1F5FA;</button>`;
});
// Basic markdown
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
text = text.replace(/\n/g, '<br>');
// Code blocks
text = text.replace(/```([\s\S]*?)```/g, '<pre>$1</pre>');
text = text.replace(/`([^`]+)`/g, '<code style="background:rgba(0,0,0,0.3);padding:2px 4px;border-radius:4px">$1</code>');
return text;
}
function flyToMap(lat, lon, zoom) {
switchTab('map');
if (leafletMap) {
leafletMap.setView([lat, lon], zoom || 10);
setTimeout(loadMapVessels, 500);
}
}
// =============================================================================
// Profile
// =============================================================================
async function loadProfile() {
if (!authToken) return;
try {
const [profResp, walResp] = await Promise.all([
fetch(API_BASE + '/api/v1/profile', { headers: { 'Authorization': 'Bearer ' + authToken } }),
fetch(API_BASE + '/api/v1/wallet', { headers: { 'Authorization': 'Bearer ' + authToken } }).catch(() => null),
]);
const profData = await profResp.json();
if (profData.success && profData.profile) {
const p = profData.profile;
const name = currentUser?.name || p.company_name || 'User';
document.getElementById('profAvatar').textContent = name.charAt(0).toUpperCase();
document.getElementById('profName').textContent = name;
document.getElementById('profRole').textContent = p.role || t('member');
document.getElementById('profRoutes').textContent =
(p.trade_routes && p.trade_routes.length) ? p.trade_routes.join(', ') : '—';
document.getElementById('profCargo').textContent =
(p.cargo_types && p.cargo_types.length) ? p.cargo_types.map(c => c.replace(/_/g,' ')).join(', ') : '—';
}
if (walResp) {
const walData = await walResp.json();
if (walData.success) {
document.getElementById('profBalance').textContent = '$' + (parseFloat(walData.balance) || 0).toFixed(2);
}
}
// Load watches
loadWatches();
} catch (e) { console.error('Profile load error:', e); }
}
async function loadWatches() {
// Watches are Telegram-specific, loaded via chat command
// For now show static message
}
function openFullSite() {
tg.openLink('https://seafare.efir.org');
}
// =============================================================================
// Auto-resize textarea
// =============================================================================
document.addEventListener('DOMContentLoaded', function() {
const ta = document.getElementById('chatInput');
if (ta) {
ta.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 100) + 'px';
});
}
});
// =============================================================================
// Start
// =============================================================================
init().catch(function(e) {
console.error("Init error:", e);
var ls = document.getElementById("loadingScreen");
if (ls) ls.style.display = "none";
var nl = document.getElementById("notLinkedScreen");
if (nl) nl.style.display = "flex";
});
</script>
</body>
</html>