edb4c211a0
- Add js/sidebar.js: generates full sidebar HTML into #app-sidebar, handles role-based visibility, active link (with prefix matching), toggle wiring, collapsed state, board/features/notif init - Replace <aside class="sidebar">...</aside> with <aside id="app-sidebar"> across all 35 standard-layout pages via scripts/apply-sidebar.js - Add notifications.js to 5 pages that were missing it - Fix api.js initPage(): skip toggle re-wiring if data-sb-wired set, fix active link selector .sb-item → .sb-link - Remove stale sbl-*/nav-admin/btn-upload-nav getElementById calls that crashed after sidebar replacement (lab, classes, collection, crossword, hangman, knowledge-map, library, pet, profile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
491 lines
25 KiB
HTML
491 lines
25 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Моя коллекция — Красная книга РБ</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
|
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;600;700;900&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
|
<link rel="stylesheet" href="/css/ls.css"/>
|
|
<style>
|
|
:root {
|
|
--rb-bg: #0a1a0d;
|
|
--rb-surface: #111d13;
|
|
--rb-border: #1e3523;
|
|
--rb-cr: #ef4444;
|
|
--rb-en: #f97316;
|
|
--rb-vu: #eab308;
|
|
--rb-nt: #22c55e;
|
|
--rb-lc: #3b82f6;
|
|
--rb-accent: #4ade80;
|
|
--rb-text: #e2f5e8;
|
|
--rb-muted: #6b9a74;
|
|
--sb-width: 240px;
|
|
}
|
|
body { background: var(--rb-bg); color: var(--rb-text); font-family: 'Manrope', sans-serif; }
|
|
.app-layout { background: var(--rb-bg); }
|
|
.sb-content { padding: 0; }
|
|
|
|
/* Sidebar overrides */
|
|
.app-layout > .sidebar { background: var(--rb-surface) !important; border-right: 1px solid var(--rb-border) !important; }
|
|
.sb-brand { border-bottom: 1px solid var(--rb-border); padding: 16px 12px 12px; }
|
|
.sb-link, a.sb-link { color: var(--rb-muted) !important; }
|
|
.sb-link:hover { background: rgba(74,222,128,.08) !important; color: var(--rb-text) !important; }
|
|
.sb-link.active { background: rgba(74,222,128,.14) !important; color: var(--rb-accent) !important; font-weight: 700; }
|
|
.sb-link.active::before { background: var(--rb-accent) !important; }
|
|
.sb-toggle { color: var(--rb-muted) !important; border-color: var(--rb-border) !important; background: transparent !important; }
|
|
.sb-toggle:hover { background: rgba(74,222,128,.12) !important; color: var(--rb-accent) !important; }
|
|
.nav-user-chip { border-color: var(--rb-border) !important; }
|
|
.nav-avatar { background: linear-gradient(135deg, #166534, #15803d) !important; }
|
|
.nav-user-name { color: var(--rb-muted) !important; }
|
|
.rb-brand-link { display:flex; align-items:center; gap:9px; text-decoration:none; flex:1; min-width:0; overflow:hidden; }
|
|
.rb-brand-icon { font-size:24px; line-height:1; flex-shrink:0; }
|
|
.rb-brand-text { font-family:'Unbounded',sans-serif; font-size:10px; font-weight:700; color:var(--rb-accent); line-height:1.4; white-space:nowrap; }
|
|
.rb-sb-section { font-size:9px; font-weight:800; letter-spacing:2px; text-transform:uppercase; color:var(--rb-muted); padding:14px 12px 4px; margin:0; opacity:.55; }
|
|
.app-layout.sb-collapsed .rb-sb-section { display:none; }
|
|
.rb-sb-divider { border:none; border-top:1px solid var(--rb-border); margin:8px 4px; }
|
|
.rb-back-link { opacity:.65; font-size:.82rem !important; }
|
|
.rb-back-link:hover { opacity:1 !important; }
|
|
|
|
/* Page layout */
|
|
.col-page { padding: 40px 40px 80px; max-width: 1300px; margin: 0 auto; }
|
|
|
|
/* Header */
|
|
.col-header {
|
|
display: flex; align-items: flex-end; gap: 20px;
|
|
margin-bottom: 32px; flex-wrap: wrap;
|
|
}
|
|
.col-title { font-family: 'Unbounded', sans-serif; font-size: 28px; font-weight: 900; color: #fff; margin: 0; }
|
|
.col-subtitle { font-size: 13px; color: var(--rb-muted); margin: 6px 0 0; }
|
|
.col-actions { margin-left: auto; display: flex; gap: 10px; }
|
|
.btn-rb-outline {
|
|
display: inline-flex; align-items: center; gap: 8px;
|
|
background: transparent; color: var(--rb-accent);
|
|
font-weight: 600; font-size: 13px; padding: 10px 18px;
|
|
border: 1.5px solid var(--rb-accent); border-radius: 12px; cursor: pointer;
|
|
text-decoration: none; transition: all .2s;
|
|
}
|
|
.btn-rb-outline:hover { background: rgba(74,222,128,.1); }
|
|
|
|
/* Progress overview */
|
|
.progress-overview {
|
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 16px; margin-bottom: 36px;
|
|
}
|
|
.prog-card {
|
|
background: var(--rb-surface); border: 1px solid var(--rb-border);
|
|
border-radius: 14px; padding: 18px 20px;
|
|
}
|
|
.prog-card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
.prog-icon { font-size: 22px; }
|
|
.prog-name { font-size: 13px; font-weight: 700; color: var(--rb-text); }
|
|
.prog-count { font-size: 11px; color: var(--rb-muted); margin-top: 2px; }
|
|
.prog-bar { height: 6px; background: var(--rb-border); border-radius: 3px; overflow: hidden; }
|
|
.prog-fill { height: 100%; background: linear-gradient(90deg, #22c55e, #4ade80); border-radius: 3px; transition: width .6s; }
|
|
.prog-pct { font-size: 20px; font-weight: 800; color: var(--rb-accent); margin-top: 8px; }
|
|
|
|
/* Filters */
|
|
.filters-bar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; align-items: center; }
|
|
.filter-chip {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--rb-surface); border: 1px solid var(--rb-border);
|
|
color: var(--rb-muted); font-size: 12px; font-weight: 600;
|
|
padding: 6px 14px; border-radius: 20px; cursor: pointer; transition: all .15s;
|
|
}
|
|
.filter-chip:hover { border-color: var(--rb-accent); color: var(--rb-text); }
|
|
.filter-chip.active { background: rgba(74,222,128,.15); border-color: var(--rb-accent); color: var(--rb-accent); }
|
|
.search-wrap { position: relative; flex: 1; min-width: 180px; }
|
|
.search-wrap input {
|
|
width: 100%; background: var(--rb-surface); border: 1px solid var(--rb-border);
|
|
color: var(--rb-text); font-size: 13px; padding: 8px 12px 8px 36px;
|
|
border-radius: 20px; outline: none; font-family: inherit;
|
|
}
|
|
.search-wrap input:focus { border-color: var(--rb-accent); }
|
|
.search-wrap input::placeholder { color: var(--rb-muted); }
|
|
.search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--rb-muted); font-size: 14px; }
|
|
|
|
/* Section header */
|
|
.section-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
|
|
.section-header h2 { font-family: 'Unbounded', sans-serif; font-size: 18px; font-weight: 700; margin: 0; }
|
|
.section-header .count { background: var(--rb-border); color: var(--rb-muted); font-size: 12px; font-weight: 700; padding: 2px 10px; border-radius: 10px; }
|
|
|
|
/* Grid */
|
|
.species-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
.species-card {
|
|
background: var(--rb-surface); border: 1px solid var(--rb-border);
|
|
border-radius: 14px; overflow: hidden; cursor: pointer;
|
|
transition: all .2s; position: relative;
|
|
animation: cardIn .35s both;
|
|
animation-delay: calc(var(--i, 0) * 35ms);
|
|
}
|
|
@keyframes cardIn {
|
|
from { opacity: 0; transform: translateY(14px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.species-card:hover { transform: translateY(-4px); border-color: var(--rb-accent); box-shadow: 0 8px 24px rgba(0,0,0,.4); }
|
|
.card-photo {
|
|
width: 100%; aspect-ratio: 1;
|
|
background: #1a2e1d;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 48px; position: relative; overflow: hidden;
|
|
}
|
|
.card-photo img { width: 100%; height: 100%; object-fit: cover; }
|
|
.card-body { padding: 12px; }
|
|
.card-name { font-size: 13px; font-weight: 700; color: var(--rb-text); margin: 0 0 2px; line-height: 1.3; }
|
|
.card-lat { font-size: 10px; color: var(--rb-muted); font-style: italic; margin: 0 0 8px; }
|
|
.card-footer { display: flex; align-items: center; justify-content: space-between; }
|
|
.cat-badge { font-size: 10px; font-weight: 800; letter-spacing: .5px; padding: 2px 8px; border-radius: 8px; }
|
|
.cat-badge.CR { background: rgba(239,68,68,.2); color: var(--rb-cr); }
|
|
.cat-badge.EN { background: rgba(249,115,22,.2); color: var(--rb-en); }
|
|
.cat-badge.VU { background: rgba(234,179,8,.2); color: var(--rb-vu); }
|
|
.cat-badge.NT { background: rgba(34,197,94,.2); color: var(--rb-nt); }
|
|
.cat-badge.LC { background: rgba(59,130,246,.2); color: var(--rb-lc); }
|
|
.card-date { font-size: 10px; color: var(--rb-muted); margin-top: 4px; }
|
|
.card-method {
|
|
display: inline-block; font-size: 9px; font-weight: 700; letter-spacing: .5px;
|
|
text-transform: uppercase;
|
|
background: rgba(74,222,128,.1); color: var(--rb-accent); padding: 2px 7px; border-radius: 6px; margin-top: 4px;
|
|
}
|
|
|
|
/* Empty state */
|
|
.rb-empty {
|
|
grid-column: 1/-1; text-align: center; padding: 80px 20px; color: var(--rb-muted);
|
|
}
|
|
.rb-empty .empty-icon { font-size: 56px; margin-bottom: 16px; }
|
|
.rb-empty h3 { font-size: 18px; font-weight: 700; margin: 0 0 8px; color: var(--rb-text); }
|
|
.rb-empty p { font-size: 14px; margin: 0 0 20px; }
|
|
|
|
/* Achievement chips */
|
|
.achievement-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 36px; }
|
|
.ach-chip {
|
|
display: flex; align-items: center; gap: 8px;
|
|
background: var(--rb-surface); border: 1px solid var(--rb-border);
|
|
border-radius: 12px; padding: 12px 16px;
|
|
font-size: 13px; font-weight: 600; color: var(--rb-text);
|
|
}
|
|
.ach-chip.unlocked { border-color: rgba(74,222,128,.4); background: rgba(74,222,128,.08); }
|
|
.ach-icon { font-size: 20px; }
|
|
.ach-label { color: var(--rb-muted); font-size: 11px; display: block; margin-top: 1px; }
|
|
|
|
/* Toasts */
|
|
#rb-toasts {
|
|
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
|
display: flex; flex-direction: column; gap: 10px; pointer-events: none;
|
|
}
|
|
.rb-toast {
|
|
display: flex; align-items: center; gap: 10px;
|
|
background: #0d2210; border: 1px solid var(--rb-border);
|
|
color: var(--rb-text); font-size: 13px; font-weight: 600;
|
|
padding: 12px 18px; border-radius: 12px;
|
|
box-shadow: 0 8px 24px rgba(0,0,0,.5);
|
|
pointer-events: auto;
|
|
animation: toastIn .3s both;
|
|
}
|
|
.rb-toast.success { border-color: rgba(74,222,128,.4); }
|
|
.rb-toast.error { border-color: rgba(239,68,68,.4); color: #fca5a5; }
|
|
@keyframes toastIn { from { opacity:0; transform:translateX(20px); } to { opacity:1; transform:translateX(0); } }
|
|
@keyframes toastOut { from { opacity:1; transform:translateX(0); } to { opacity:0; transform:translateX(20px); } }
|
|
|
|
/* ── Mobile ── */
|
|
@media (max-width: 768px) {
|
|
.sb-content { padding: 0; overflow-y: auto; }
|
|
#rb-toasts { right: 14px; bottom: 80px; }
|
|
}
|
|
@media (max-width: 480px) {
|
|
.ach-chip { padding: 9px 12px; font-size: 12px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout" id="app">
|
|
|
|
<!-- RB Sidebar -->
|
|
<nav class="sidebar">
|
|
<div class="sb-brand">
|
|
<a href="/red-book.html" class="rb-brand-link">
|
|
<span class="rb-brand-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg></span>
|
|
<span class="sb-lbl rb-brand-text">Красная<br>книга РБ</span>
|
|
</a>
|
|
<button class="sb-toggle" onclick="toggleSidebar()" title="Свернуть меню"><i data-lucide="panel-left-close"></i></button>
|
|
</div>
|
|
<nav class="sb-nav">
|
|
<p class="rb-sb-section">РАЗДЕЛЫ</p>
|
|
<a href="/red-book.html" class="sb-link"><i data-lucide="leaf" class="sb-icon"></i><span class="sb-lbl">Каталог видов</span></a>
|
|
<a href="/collection-rb.html" class="sb-link active"><i data-lucide="star" class="sb-icon"></i><span class="sb-lbl">Моя коллекция</span></a>
|
|
<a href="/red-book-ecosystem.html" class="sb-link"><i data-lucide="git-fork" class="sb-icon"></i><span class="sb-lbl">Пищевые сети</span></a>
|
|
<a href="/red-book-biomes.html" class="sb-link"><i data-lucide="trees" class="sb-icon"></i><span class="sb-lbl">Биомы</span></a>
|
|
<a href="/red-book-games.html" class="sb-link"><i data-lucide="gamepad-2" class="sb-icon"></i><span class="sb-lbl">Игры</span></a>
|
|
<hr class="rb-sb-divider">
|
|
<a href="/dashboard.html" class="sb-link rb-back-link"><i data-lucide="chevron-left" class="sb-icon"></i><span class="sb-lbl">Назад</span></a>
|
|
<a href="/profile.html" class="sb-link"><i data-lucide="user" class="sb-icon"></i><span class="sb-lbl">Профиль</span></a>
|
|
</nav>
|
|
<div class="sb-footer">
|
|
<div class="nav-user-chip" onclick="location.href='/profile.html'">
|
|
<div class="nav-avatar" id="nav-avatar">LS</div>
|
|
<span class="nav-user-name" id="nav-user">—</span>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="sb-content">
|
|
<div class="col-page">
|
|
|
|
<!-- Header -->
|
|
<div class="col-header">
|
|
<div>
|
|
<h1 class="col-title">Моя коллекция</h1>
|
|
<p class="col-subtitle">Открытые виды Красной книги Беларуси</p>
|
|
</div>
|
|
<div class="col-actions">
|
|
<a href="/red-book.html" class="btn-rb-outline"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Исследовать виды</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Group progress cards -->
|
|
<div class="progress-overview" id="progress-overview">
|
|
<!-- filled by JS -->
|
|
</div>
|
|
|
|
<!-- Achievements -->
|
|
<div class="achievement-row" id="achievement-row">
|
|
<!-- filled by JS -->
|
|
</div>
|
|
|
|
<!-- Filter bar -->
|
|
<div class="filters-bar" id="cat-filter">
|
|
<div class="filter-chip active" data-cat="" onclick="setCat('')">Все</div>
|
|
<div class="filter-chip" data-cat="CR" onclick="setCat('CR')" style="color:var(--rb-cr);border-color:rgba(239,68,68,.3)">CR</div>
|
|
<div class="filter-chip" data-cat="EN" onclick="setCat('EN')" style="color:var(--rb-en);border-color:rgba(249,115,22,.3)">EN</div>
|
|
<div class="filter-chip" data-cat="VU" onclick="setCat('VU')" style="color:var(--rb-vu);border-color:rgba(234,179,8,.3)">VU</div>
|
|
<div class="filter-chip" data-cat="NT" onclick="setCat('NT')" style="color:var(--rb-nt);border-color:rgba(34,197,94,.3)">NT</div>
|
|
<div class="search-wrap">
|
|
<span class="search-icon"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
|
<input type="text" id="search-input" placeholder="Поиск в коллекции..." oninput="debouncedSearch()"/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section header -->
|
|
<div class="section-header">
|
|
<h2>Открытые виды</h2>
|
|
<span class="count" id="col-count">—</span>
|
|
</div>
|
|
|
|
<!-- Grid -->
|
|
<div class="species-grid" id="col-grid">
|
|
<!-- filled by JS -->
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<div id="rb-toasts"></div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/sidebar.js"></script>
|
|
<script src="/js/mobile.js"></script>
|
|
<script>
|
|
/* ── State ── */
|
|
let all = [];
|
|
let filter = { cat: '', q: '' };
|
|
|
|
const METHOD_LABELS = {
|
|
explore: '<svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Исследование',
|
|
quest: '<svg class="ic" viewBox="0 0 24 24"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg> Квест',
|
|
sighting:'<svg class="ic" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> Наблюдение',
|
|
daily: '⭐ Вид дня',
|
|
};
|
|
|
|
const ACHIEVEMENTS = [
|
|
{ id: 'first', label: 'Первый шаг', desc: 'Открыт первый вид', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M7 20h10"/><path d="M10 20c5.5-2.5.8-6.4 3-10"/><path d="M9.5 9.4c1.1.8 1.8 2.2 2.3 3.7-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2 2.8-.5 4.4 0 5.5.8z"/><path d="M14.1 6a7 7 0 0 1 1.5 4.7c-1.7 1.3-3 1.4-4 1.1a3 3 0 0 1-1.8-1.9c.8-2 2.3-3.3 4.3-3.9z"/></svg>', need: 1 },
|
|
{ id: 'five', label: 'Пять видов', desc: 'Открыто 5 видов', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>', need: 5 },
|
|
{ id: 'ten', label: 'Десятка', desc: 'Открыто 10 видов', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M11.1 7.1a16.55 16.55 0 0 1 10.9 4"/><path d="M12 12 5.78 17.43A1 1 0 0 0 6.95 19"/><path d="M12.12 12a16.55 16.55 0 0 1-5.2 11.14"/><path d="M12 12 7.7 4.5A1 1 0 0 0 6.02 5.1"/><path d="M12 12 3.88 10.28A1 1 0 0 0 3.6 12.01"/><path d="M12 12v9a1 1 0 0 0 1.8.6"/></svg>', need: 10 },
|
|
{ id: 'quarter', label: 'Четверть', desc: 'Открыто 25% каталога', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M17 14 12 3 7 14"/><path d="M4 20 8 11h8l4 9"/><line x1="12" y1="20" x2="12" y2="22"/></svg>', need: null, pct: 25 },
|
|
{ id: 'half', label: 'Половина', desc: 'Открыто 50% каталога', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M17 14 12 3 7 14"/><path d="M4 20 8 11h8l4 9"/><line x1="12" y1="20" x2="12" y2="22"/></svg>', need: null, pct: 50 },
|
|
{ id: 'complete', label: 'Полная книга', desc: 'Открыты все виды', icon: '<svg class="ic" viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>', need: null, pct: 100 },
|
|
];
|
|
|
|
/* ── Init ── */
|
|
async function init() {
|
|
LS.hideDisabledFeatures?.();
|
|
lucide.createIcons();
|
|
|
|
const user = LS.getUser?.() || null;
|
|
if (user) {
|
|
document.getElementById('nav-user').textContent = user.name?.split(' ')[0] || 'Профиль';
|
|
const initials = (user.name||'LS').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'LS';
|
|
document.getElementById('nav-avatar').textContent = initials;
|
|
}
|
|
if (localStorage.getItem('ls_sb_collapsed') === '1') {
|
|
document.getElementById('app').classList.add('sb-collapsed');
|
|
}
|
|
|
|
const [colRes, groupsRes, statsRes] = await Promise.all([
|
|
LS.get('/api/red-book/collection').catch(() => ({ total: 0, species: [] })),
|
|
LS.get('/api/red-book/groups').catch(() => []),
|
|
LS.get('/api/red-book/stats').catch(() => ({})),
|
|
]);
|
|
|
|
all = colRes.species || [];
|
|
renderProgressCards(groupsRes, all, statsRes);
|
|
renderAchievements(all, statsRes);
|
|
renderGrid();
|
|
}
|
|
|
|
/* ── Progress cards by group ── */
|
|
function renderProgressCards(groups, collected, stats) {
|
|
const el = document.getElementById('progress-overview');
|
|
// Total
|
|
const total = stats.total || 1;
|
|
const colCnt = collected.length;
|
|
const pct = Math.round(colCnt / total * 100);
|
|
|
|
let html = `<div class="prog-card" style="grid-column:span 2;border-color:rgba(74,222,128,.3)">
|
|
<div class="prog-card-header">
|
|
<span class="prog-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></span>
|
|
<div><div class="prog-name">Вся Красная книга</div><div class="prog-count">${colCnt} из ${total} видов</div></div>
|
|
</div>
|
|
<div class="prog-bar"><div class="prog-fill" style="width:${pct}%"></div></div>
|
|
<div class="prog-pct">${pct}%</div>
|
|
</div>`;
|
|
|
|
// Per group
|
|
const byGroup = {};
|
|
collected.forEach(s => {
|
|
const g = s.icon || '?';
|
|
if (!byGroup[g]) byGroup[g] = { count: 0, icon: s.icon, name: '' };
|
|
byGroup[g].count++;
|
|
});
|
|
|
|
groups.forEach(g => {
|
|
const cnt = collected.filter(s => s.color === g.color).length;
|
|
const gpct = g.n ? Math.round(cnt / g.n * 100) : 0;
|
|
html += `<div class="prog-card">
|
|
<div class="prog-card-header">
|
|
<span class="prog-icon">${g.icon}</span>
|
|
<div><div class="prog-name">${g.name_ru}</div><div class="prog-count">${cnt} из ${g.n}</div></div>
|
|
</div>
|
|
<div class="prog-bar"><div class="prog-fill" style="width:${gpct}%"></div></div>
|
|
<div class="prog-pct">${gpct}%</div>
|
|
</div>`;
|
|
});
|
|
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
/* ── Achievements ── */
|
|
function renderAchievements(collected, stats) {
|
|
const n = collected.length;
|
|
const total = stats.total || 1;
|
|
const pct = n / total * 100;
|
|
|
|
const html = ACHIEVEMENTS.map(a => {
|
|
const unlocked = a.need ? n >= a.need : pct >= a.pct;
|
|
return `<div class="ach-chip ${unlocked ? 'unlocked' : ''}">
|
|
<span class="ach-icon">${a.icon}</span>
|
|
<div>
|
|
<div style="font-weight:700;font-size:12px;${unlocked?'':'color:var(--rb-muted)'}">${a.label}</div>
|
|
<span class="ach-label">${a.desc}</span>
|
|
</div>
|
|
${unlocked ? '<span style="color:var(--rb-accent);font-size:18px;margin-left:auto"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></span>' : ''}
|
|
</div>`;
|
|
}).join('');
|
|
|
|
document.getElementById('achievement-row').innerHTML = html;
|
|
}
|
|
|
|
/* ── Filters ── */
|
|
function setCat(cat) {
|
|
filter.cat = cat;
|
|
document.querySelectorAll('#cat-filter .filter-chip').forEach(c => {
|
|
c.classList.toggle('active', c.dataset.cat === cat);
|
|
});
|
|
renderGrid();
|
|
}
|
|
let searchTimer;
|
|
function debouncedSearch() {
|
|
clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => {
|
|
filter.q = document.getElementById('search-input').value.trim().toLowerCase();
|
|
renderGrid();
|
|
}, 200);
|
|
}
|
|
|
|
/* ── Render grid ── */
|
|
function renderGrid() {
|
|
const grid = document.getElementById('col-grid');
|
|
let list = all;
|
|
if (filter.cat) list = list.filter(s => s.category === filter.cat);
|
|
if (filter.q) list = list.filter(s => s.name_ru.toLowerCase().includes(filter.q) || (s.name_lat||'').toLowerCase().includes(filter.q));
|
|
|
|
document.getElementById('col-count').textContent = list.length;
|
|
|
|
if (!list.length && !all.length) {
|
|
grid.innerHTML = `<div class="rb-empty">
|
|
<div class="empty-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/><path d="M16 19h6M19 16v6"/></svg></div>
|
|
<h3>Коллекция пуста</h3>
|
|
<p>Откройте виды в каталоге, чтобы они появились здесь</p>
|
|
<a href="/red-book.html" class="btn-rb-outline"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> Исследовать виды</a>
|
|
<a href="/classroom" class="sb-link"><i data-lucide="presentation" class="sb-icon"></i><span class="sb-lbl">Онлайн-урок</span></a>
|
|
<a href="/lesson-history" class="sb-link"><i data-lucide="archive" class="sb-icon"></i><span class="sb-lbl">Архив уроков</span></a>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
if (!list.length) {
|
|
grid.innerHTML = `<div class="rb-empty"><div class="empty-icon"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></div><h3>Ничего не найдено</h3><p>Измените фильтры</p></div>`;
|
|
return;
|
|
}
|
|
|
|
const formatDate = dt => dt ? new Date(dt).toLocaleDateString('ru', { day:'2-digit', month:'short', year:'2-digit' }) : '';
|
|
|
|
grid.innerHTML = list.map((s, i) => `
|
|
<div class="species-card" style="--i:${i%30}" onclick="location.href='/red-book.html?species=${s.id}'">
|
|
<div class="card-photo">
|
|
${s.photo_url ? `<img src="${s.photo_url}" alt="${s.name_ru}" loading="lazy"/>` : `<span style="font-size:48px">${s.icon||'<svg class="ic" viewBox="0 0 24 24"><path d="M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10z"/><path d="M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"/></svg>'}</span>`}
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="card-name">${s.name_ru}</div>
|
|
<div class="card-lat">${s.name_lat || ''}</div>
|
|
<div class="card-footer">
|
|
<span class="cat-badge ${s.category}">${s.category}</span>
|
|
<span style="font-size:16px">${s.icon||''}</span>
|
|
</div>
|
|
<div class="card-date">${formatDate(s.at)}</div>
|
|
${s.method ? `<span class="card-method">${METHOD_LABELS[s.method] || s.method}</span>` : ''}
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
/* ── Sidebar ── */
|
|
function toggleSidebar() {
|
|
const app = document.getElementById('app');
|
|
app.classList.toggle('sb-collapsed');
|
|
localStorage.setItem('ls_sb_collapsed', app.classList.contains('sb-collapsed') ? '1' : '0');
|
|
lucide.createIcons();
|
|
}
|
|
|
|
/* ── Toast ── */
|
|
function showToast(msg, type = 'success') {
|
|
const wrap = document.getElementById('rb-toasts');
|
|
const t = document.createElement('div');
|
|
t.className = `rb-toast ${type}`;
|
|
t.innerHTML = msg;
|
|
wrap.appendChild(t);
|
|
setTimeout(() => {
|
|
t.classList.add('out');
|
|
t.addEventListener('animationend', () => t.remove());
|
|
}, 3000);
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|