sozday-pezu-obuenie-vaybkodi/vibemap.html

748 lines
30 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="theme-color" content="#0066CC">
<meta name="description" content="VibeMap — карта студентов и проектов Vibe42. Найди единомышленников, поделись проектом.">
<title>VibeMap — карта студентов Vibe42</title>
<!-- PWA -->
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" type="image/png" sizes="192x192" href="icon-192.png">
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="VibeMap">
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>
:root {
--kt-blue: #0066CC;
--kt-red: #E4002B;
--kt-dark: #1a202c;
--kt-light: #f5f7fa;
--text: #1a202c;
--text-muted: #718096;
--bg: #f5f7fa;
--card-bg: #ffffff;
--success: #2a9d8f;
--warning: #f4a261;
--danger: #e63946;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); overflow: hidden; }
/* TOP BAR */
.topbar {
position: fixed; top: 0; left: 0; right: 0;
background: linear-gradient(135deg, var(--kt-blue) 0%, #004c99 100%);
color: white; padding: 12px 16px; z-index: 1000;
display: flex; align-items: center; gap: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
}
.logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 18px; }
.logo-emoji { font-size: 28px; }
.logo-text div:first-child { font-size: 18px; font-weight: 800; }
.logo-text .tagline { font-size: 11px; opacity: 0.9; font-weight: 400; }
.menu-btn {
margin-left: auto; background: rgba(255,255,255,0.15);
border: none; color: white; width: 40px; height: 40px;
border-radius: 10px; cursor: pointer; font-size: 20px;
transition: background 0.3s;
}
.menu-btn:hover { background: rgba(255,255,255,0.25); }
.stats-btn {
background: rgba(255,255,255,0.15); border: none; color: white;
padding: 8px 14px; border-radius: 10px; cursor: pointer;
display: flex; align-items: center; gap: 8px; font-size: 13px;
transition: all 0.3s;
}
.stats-btn:hover { background: rgba(255,255,255,0.25); transform: translateY(-1px); }
.stats-count {
background: var(--kt-red); color: white;
padding: 2px 8px; border-radius: 12px; font-size: 12px;
font-weight: 700;
}
.city-selector {
background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3);
color: white; padding: 8px 12px; border-radius: 8px;
font-size: 13px; cursor: pointer; outline: none;
}
.city-selector option { background: var(--kt-dark); color: white; }
/* FILTER BAR */
.filter-bar {
position: fixed; top: 70px; left: 0; right: 0;
background: white; padding: 12px 16px; z-index: 999;
display: flex; gap: 8px; overflow-x: auto;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.chip {
background: #f0f0f0; border: none; padding: 8px 14px;
border-radius: 20px; font-size: 13px; cursor: pointer;
white-space: nowrap; transition: all 0.3s;
display: flex; align-items: center; gap: 6px;
}
.chip:hover { background: #e0e0e0; }
.chip.active { background: var(--kt-blue); color: white; }
.chip .dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--kt-blue);
}
.chip .dot-vet { background: var(--success); }
.chip .dot-shelter { background: var(--warning); }
.chip .dot-petshop { background: var(--danger); }
/* MAP */
#map { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1; }
/* FAB */
.fab {
position: fixed; bottom: 30px; right: 30px;
width: 64px; height: 64px; border-radius: 50%;
background: linear-gradient(135deg, var(--kt-red) 0%, #c40025 100%);
color: white; border: none; font-size: 32px;
cursor: pointer; box-shadow: 0 4px 20px rgba(228,0,43,0.4);
z-index: 1000; transition: all 0.3s;
}
.fab:hover { transform: scale(1.1); box-shadow: 0 6px 30px rgba(228,0,43,0.5); }
/* AI FAB */
.ai-fab {
position: fixed; bottom: 110px; right: 30px;
width: 52px; height: 52px; border-radius: 50%;
background: linear-gradient(135deg, var(--kt-blue) 0%, #004c99 100%);
color: white; border: none; font-size: 24px;
cursor: pointer; box-shadow: 0 4px 15px rgba(0,102,204,0.3);
z-index: 1000; transition: all 0.3s;
}
.ai-fab:hover { transform: scale(1.05); }
.ai-fab-pulse {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 100%; height: 100%; border-radius: 50%;
background: inherit; animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.7; }
100% { transform: translate(-50%, -50%) scale(1.6); opacity: 0; }
}
/* AI PANEL */
.ai-panel {
position: fixed; bottom: 180px; right: 30px;
width: 340px; max-height: 450px;
background: white; border-radius: 16px;
box-shadow: 0 8px 40px rgba(0,0,0,0.15);
z-index: 1001; display: flex; flex-direction: column;
overflow: hidden;
}
.ai-panel[hidden] { display: none; }
.ai-panel-head {
background: linear-gradient(135deg, var(--kt-blue), #004c99);
color: white; padding: 14px 16px;
display: flex; justify-content: space-between; align-items: center;
}
.ai-head-info { display: flex; align-items: center; gap: 10px; }
.ai-head-avatar { font-size: 24px; }
.ai-head-name { font-weight: 700; font-size: 14px; }
.ai-head-status { font-size: 11px; opacity: 0.9; display: flex; align-items: center; gap: 4px; }
.ai-status-dot { width: 6px; height: 6px; background: #2a9d8f; border-radius: 50%; }
.ai-panel-close {
background: rgba(255,255,255,0.2); border: none;
color: white; width: 28px; height: 28px; border-radius: 50%;
cursor: pointer; font-size: 18px;
}
.ai-messages {
flex: 1; overflow-y: auto; padding: 16px;
display: flex; flex-direction: column; gap: 12px;
background: #f8f9fa;
}
.ai-msg {
max-width: 85%; padding: 12px 16px; border-radius: 14px;
font-size: 14px; line-height: 1.5;
}
.ai-msg.ai {
background: white; align-self: flex-start;
border-bottom-left-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.ai-msg.user {
background: var(--kt-blue); color: white;
align-self: flex-end; border-bottom-right-radius: 4px;
}
.ai-suggestions {
display: flex; gap: 8px; padding: 12px 16px;
overflow-x: auto; background: white;
border-top: 1px solid #e2e8f0;
}
.ai-sug {
background: #f0f4f8; color: var(--kt-blue);
padding: 8px 14px; border-radius: 20px;
font-size: 12px; white-space: nowrap; cursor: pointer;
border: 1px solid transparent; transition: all 0.3s;
}
.ai-sug:hover { border-color: var(--kt-blue); background: white; }
.ai-input-row {
display: flex; gap: 8px; padding: 12px 16px;
background: white; border-top: 1px solid #e2e8f0;
}
.ai-input-row input {
flex: 1; padding: 10px 14px; border: 1px solid #e2e8f0;
border-radius: 24px; font-size: 14px; outline: none;
}
.ai-input-row input:focus { border-color: var(--kt-blue); }
.ai-input-row button {
background: var(--kt-blue); color: white; border: none;
width: 40px; height: 40px; border-radius: 50%;
cursor: pointer; font-size: 16px;
transition: background 0.3s;
}
.ai-input-row button:hover { background: #004c99; }
/* DRAWER */
.drawer { position: fixed; inset: 0; z-index: 2000; }
.drawer[hidden] { display: none; }
.drawer-backdrop {
position: absolute; inset: 0; background: rgba(0,0,0,0.5);
}
.drawer-panel {
position: absolute; left: 0; top: 0; bottom: 0;
width: 300px; background: white;
box-shadow: 4px 0 20px rgba(0,0,0,0.15);
display: flex; flex-direction: column;
}
.drawer-head {
background: linear-gradient(135deg, var(--kt-blue), #004c99);
color: white; padding: 20px 16px;
display: flex; justify-content: space-between; align-items: center;
}
.drawer-logo { font-size: 20px; font-weight: 800; }
.drawer-close {
background: rgba(255,255,255,0.2); border: none;
color: white; width: 32px; height: 32px; border-radius: 50%;
cursor: pointer; font-size: 20px;
}
.drawer-nav { flex: 1; padding: 16px 0; }
.drawer-nav button {
width: 100%; padding: 14px 20px; text-align: left;
background: none; border: none; font-size: 15px;
color: var(--text); cursor: pointer;
display: flex; align-items: center; gap: 12px;
transition: background 0.2s;
}
.drawer-nav button:hover { background: #f0f4f8; }
.drawer-footer {
padding: 20px; border-top: 1px solid #e2e8f0;
font-size: 12px; color: var(--text-muted); text-align: center;
}
/* PREVIEW BOTTOM SHEET */
.preview {
position: fixed; bottom: 0; left: 0; right: 0;
background: white; border-radius: 20px 20px 0 0;
box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
z-index: 1002; transform: translateY(100%);
transition: transform 0.3s;
max-height: 70vh; overflow-y: auto;
}
.preview.open { transform: translateY(0); }
.preview-handle {
width: 40px; height: 4px; background: #cbd5e0;
border-radius: 2px; margin: 12px auto;
}
.preview-content { padding: 0 20px 20px; }
.preview-title { font-size: 20px; font-weight: 700; margin-bottom: 12px; }
.preview-meta { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
.preview-tag {
background: #f0f4f8; color: var(--kt-blue);
padding: 4px 10px; border-radius: 12px; font-size: 12px;
}
.preview-desc { color: var(--text-muted); line-height: 1.6; margin-bottom: 16px; }
.preview-contact {
background: #f0f4f8; padding: 12px; border-radius: 10px;
margin-bottom: 16px;
}
.preview-actions { display: flex; gap: 12px; }
.preview-btn {
flex: 1; padding: 12px; border: none; border-radius: 10px;
font-weight: 600; cursor: pointer; transition: all 0.3s;
}
.preview-btn.primary { background: var(--kt-blue); color: white; }
.preview-btn.primary:hover { background: #004c99; }
.preview-btn.secondary { background: #f0f0f0; color: var(--text); }
.preview-btn.secondary:hover { background: #e0e0e0; }
/* MODAL */
.modal {
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
z-index: 3000; display: none; justify-content: center;
align-items: center; padding: 20px;
}
.modal.open { display: flex; }
.modal-content {
background: white; border-radius: 16px;
max-width: 500px; width: 100%; max-height: 90vh;
overflow-y: auto; padding: 24px;
}
.modal-title { font-size: 22px; font-weight: 700; margin-bottom: 20px; }
.form-group { margin-bottom: 16px; }
.form-group label {
display: block; margin-bottom: 6px; font-weight: 600;
font-size: 14px; color: var(--text-muted);
}
.form-group input, .form-group textarea, .form-group select {
width: 100%; padding: 12px; border: 1px solid #e2e8f0;
border-radius: 10px; font-size: 14px; outline: none;
}
.form-group input:focus, .form-group textarea:focus { border-color: var(--kt-blue); }
.form-actions { display: flex; gap: 12px; margin-top: 20px; }
/* TOAST */
.toast {
position: fixed; bottom: 30px; left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--kt-dark); color: white;
padding: 14px 24px; border-radius: 10px;
z-index: 5000; transition: transform 0.3s;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
}
.toast.show { transform: translateX(-50%) translateY(0); }
/* MASCOT */
.mascot {
position: fixed; bottom: 20px; left: 20px;
width: 100px; height: 100px; z-index: 999;
cursor: pointer; transition: transform 0.3s;
}
.mascot:hover { transform: scale(1.05); }
.mascot-bubble {
position: absolute; bottom: 100px; left: 50%;
transform: translateX(-50%);
background: white; padding: 12px 16px;
border-radius: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.15);
font-size: 13px; white-space: nowrap;
opacity: 0; transition: opacity 0.3s;
pointer-events: none;
}
.mascot:hover .mascot-bubble { opacity: 1; }
/* PWA INSTALL */
.pwa-install {
position: fixed; bottom: 20px; left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, var(--kt-blue), #004c99);
color: white; padding: 12px 20px; border-radius: 30px;
z-index: 998; display: flex; align-items: center; gap: 10px;
border: none; cursor: pointer; box-shadow: 0 4px 15px rgba(0,102,204,0.3);
}
.pwa-close {
background: rgba(255,255,255,0.2); border: none;
color: white; width: 24px; height: 24px; border-radius: 50%;
cursor: pointer; font-size: 14px;
}
@media (max-width: 768px) {
.topbar { padding: 10px 12px; }
.logo-text .tagline { display: none; }
.ai-panel { width: calc(100% - 30px); right: 15px; bottom: 180px; }
.fab { bottom: 20px; right: 20px; width: 56px; height: 56px; }
.ai-fab { bottom: 90px; right: 20px; }
.mascot { width: 80px; height: 80px; bottom: 15px; left: 15px; }
}
</style>
</head>
<body>
<!-- TOP BAR -->
<header class="topbar">
<div class="logo">
<span class="logo-emoji">🚀</span>
<div class="logo-text">
<div>VibeMap</div>
<div class="tagline">Карта студентов Vibe42</div>
</div>
</div>
<button class="stats-btn" id="open-stats">
<span>📊</span>
<span>Проектов</span>
<span class="stats-count" id="project-count">0</span>
</button>
<select class="city-selector" id="city">
<option value="astana">Астана</option>
<option value="almaty">Алматы</option>
<option value="shymkent">Шымкент</option>
<option value="karaganda">Караганда</option>
<option value="aktobe">Актобе</option>
<option value="taraz">Тараз</option>
<option value="pavlodar">Павлодар</option>
<option value="uk">Усть-Каменогорск</option>
<option value="semey">Семей</option>
<option value="kostanay">Костанай</option>
</select>
<button class="menu-btn" id="open-menu" title="Меню">☰</button>
</header>
<!-- FILTER BAR -->
<div class="filter-bar">
<button class="chip chip-all active">Все</button>
<button class="chip" data-cat="website">🌐 Сайты</button>
<button class="chip" data-cat="bot">🤖 Боты</button>
<button class="chip" data-cat="app">📱 Приложения</button>
<button class="chip" data-cat="presentation">📊 Презентации</button>
<button class="chip" data-cat="course">📚 Курсы</button>
</div>
<!-- MAP -->
<div id="map"></div>
<!-- FAB -->
<button class="fab" id="fab" title="Добавить проект">+</button>
<!-- AI FAB -->
<button class="ai-fab" id="ai-fab" title="ИИ-помощник">
<span>💬</span>
<span class="ai-fab-pulse"></span>
</button>
<!-- AI PANEL -->
<div class="ai-panel" id="ai-panel" hidden>
<header class="ai-panel-head">
<div class="ai-head-info">
<span class="ai-head-avatar">🚀</span>
<div>
<div class="ai-head-name">Vibe42 Помощник</div>
<div class="ai-head-status"><span class="ai-status-dot"></span> Онлайн</div>
</div>
</div>
<button class="ai-panel-close" id="ai-panel-close">×</button>
</header>
<div class="ai-messages" id="ai-messages">
<div class="ai-msg ai">
👋 Привет! Я ИИ-помощник Vibe42. Спроси меня о чём угодно:
<br>• Как создать сайт?
<br>• Где найти проекты других?
<br>• Как опубликовать проект?
</div>
</div>
<div class="ai-suggestions" id="ai-suggestions">
<button class="ai-sug" data-sug="Как создать сайт?">Как создать сайт?</button>
<button class="ai-sug" data-sug="Где найти проекты?">Где найти проекты?</button>
<button class="ai-sug" data-sug="Как опубликовать?">Как опубликовать?</button>
</div>
<form class="ai-input-row" id="ai-form">
<input type="text" id="ai-input" placeholder="Спросите что угодно..." autocomplete="off">
<button type="submit">➤</button>
</form>
</div>
<!-- DRAWER -->
<div class="drawer" id="drawer" hidden>
<div class="drawer-backdrop" id="drawer-backdrop"></div>
<aside class="drawer-panel">
<div class="drawer-head">
<div class="drawer-logo">🚀 VibeMap</div>
<button class="drawer-close" id="drawer-close">×</button>
</div>
<nav class="drawer-nav">
<button data-nav="map">🗺️ Карта</button>
<button data-nav="projects">📁 Все проекты</button>
<button data-nav="stats">📊 Статистика</button>
<button data-nav="about">ℹ️ О платформе</button>
<button data-nav="install">📲 Установить</button>
<button data-nav="feedback">💬 Обратная связь</button>
</nav>
<div class="drawer-footer">
<div>VibeMap © 2025</div>
<div>Сделано с ❤️ для студентов Казахстана</div>
</div>
</aside>
</div>
<!-- PREVIEW BOTTOM SHEET -->
<div class="preview" id="preview">
<div class="preview-handle"></div>
<div class="preview-content">
<h3 class="preview-title" id="preview-title">Название проекта</h3>
<div class="preview-meta" id="preview-meta"></div>
<p class="preview-desc" id="preview-desc">Описание проекта</p>
<div class="preview-contact" id="preview-contact"></div>
<div class="preview-actions">
<button class="preview-btn primary" id="preview-open">Открыть проект</button>
<button class="preview-btn secondary" id="preview-close">Закрыть</button>
</div>
</div>
</div>
<!-- MODAL -->
<div class="modal" id="modal">
<div class="modal-content">
<h3 class="modal-title">➕ Добавить проект</h3>
<form id="project-form">
<div class="form-group">
<label>Название проекта</label>
<input type="text" name="title" required placeholder="Мой первый сайт">
</div>
<div class="form-group">
<label>Категория</label>
<select name="category">
<option value="website">🌐 Сайт</option>
<option value="bot">🤖 Telegram-бот</option>
<option value="app">📱 Приложение</option>
<option value="presentation">📊 Презентация</option>
<option value="course">📚 Курс</option>
</select>
</div>
<div class="form-group">
<label>Город</label>
<select name="city">
<option value="astana">Астана</option>
<option value="almaty">Алматы</option>
<option value="shymkent">Шымкент</option>
<option value="karaganda">Караганда</option>
<option value="aktobe">Актобе</option>
</select>
</div>
<div class="form-group">
<label>Ссылка на проект</label>
<input type="url" name="url" placeholder="https://pages.git.vibe42.kz/...">
</div>
<div class="form-group">
<label>Описание</label>
<textarea name="description" rows="4" placeholder="Расскажите о проекте..."></textarea>
</div>
<div class="form-group">
<label>Контакт (Telegram/Email)</label>
<input type="text" name="contact" placeholder="@username или email">
</div>
<div class="form-actions">
<button type="button" class="preview-btn secondary" id="modal-close">Отмена</button>
<button type="submit" class="preview-btn primary">Опубликовать</button>
</div>
</form>
</div>
</div>
<!-- TOAST -->
<div class="toast" id="toast"></div>
<!-- MASCOT -->
<div class="mascot" id="mascot" title="Помощник">
<div class="mascot-bubble" id="mascot-bubble">Нажми + чтобы добавить проект!</div>
<svg viewBox="0 0 100 100">
<defs>
<radialGradient id="mg" cx="0.4" cy="0.4" r="0.7">
<stop offset="0%" stop-color="#0080ff"/>
<stop offset="100%" stop-color="#0066CC"/>
</radialGradient>
</defs>
<circle cx="50" cy="50" r="45" fill="url(#mg)"/>
<circle cx="35" cy="42" r="8" fill="white"/>
<circle cx="65" cy="42" r="8" fill="white"/>
<circle cx="35" cy="42" r="4" fill="#1a202c"/>
<circle cx="65" cy="42" r="4" fill="#1a202c"/>
<circle cx="37" cy="40" r="2" fill="white"/>
<circle cx="67" cy="40" r="2" fill="white"/>
<ellipse cx="50" cy="60" rx="12" ry="8" fill="#fff5e2"/>
<path d="M45 58 Q50 63 55 58" stroke="#1a202c" stroke-width="2" fill="none" stroke-linecap="round"/>
<circle cx="25" cy="55" r="5" fill="#ff6b6b" opacity="0.4"/>
<circle cx="75" cy="55" r="5" fill="#ff6b6b" opacity="0.4"/>
</svg>
</div>
<!-- PWA INSTALL -->
<button class="pwa-install" id="pwa-install" hidden>
<span class="pwa-icon">📲</span>
<span class="pwa-text">Установить как приложение</span>
<span class="pwa-close" id="pwa-install-close">×</span>
</button>
<!-- Leaflet -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ДАННЫЕ ПРОЕКТОВ (мок-данные для примера)
const PROJECTS = [
{ id: 1, title: "Сайт-визитка", category: "website", city: "astana", url: "https://pages.git.vibe42.kz/Medet/site1", description: "Мой первый сайт на Vibe42", contact: "@medet" },
{ id: 2, title: "Telegram-бот", category: "bot", city: "almaty", url: "https://t.me/mybot", description: "Бот для записи на курсы", contact: "@user123" },
{ id: 3, title: "Презентация", category: "presentation", city: "astana", url: "#", description: "Презентация по вайбкодингу", contact: "student@mail.kz" },
{ id: 4, title: "Интернет-магазин", category: "app", city: "shymkent", url: "#", description: "Прототип магазина одежды", contact: "@shop" },
{ id: 5, title: "Курс по HTML", category: "course", city: "karaganda", url: "#", description: "Бесплатный курс для начинающих", contact: "@teacher" },
];
const CITIES = {
astana: { lat: 51.1694, lng: 71.4491, name: "Астана" },
almaty: { lat: 43.2220, lng: 76.8512, name: "Алматы" },
shymkent: { lat: 42.3000, lng: 69.6000, name: "Шымкент" },
karaganda: { lat: 49.8047, lng: 73.1094, name: "Караганда" },
aktobe: { lat: 50.2839, lng: 57.1670, name: "Актобе" },
taraz: { lat: 42.9000, lng: 71.3667, name: "Тараз" },
pavlodar: { lat: 52.2833, lng: 76.9667, name: "Павлодар" },
uk: { lat: 49.9667, lng: 82.6000, name: "Усть-Каменогорск" },
semey: { lat: 50.4111, lng: 80.2275, name: "Семей" },
kostanay: { lat: 53.2144, lng: 63.6246, name: "Костанай" }
};
// Инициализация карты
const map = L.map('map', { zoomControl: false }).setView([49.8, 71.5], 5);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Маркеры
const markers = [];
function addMarkers(filter = 'all') {
markers.forEach(m => map.removeLayer(m));
markers.length = 0;
PROJECTS.forEach(p => {
if (filter !== 'all' && p.category !== filter) return;
const city = CITIES[p.city];
if (!city) return;
const marker = L.marker([city.lat, city.lng]).addTo(map);
marker.bindPopup(`<b>${p.title}</b><br>${p.description}`);
marker.on('click', () => showPreview(p));
markers.push(marker);
});
document.getElementById('project-count').textContent = markers.length;
}
addMarkers();
// Фильтры
document.querySelectorAll('.chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
const filter = chip.dataset.cat || 'all';
addMarkers(filter);
});
});
// Preview
const preview = document.getElementById('preview');
function showPreview(project) {
document.getElementById('preview-title').textContent = project.title;
document.getElementById('preview-meta').innerHTML = `
<span class="preview-tag">${project.category}</span>
<span class="preview-tag">${CITIES[project.city]?.name}</span>
`;
document.getElementById('preview-desc').textContent = project.description;
document.getElementById('preview-contact').textContent = `Контакт: ${project.contact}`;
document.getElementById('preview-open').onclick = () => window.open(project.url, '_blank');
preview.classList.add('open');
}
document.getElementById('preview-close').onclick = () => preview.classList.remove('open');
// Modal
const modal = document.getElementById('modal');
document.getElementById('fab').onclick = () => modal.classList.add('open');
document.getElementById('modal-close').onclick = () => modal.classList.remove('open');
document.getElementById('project-form').onsubmit = (e) => {
e.preventDefault();
showToast('✅ Проект опубликован!');
modal.classList.remove('open');
};
// Drawer
const drawer = document.getElementById('drawer');
document.getElementById('open-menu').onclick = () => drawer.hidden = false;
document.getElementById('drawer-close').onclick = () => drawer.hidden = true;
document.getElementById('drawer-backdrop').onclick = () => drawer.hidden = true;
// AI Panel
const aiPanel = document.getElementById('ai-panel');
document.getElementById('ai-fab').onclick = () => aiPanel.hidden = false;
document.getElementById('ai-panel-close').onclick = () => aiPanel.hidden = true;
const aiMessages = document.getElementById('ai-messages');
const aiForm = document.getElementById('ai-form');
const aiInput = document.getElementById('ai-input');
aiForm.onsubmit = (e) => {
e.preventDefault();
const msg = aiInput.value.trim();
if (!msg) return;
aiMessages.innerHTML += `<div class="ai-msg user">${msg}</div>`;
aiInput.value = '';
setTimeout(() => {
const responses = {
'Как создать сайт?': '📝 Открой Vibe42, создай index.html в папке проекта. Используй design.md как основу!',
'Где найти проекты?': '🗺️ Смотри на карту! Фильтры сверху помогут найти нужную категорию.',
'Как опубликовать?': '🚀 Нажми кнопку "Опубликовать" в интерфейсе Vibe42. Ссылка будет: pages.git.vibe42.kz/...'
};
const answer = responses[msg] || 'Спасибо за вопрос! Я учусь. Попробуй спросить что-то из предложенных вариантов.';
aiMessages.innerHTML += `<div class="ai-msg ai">${answer}</div>`;
aiMessages.scrollTop = aiMessages.scrollHeight;
}, 500);
};
document.querySelectorAll('.ai-sug').forEach(btn => {
btn.onclick = () => {
aiInput.value = btn.dataset.sug;
aiForm.requestSubmit();
};
});
// Mascot
const mascot = document.getElementById('mascot');
const mascotBubble = document.getElementById('mascot-bubble');
let clickCount = 0;
mascot.onclick = () => {
clickCount++;
const messages = ['Нажми + чтобы добавить проект!', 'Исследуй карту!', 'Vibe42 — это круто! 🚀', 'Создавай, публикуй, вдохновляй!'];
mascotBubble.textContent = messages[clickCount % messages.length];
};
// Toast
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
// PWA Install
const installBtn = document.getElementById('pwa-install');
let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
installBtn.hidden = false;
});
installBtn.onclick = async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
await deferredPrompt.userChoice;
deferredPrompt = null;
installBtn.hidden = true;
}
};
document.getElementById('pwa-install-close').onclick = () => {
installBtn.hidden = true;
};
// City selector
document.getElementById('city').onchange = (e) => {
const city = CITIES[e.target.value];
if (city) map.flyTo([city.lat, city.lng], 11);
};
// Stats button
document.getElementById('open-stats').onclick = () => {
showToast(`📊 Всего проектов: ${PROJECTS.length}`);
};
</script>
</body>
</html>