d85da0198c
const COLORS и let _deckColor объявлены в temporal dead zone во время вызова init() из IIFE (const не hoisting, function — да). Перемещены перед IIFE: теперь COLORS инициализирован до первого вызова buildColorPicker(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
861 lines
42 KiB
HTML
861 lines
42 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Флэш-карточки — LearnSpace</title>
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||
<link rel="stylesheet" href="/css/ls.css" />
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
||
<style>
|
||
.sb-content { background: #f4f5f8; min-height: 100vh; }
|
||
.fc-wrap { max-width: 1100px; margin: 0 auto; padding: 28px 28px 80px; }
|
||
|
||
/* ── header ── */
|
||
.fc-header { display: flex; align-items: center; gap: 14px; margin-bottom: 28px; }
|
||
.fc-back { display: none; background: none; border: 1.5px solid var(--border);
|
||
border-radius: 8px; padding: 6px 12px; cursor: pointer; font-size: .82rem;
|
||
color: var(--text-2); transition: .15s; }
|
||
.fc-back:hover { background: var(--surface-2); }
|
||
.fc-back.visible { display: flex; align-items: center; gap: 6px; }
|
||
.fc-title { font-family: 'Unbounded', sans-serif; font-size: 1.25rem; font-weight: 800;
|
||
color: var(--text); flex: 1; }
|
||
.fc-btn { padding: 8px 16px; border-radius: 9px; border: none; cursor: pointer;
|
||
font-family: 'Manrope', sans-serif; font-size: .82rem; font-weight: 700;
|
||
transition: .15s; }
|
||
.fc-btn-primary { background: var(--violet); color: #fff; }
|
||
.fc-btn-primary:hover { filter: brightness(1.12); }
|
||
.fc-btn-ghost { background: var(--surface); border: 1.5px solid var(--border); color: var(--text-2); }
|
||
.fc-btn-ghost:hover { background: var(--surface-2); }
|
||
.fc-btn-danger { background: #FEE2E2; border: 1.5px solid #FECACA; color: #DC2626; }
|
||
.fc-btn-danger:hover { background: #FECACA; }
|
||
|
||
/* ── stats bar ── */
|
||
.fc-stats { display: flex; gap: 12px; margin-bottom: 24px; flex-wrap: wrap; }
|
||
.fc-stat { background: #fff; border: 1.5px solid var(--border); border-radius: 12px;
|
||
padding: 12px 18px; display: flex; flex-direction: column; gap: 2px; min-width: 110px; }
|
||
.fc-stat-val { font-family: 'Unbounded', sans-serif; font-size: 1.4rem; font-weight: 800; color: var(--violet); }
|
||
.fc-stat-lbl { font-size: .72rem; font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: .04em; }
|
||
|
||
/* ── deck grid ── */
|
||
.deck-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
|
||
.deck-card { background: #fff; border: 1.5px solid var(--border); border-radius: 16px;
|
||
overflow: hidden; cursor: pointer; transition: box-shadow .18s, transform .18s;
|
||
display: flex; flex-direction: column; }
|
||
.deck-card:hover { box-shadow: 0 6px 24px rgba(0,0,0,.1); transform: translateY(-2px); }
|
||
.deck-stripe { height: 6px; }
|
||
.deck-body { padding: 16px 18px 14px; flex: 1; }
|
||
.deck-name { font-family: 'Manrope', sans-serif; font-weight: 700; font-size: .96rem;
|
||
color: var(--text); margin-bottom: 5px; }
|
||
.deck-desc { font-size: .78rem; color: var(--text-3); margin-bottom: 12px;
|
||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.deck-meta { display: flex; gap: 8px; align-items: center; margin-bottom: 14px; }
|
||
.deck-badge { padding: 3px 9px; border-radius: 20px; font-size: .7rem; font-weight: 700;
|
||
background: var(--surface-2); color: var(--text-2); }
|
||
.deck-badge.due { background: #FEF3C7; color: #D97706; }
|
||
.deck-badge.zero { background: #DCFCE7; color: #16A34A; }
|
||
.deck-actions { display: flex; gap: 8px; padding: 0 18px 14px; }
|
||
.deck-btn-study { flex: 1; padding: 7px; border-radius: 8px; border: none; cursor: pointer;
|
||
background: var(--violet); color: #fff; font-family: 'Manrope', sans-serif;
|
||
font-size: .8rem; font-weight: 700; transition: .15s; }
|
||
.deck-btn-study:hover { filter: brightness(1.1); }
|
||
.deck-btn-study:disabled { background: var(--surface-2); color: var(--text-3); cursor: default; filter: none; }
|
||
.deck-btn-edit { padding: 7px 12px; border-radius: 8px; border: 1.5px solid var(--border);
|
||
cursor: pointer; background: none; color: var(--text-2);
|
||
font-size: .8rem; font-weight: 700; font-family: 'Manrope', sans-serif; transition: .15s; }
|
||
.deck-btn-edit:hover { background: var(--surface-2); }
|
||
|
||
/* new deck card */
|
||
.deck-add { border: 2px dashed var(--border); border-radius: 16px; min-height: 140px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
cursor: pointer; transition: .15s; flex-direction: column; gap: 8px;
|
||
color: var(--text-3); font-size: .84rem; font-weight: 600; }
|
||
.deck-add:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,.04); }
|
||
.deck-add svg { opacity: .5; }
|
||
|
||
/* ── card list (deck detail) ── */
|
||
#view-cards { display: none; }
|
||
.card-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px; }
|
||
.card-item { background: #fff; border: 1.5px solid var(--border); border-radius: 12px;
|
||
display: flex; gap: 0; overflow: hidden; }
|
||
.card-item.editing { border-color: var(--violet); }
|
||
.card-side { flex: 1; padding: 12px 14px; min-width: 0; }
|
||
.card-divider { width: 1px; background: var(--border); flex-shrink: 0; }
|
||
.card-side-lbl { font-size: .68rem; font-weight: 700; text-transform: uppercase;
|
||
letter-spacing: .06em; color: var(--text-3); margin-bottom: 5px; }
|
||
.card-text { font-size: .88rem; color: var(--text); white-space: pre-wrap; }
|
||
.card-textarea { width: 100%; border: none; outline: none; resize: none; background: transparent;
|
||
font-family: 'Manrope', sans-serif; font-size: .88rem; color: var(--text);
|
||
min-height: 48px; line-height: 1.5; padding: 0; }
|
||
.card-actions { display: flex; flex-direction: column; gap: 0; border-left: 1px solid var(--border); }
|
||
.card-act-btn { padding: 0 12px; height: 100%; flex: 1; border: none; background: none;
|
||
cursor: pointer; color: var(--text-3); transition: .15s; display: flex;
|
||
align-items: center; justify-content: center; }
|
||
.card-act-btn:hover { background: var(--surface-2); color: var(--text); }
|
||
.card-act-btn.del:hover { background: #FEE2E2; color: #DC2626; }
|
||
|
||
.card-add-bar { display: flex; gap: 10px; align-items: center; }
|
||
.card-add-input { flex: 1; padding: 10px 14px; border: 1.5px solid var(--border); border-radius: 10px;
|
||
font-family: 'Manrope', sans-serif; font-size: .88rem; background: #fff;
|
||
color: var(--text); outline: none; transition: .15s; }
|
||
.card-add-input:focus { border-color: var(--violet); }
|
||
|
||
/* ── study mode ── */
|
||
#view-study { display: none; }
|
||
.study-wrap { max-width: 600px; margin: 0 auto; }
|
||
.study-progress-bar { height: 5px; background: var(--surface-2); border-radius: 3px;
|
||
margin-bottom: 22px; overflow: hidden; }
|
||
.study-progress-fill { height: 100%; background: var(--violet); border-radius: 3px;
|
||
transition: width .35s ease; }
|
||
.study-counter { text-align: center; font-size: .8rem; color: var(--text-3);
|
||
font-weight: 600; margin-bottom: 18px; }
|
||
|
||
/* card */
|
||
.study-card-scene { perspective: 1000px; height: 260px; margin-bottom: 22px; cursor: pointer;
|
||
user-select: none; -webkit-user-select: none; }
|
||
.study-card-inner { width: 100%; height: 100%; position: relative; transition: transform .5s cubic-bezier(.4,0,.2,1);
|
||
transform-style: preserve-3d; will-change: transform; }
|
||
.study-card-inner.flipped { transform: rotateY(180deg); }
|
||
.study-card-inner.swipe-right { animation: swipeRight .4s ease forwards; }
|
||
.study-card-inner.swipe-left { animation: swipeLeft .4s ease forwards; }
|
||
@keyframes swipeRight { to { transform: translateX(110%) rotate(20deg) rotateY(0deg); opacity: 0; } }
|
||
@keyframes swipeLeft { to { transform: translateX(-110%) rotate(-20deg) rotateY(180deg); opacity: 0; } }
|
||
|
||
.study-face { position: absolute; inset: 0; border-radius: 18px; padding: 28px 32px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
backface-visibility: hidden; -webkit-backface-visibility: hidden;
|
||
border: 1.5px solid var(--border); overflow: auto; }
|
||
.study-face-front { background: #fff; box-shadow: 0 4px 24px rgba(0,0,0,.08); }
|
||
.study-face-back { background: #fff; transform: rotateY(180deg);
|
||
box-shadow: 0 4px 24px rgba(0,0,0,.08); }
|
||
.study-face-text { font-family: 'Manrope', sans-serif; font-size: 1.15rem; font-weight: 600;
|
||
color: var(--text); text-align: center; line-height: 1.6; }
|
||
.study-face-label { position: absolute; top: 12px; left: 16px; font-size: .65rem; font-weight: 700;
|
||
text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); }
|
||
.study-hint { text-align: center; font-size: .77rem; color: var(--text-3); margin-bottom: 20px; }
|
||
|
||
/* drag tilt */
|
||
.study-card-inner.drag-right { transform: rotate(6deg) translateX(30px); }
|
||
.study-card-inner.drag-left { transform: rotate(-6deg) translateX(-30px); }
|
||
|
||
/* answer quality buttons */
|
||
.study-btns { display: none; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 24px; }
|
||
.study-btns.visible { display: flex; }
|
||
.sq-btn { padding: 10px 20px; border-radius: 10px; border: 2px solid transparent;
|
||
cursor: pointer; font-family: 'Manrope', sans-serif; font-size: .84rem; font-weight: 700;
|
||
transition: .18s; display: flex; flex-direction: column; align-items: center; gap: 2px; }
|
||
.sq-btn .sq-days { font-size: .66rem; font-weight: 600; opacity: .65; }
|
||
.sq-btn-again { background: #FEE2E2; border-color: #FECACA; color: #DC2626; }
|
||
.sq-btn-again:hover { background: #FECACA; }
|
||
.sq-btn-hard { background: #FEF3C7; border-color: #FDE68A; color: #D97706; }
|
||
.sq-btn-hard:hover { background: #FDE68A; }
|
||
.sq-btn-good { background: #DBEAFE; border-color: #BFDBFE; color: #2563EB; }
|
||
.sq-btn-good:hover { background: #BFDBFE; }
|
||
.sq-btn-easy { background: #DCFCE7; border-color: #BBF7D0; color: #16A34A; }
|
||
.sq-btn-easy:hover { background: #BBF7D0; }
|
||
|
||
/* swipe indicator */
|
||
.swipe-indicator { position: absolute; top: 18px; font-size: 1.4rem; font-weight: 900;
|
||
letter-spacing: .04em; padding: 5px 14px; border-radius: 8px;
|
||
opacity: 0; pointer-events: none; transition: opacity .1s; z-index: 10; }
|
||
.swipe-right-ind { right: 20px; background: #DCFCE7; color: #16A34A; border: 2px solid #BBF7D0; }
|
||
.swipe-left-ind { left: 20px; background: #FEE2E2; color: #DC2626; border: 2px solid #FECACA; }
|
||
|
||
/* finished state */
|
||
.study-done { text-align: center; padding: 48px 24px; }
|
||
.study-done-icon { font-size: 3rem; margin-bottom: 16px; }
|
||
.study-done h2 { font-family: 'Unbounded', sans-serif; font-size: 1.4rem; font-weight: 800;
|
||
color: var(--text); margin-bottom: 8px; }
|
||
.study-done p { color: var(--text-3); font-size: .88rem; margin-bottom: 24px; }
|
||
.study-session-stats { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-bottom: 28px; }
|
||
.ss-stat { background: #fff; border: 1.5px solid var(--border); border-radius: 12px;
|
||
padding: 12px 20px; text-align: center; }
|
||
.ss-stat-n { font-family: 'Unbounded', sans-serif; font-size: 1.4rem; font-weight: 800; }
|
||
.ss-stat-l { font-size: .72rem; color: var(--text-3); font-weight: 600; }
|
||
|
||
/* ── modals ── */
|
||
.fc-modal { position: fixed; inset: 0; z-index: 300; display: none;
|
||
align-items: center; justify-content: center; padding: 16px; }
|
||
.fc-modal.open { display: flex; }
|
||
.fc-modal-bg { position: absolute; inset: 0; background: rgba(0,0,0,.45); }
|
||
.fc-modal-box { position: relative; z-index: 1; background: #fff; border-radius: 18px;
|
||
padding: 28px; width: 100%; max-width: 480px; box-shadow: 0 20px 60px rgba(0,0,0,.18); }
|
||
.fc-modal-title { font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800;
|
||
color: var(--text); margin-bottom: 20px; }
|
||
.fc-modal-field { margin-bottom: 14px; }
|
||
.fc-modal-label { font-size: .75rem; font-weight: 700; color: var(--text-2);
|
||
text-transform: uppercase; letter-spacing: .05em; margin-bottom: 5px; }
|
||
.fc-modal-input { width: 100%; padding: 9px 12px; border: 1.5px solid var(--border);
|
||
border-radius: 9px; font-family: 'Manrope', sans-serif; font-size: .88rem;
|
||
color: var(--text); outline: none; box-sizing: border-box; transition: .15s; }
|
||
.fc-modal-input:focus { border-color: var(--violet); }
|
||
.fc-modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 22px; }
|
||
.color-picker-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||
.cp-swatch { width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
|
||
border: 3px solid transparent; transition: .15s; }
|
||
.cp-swatch.active, .cp-swatch:hover { border-color: var(--text); transform: scale(1.15); }
|
||
|
||
/* ── empty ── */
|
||
.fc-empty { text-align: center; padding: 60px 24px; }
|
||
.fc-empty-icon { font-size: 3rem; margin-bottom: 14px; }
|
||
.fc-empty h3 { font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800;
|
||
color: var(--text); margin-bottom: 8px; }
|
||
.fc-empty p { color: var(--text-3); font-size: .84rem; margin-bottom: 22px; }
|
||
|
||
@media (max-width: 768px) {
|
||
.fc-wrap { padding: 16px 14px 60px; }
|
||
.fc-title { font-size: 1rem; }
|
||
.fc-stats { gap: 8px; }
|
||
.fc-stat { min-width: 90px; padding: 10px 12px; }
|
||
.fc-stat-val { font-size: 1.1rem; }
|
||
.deck-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
|
||
.study-face { padding: 24px 18px; }
|
||
.sq-btn { padding: 9px 16px; font-size: .8rem; }
|
||
}
|
||
@media (max-width: 480px) {
|
||
.fc-wrap { padding: 12px 10px 60px; }
|
||
.fc-header { flex-wrap: wrap; gap: 10px; }
|
||
.fc-title { font-size: 0.92rem; }
|
||
.deck-grid { grid-template-columns: 1fr; }
|
||
.study-face { padding: 20px 14px; }
|
||
.sq-btn { padding: 8px 14px; font-size: .78rem; flex: 1; justify-content: center; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="app-sidebar"></aside>
|
||
|
||
<main class="sb-content">
|
||
<div class="fc-wrap">
|
||
|
||
<!-- ── DECKS VIEW ── -->
|
||
<div id="view-decks">
|
||
<div class="fc-header">
|
||
<h1 class="fc-title">Флэш-карточки</h1>
|
||
<button class="fc-btn fc-btn-primary" onclick="openNewDeckModal()">+ Новая колода</button>
|
||
</div>
|
||
<div class="fc-stats" id="fc-stats-bar"></div>
|
||
<div class="deck-grid" id="deck-grid">
|
||
<div style="grid-column:1/-1; text-align:center; padding:40px; color:var(--text-3)">Загрузка…</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── CARDS VIEW ── -->
|
||
<div id="view-cards">
|
||
<div class="fc-header">
|
||
<button class="fc-back visible" id="cards-back-btn" onclick="showDecks()">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 12H5M12 5l-7 7 7 7"/></svg>
|
||
Колоды
|
||
</button>
|
||
<h1 class="fc-title" id="cards-deck-title">Название колоды</h1>
|
||
<button class="fc-btn fc-btn-ghost" onclick="openBulkModal()">Добавить список</button>
|
||
<button class="fc-btn fc-btn-primary" id="cards-study-btn" onclick="startStudy()">Начать изучение</button>
|
||
</div>
|
||
<div class="card-list" id="card-list"></div>
|
||
<!-- Add card row -->
|
||
<div class="card-add-bar" style="margin-bottom:14px">
|
||
<input class="card-add-input" id="new-card-front" placeholder="Лицевая сторона (вопрос)…" onkeydown="addCardOnEnter(event)" />
|
||
<input class="card-add-input" id="new-card-back" placeholder="Обратная сторона (ответ)…" onkeydown="addCardOnEnter(event)" />
|
||
<button class="fc-btn fc-btn-primary" onclick="addCard()">+ Добавить</button>
|
||
</div>
|
||
<div style="display:flex;gap:10px;align-items:center">
|
||
<button class="fc-btn fc-btn-ghost" onclick="openEditDeckModal()"><svg class="ic" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>️ Редактировать колоду</button>
|
||
<button class="fc-btn fc-btn-danger" onclick="confirmDeleteDeck()">Удалить колоду</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── STUDY VIEW ── -->
|
||
<div id="view-study">
|
||
<div class="fc-header">
|
||
<button class="fc-back visible" onclick="showCards()">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 12H5M12 5l-7 7 7 7"/></svg>
|
||
Карточки
|
||
</button>
|
||
<h1 class="fc-title" id="study-deck-title">Изучение</h1>
|
||
</div>
|
||
<div class="study-wrap">
|
||
<div class="study-progress-bar"><div class="study-progress-fill" id="study-prog" style="width:0%"></div></div>
|
||
<div class="study-counter" id="study-counter">1 / 10</div>
|
||
|
||
<div class="study-card-scene" id="study-scene" onclick="flipCard()">
|
||
<div class="study-card-inner" id="study-card">
|
||
<div class="study-face study-face-front">
|
||
<span class="study-face-label">Вопрос</span>
|
||
<div class="study-face-text" id="study-front-text"></div>
|
||
</div>
|
||
<div class="study-face study-face-back">
|
||
<span class="study-face-label">Ответ</span>
|
||
<div class="study-face-text" id="study-back-text"></div>
|
||
</div>
|
||
<span class="swipe-indicator swipe-right-ind" id="ind-right">ЗНАЮ <svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></span>
|
||
<span class="swipe-indicator swipe-left-ind" id="ind-left">ЕЩЁ РАЗ <svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span>
|
||
</div>
|
||
</div>
|
||
<div class="study-hint" id="study-flip-hint">Нажмите, чтобы увидеть ответ</div>
|
||
|
||
<div class="study-btns" id="study-btns">
|
||
<button class="sq-btn sq-btn-again" onclick="answer(0)">Снова<span class="sq-days" id="sq-days-0"><1 мин</span></button>
|
||
<button class="sq-btn sq-btn-hard" onclick="answer(3)">Трудно<span class="sq-days" id="sq-days-3">—</span></button>
|
||
<button class="sq-btn sq-btn-good" onclick="answer(4)">Знаю<span class="sq-days" id="sq-days-4">—</span></button>
|
||
<button class="sq-btn sq-btn-easy" onclick="answer(5)">Легко<span class="sq-days" id="sq-days-5">—</span></button>
|
||
</div>
|
||
|
||
<!-- done screen -->
|
||
<div class="study-done" id="study-done" style="display:none">
|
||
<div class="study-done-icon"><svg class="ic" viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/></svg></div>
|
||
<h2>Сессия завершена!</h2>
|
||
<p id="study-done-sub">Хорошая работа — вы просмотрели все карточки</p>
|
||
<div class="study-session-stats" id="study-session-stats"></div>
|
||
<div style="display:flex;gap:12px;justify-content:center">
|
||
<button class="fc-btn fc-btn-primary" onclick="startStudy()">Повторить ещё раз</button>
|
||
<button class="fc-btn fc-btn-ghost" onclick="showCards()">Вернуться</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- ── New / Edit Deck Modal ── -->
|
||
<div class="fc-modal" id="modal-deck">
|
||
<div class="fc-modal-bg" onclick="closeModal('modal-deck')"></div>
|
||
<div class="fc-modal-box">
|
||
<div class="fc-modal-title" id="modal-deck-title">Новая колода</div>
|
||
<div class="fc-modal-field">
|
||
<div class="fc-modal-label">Название</div>
|
||
<input class="fc-modal-input" id="modal-deck-name" placeholder="Биология — клетка" maxlength="80" />
|
||
</div>
|
||
<div class="fc-modal-field">
|
||
<div class="fc-modal-label">Описание (необязательно)</div>
|
||
<input class="fc-modal-input" id="modal-deck-desc" placeholder="Краткое описание…" maxlength="200" />
|
||
</div>
|
||
<div class="fc-modal-field">
|
||
<div class="fc-modal-label">Цвет</div>
|
||
<div class="color-picker-row" id="color-picker">
|
||
<!-- filled by JS -->
|
||
</div>
|
||
</div>
|
||
<div class="fc-modal-actions">
|
||
<button class="fc-btn fc-btn-ghost" onclick="closeModal('modal-deck')">Отмена</button>
|
||
<button class="fc-btn fc-btn-primary" onclick="saveDeckModal()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Bulk add Modal ── -->
|
||
<div class="fc-modal" id="modal-bulk">
|
||
<div class="fc-modal-bg" onclick="closeModal('modal-bulk')"></div>
|
||
<div class="fc-modal-box">
|
||
<div class="fc-modal-title">Добавить список карточек</div>
|
||
<div class="fc-modal-field">
|
||
<div class="fc-modal-label">Формат: одна карточка = одна строка, вопрос и ответ разделены «—» или «|»</div>
|
||
<textarea class="fc-modal-input" id="bulk-text" rows="10" style="resize:vertical"
|
||
placeholder="Митохондрия — органелл клетки, производит АТФ Ядро | содержит ДНК Рибосома — синтез белка"></textarea>
|
||
</div>
|
||
<div class="fc-modal-actions">
|
||
<button class="fc-btn fc-btn-ghost" onclick="closeModal('modal-bulk')">Отмена</button>
|
||
<button class="fc-btn fc-btn-primary" onclick="saveBulk()">Добавить</button>
|
||
</div>
|
||
</div>
|
||
</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/notifications.js"></script>
|
||
<script src="/js/search.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||
<script>
|
||
/* ════ Constants & State ════ */
|
||
const COLORS = ['#9B5DE5','#EF476F','#FF9F1C','#06D6E0','#22d399','#3B82F6','#F15BB5','#6B7280'];
|
||
|
||
let _decks = [];
|
||
let _curDeck = null;
|
||
let _cards = [];
|
||
let _editingDeckId = null;
|
||
let _deckColor = '#9B5DE5';
|
||
|
||
(async () => {
|
||
/* ── auth ── */
|
||
const { user } = LS.initPage();
|
||
if (!user) return;
|
||
const avatarEl = document.getElementById('nav-avatar');
|
||
const nameEl = document.getElementById('nav-user');
|
||
LS.renderNavAvatar(avatarEl, user);
|
||
if (nameEl) nameEl.textContent = user.name || '';
|
||
LS.showBoardIfAllowed();
|
||
if (user.role!=='student') { document.getElementById('btn-classes')?.style && (document.getElementById('btn-classes').style.display='flex'); }
|
||
if (user.role==='admin') { document.getElementById('btn-admin')?.style && (document.getElementById('btn-admin').style.display='flex'); }
|
||
|
||
if (localStorage.getItem('ls_sb_collapsed') === '1')
|
||
document.querySelector('.app-layout')?.classList.add('sb-collapsed');
|
||
|
||
lucide.createIcons();
|
||
init();
|
||
})();
|
||
|
||
/* ════ Init ════ */
|
||
async function init() {
|
||
buildColorPicker();
|
||
await loadDecks();
|
||
}
|
||
|
||
async function loadDecks() {
|
||
const [decks, stats] = await Promise.all([
|
||
LS.api('/flashcards/decks').catch(()=>({decks:[]})),
|
||
LS.api('/flashcards/stats').catch(()=>null),
|
||
]);
|
||
_decks = decks.decks || [];
|
||
renderStats(stats);
|
||
renderDecks();
|
||
}
|
||
|
||
/* ════ Sidebar toggle ════ */
|
||
function toggleSidebar() {
|
||
const l = document.querySelector('.app-layout');
|
||
l.classList.toggle('sb-collapsed');
|
||
localStorage.setItem('ls_sb_collapsed', l.classList.contains('sb-collapsed') ? '1' : '0');
|
||
}
|
||
|
||
/* ════ Stats bar ════ */
|
||
function renderStats(s) {
|
||
if (!s) return;
|
||
const bar = document.getElementById('fc-stats-bar');
|
||
bar.innerHTML = [
|
||
{ val: s.decks_count, lbl: 'Колод', col: '#9B5DE5' },
|
||
{ val: s.cards_count, lbl: 'Карточек', col: '#3B82F6' },
|
||
{ val: s.due_count, lbl: 'К повторению', col: '#D97706' },
|
||
{ val: s.reviewed_today, lbl: 'Сегодня', col: '#16A34A' },
|
||
].map(s => `<div class="fc-stat">
|
||
<span class="fc-stat-val" style="color:${s.col}">${s.val}</span>
|
||
<span class="fc-stat-lbl">${s.lbl}</span>
|
||
</div>`).join('');
|
||
}
|
||
|
||
/* ════ Deck grid ════ */
|
||
function renderDecks() {
|
||
const grid = document.getElementById('deck-grid');
|
||
if (!_decks.length) {
|
||
grid.innerHTML = `<div class="fc-empty" style="grid-column:1/-1">
|
||
<div class="fc-empty-icon">🃏</div>
|
||
<h3>Нет колод</h3>
|
||
<p>Создайте первую колоду карточек</p>
|
||
<button class="fc-btn fc-btn-primary" onclick="openNewDeckModal()">+ Создать колоду</button>
|
||
</div>`;
|
||
return;
|
||
}
|
||
grid.innerHTML = _decks.map(d => {
|
||
const due = d.due_count;
|
||
const dueHtml = due > 0
|
||
? `<span class="deck-badge due"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> ${due} к повторению</span>`
|
||
: `<span class="deck-badge zero"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg> Актуально</span>`;
|
||
return `<div class="deck-card">
|
||
<div class="deck-stripe" style="background:${d.color}"></div>
|
||
<div class="deck-body" onclick="openDeck(${d.id})">
|
||
<div class="deck-name">${esc(d.title)}</div>
|
||
${d.description ? `<div class="deck-desc">${esc(d.description)}</div>` : ''}
|
||
<div class="deck-meta">
|
||
<span class="deck-badge"><svg class="ic" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg> ${d.card_count} карточек</span>
|
||
${dueHtml}
|
||
</div>
|
||
</div>
|
||
<div class="deck-actions">
|
||
<button class="deck-btn-study" ${due===0&&d.card_count>0?'':''}
|
||
onclick="openDeckStudy(${d.id})" ${d.card_count===0?'disabled':''}>
|
||
${due > 0 ? `<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Повторить (${due})` : d.card_count > 0 ? '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Изучать' : 'Нет карточек'}
|
||
</button>
|
||
<button class="deck-btn-edit" onclick="openDeck(${d.id})"><svg class="ic" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>️</button>
|
||
</div>
|
||
</div>`;
|
||
}).join('') + `<div class="deck-add" onclick="openNewDeckModal()">
|
||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>
|
||
Новая колода
|
||
</div>`;
|
||
}
|
||
|
||
/* ════ Open deck (card editor) ════ */
|
||
async function openDeck(id) {
|
||
_curDeck = _decks.find(d => d.id === id);
|
||
if (!_curDeck) return;
|
||
document.getElementById('cards-deck-title').textContent = _curDeck.title;
|
||
const data = await LS.api(`/flashcards/decks/${id}/cards`).catch(()=>({cards:[]}));
|
||
_cards = data.cards || [];
|
||
renderCardList();
|
||
document.getElementById('view-decks').style.display = 'none';
|
||
document.getElementById('view-cards').style.display = 'block';
|
||
document.getElementById('view-study').style.display = 'none';
|
||
}
|
||
|
||
function openDeckStudy(id) {
|
||
_curDeck = _decks.find(d => d.id === id);
|
||
if (!_curDeck) return;
|
||
startStudyForDeck(id);
|
||
}
|
||
|
||
function showDecks() {
|
||
document.getElementById('view-decks').style.display = 'block';
|
||
document.getElementById('view-cards').style.display = 'none';
|
||
document.getElementById('view-study').style.display = 'none';
|
||
loadDecks();
|
||
}
|
||
|
||
function showCards() {
|
||
document.getElementById('view-decks').style.display = 'none';
|
||
document.getElementById('view-cards').style.display = 'block';
|
||
document.getElementById('view-study').style.display = 'none';
|
||
}
|
||
|
||
/* ════ Card list ════ */
|
||
function renderCardList() {
|
||
const list = document.getElementById('card-list');
|
||
if (!_cards.length) {
|
||
list.innerHTML = `<div class="fc-empty" style="padding:30px 0">
|
||
<div class="fc-empty-icon"><svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></div>
|
||
<h3>Нет карточек</h3>
|
||
<p>Добавьте первую карточку ниже</p>
|
||
</div>`;
|
||
return;
|
||
}
|
||
list.innerHTML = _cards.map((c, i) => `
|
||
<div class="card-item" id="ci-${c.id}">
|
||
<div class="card-side">
|
||
<div class="card-side-lbl">Вопрос</div>
|
||
<textarea class="card-textarea" rows="2"
|
||
onchange="saveCard(${c.id},'front',this.value)">${esc(c.front)}</textarea>
|
||
</div>
|
||
<div class="card-divider"></div>
|
||
<div class="card-side">
|
||
<div class="card-side-lbl">Ответ</div>
|
||
<textarea class="card-textarea" rows="2"
|
||
onchange="saveCard(${c.id},'back',this.value)">${esc(c.back)}</textarea>
|
||
</div>
|
||
<div class="card-actions">
|
||
<button class="card-act-btn del" onclick="deleteCard(${c.id})" title="Удалить">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
async function addCard() {
|
||
if (!_curDeck) return;
|
||
const front = document.getElementById('new-card-front').value.trim();
|
||
const back = document.getElementById('new-card-back').value.trim();
|
||
if (!front && !back) return;
|
||
const card = await LS.api(`/flashcards/decks/${_curDeck.id}/cards`, {
|
||
method: 'POST', body: JSON.stringify({ front, back })
|
||
}).catch(()=>null);
|
||
if (!card) return;
|
||
_cards.push(card);
|
||
document.getElementById('new-card-front').value = '';
|
||
document.getElementById('new-card-back').value = '';
|
||
document.getElementById('new-card-front').focus();
|
||
renderCardList();
|
||
}
|
||
|
||
function addCardOnEnter(e) {
|
||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); addCard(); }
|
||
}
|
||
|
||
async function saveCard(id, field, value) {
|
||
const card = _cards.find(c => c.id === id);
|
||
if (!card) return;
|
||
card[field] = value;
|
||
await LS.api(`/flashcards/cards/${id}`, {
|
||
method: 'PUT', body: JSON.stringify({ [field]: value })
|
||
}).catch(()=>{});
|
||
}
|
||
|
||
async function deleteCard(id) {
|
||
if (!confirm('Удалить карточку?')) return;
|
||
await LS.api(`/flashcards/cards/${id}`, { method: 'DELETE' }).catch(()=>{});
|
||
_cards = _cards.filter(c => c.id !== id);
|
||
renderCardList();
|
||
}
|
||
|
||
/* ════ Bulk add ════ */
|
||
function openBulkModal() {
|
||
document.getElementById('bulk-text').value = '';
|
||
document.getElementById('modal-bulk').classList.add('open');
|
||
}
|
||
|
||
async function saveBulk() {
|
||
const text = document.getElementById('bulk-text').value.trim();
|
||
if (!text || !_curDeck) return;
|
||
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
|
||
const cards = lines.map(l => {
|
||
const sep = l.includes('—') ? '—' : '|';
|
||
const [front, ...rest] = l.split(sep);
|
||
return { front: (front||'').trim(), back: rest.join(sep).trim() };
|
||
}).filter(c => c.front);
|
||
if (!cards.length) return;
|
||
const result = await LS.api(`/flashcards/decks/${_curDeck.id}/cards/bulk`, {
|
||
method: 'POST', body: JSON.stringify({ cards })
|
||
}).catch(()=>null);
|
||
if (result?.inserted) {
|
||
_cards.push(...result.inserted);
|
||
renderCardList();
|
||
}
|
||
closeModal('modal-bulk');
|
||
}
|
||
|
||
/* ════ Study mode ════ */
|
||
let _studyCards = [];
|
||
let _studyIdx = 0;
|
||
let _studyFlipped = false;
|
||
let _sessionStats = { again: 0, hard: 0, good: 0, easy: 0 };
|
||
|
||
async function startStudy() {
|
||
if (!_curDeck) return;
|
||
await startStudyForDeck(_curDeck.id);
|
||
}
|
||
|
||
async function startStudyForDeck(deckId) {
|
||
_curDeck = _curDeck || _decks.find(d => d.id === deckId);
|
||
if (!_curDeck) return;
|
||
const data = await LS.api(`/flashcards/decks/${deckId}/study`).catch(()=>null);
|
||
if (!data || !data.cards?.length) {
|
||
LS.toast('Нет карточек для повторения — всё актуально!', 'success');
|
||
return;
|
||
}
|
||
_studyCards = data.cards;
|
||
_studyIdx = 0;
|
||
_studyFlipped = false;
|
||
_sessionStats = { again: 0, hard: 0, good: 0, easy: 0 };
|
||
document.getElementById('study-deck-title').textContent = _curDeck.title;
|
||
document.getElementById('study-done').style.display = 'none';
|
||
document.getElementById('study-scene').style.display = 'block';
|
||
document.getElementById('study-flip-hint').style.display = 'block';
|
||
document.getElementById('view-decks').style.display = 'none';
|
||
document.getElementById('view-cards').style.display = 'none';
|
||
document.getElementById('view-study').style.display = 'block';
|
||
showStudyCard();
|
||
bindSwipe();
|
||
}
|
||
|
||
const _FC_DELIMS = [
|
||
{ left: '\\(', right: '\\)', display: false },
|
||
{ left: '\\[', right: '\\]', display: true },
|
||
{ left: '$', right: '$', display: false },
|
||
];
|
||
function mathHtmlFC(text) {
|
||
if (!text) return '';
|
||
const tmp = document.createElement('span');
|
||
tmp.textContent = text;
|
||
if (window.renderMathInElement) {
|
||
try { renderMathInElement(tmp, { delimiters: _FC_DELIMS, throwOnError: false }); } catch {}
|
||
}
|
||
return tmp.innerHTML;
|
||
}
|
||
|
||
function showStudyCard() {
|
||
const card = _studyCards[_studyIdx];
|
||
if (!card) { finishStudy(); return; }
|
||
const el = document.getElementById('study-card');
|
||
el.className = 'study-card-inner';
|
||
document.getElementById('study-front-text').innerHTML = mathHtmlFC(card.front);
|
||
document.getElementById('study-back-text').innerHTML = mathHtmlFC(card.back);
|
||
_studyFlipped = false;
|
||
document.getElementById('study-btns').classList.remove('visible');
|
||
document.getElementById('study-flip-hint').style.display = 'block';
|
||
document.getElementById('ind-right').style.opacity = '0';
|
||
document.getElementById('ind-left').style.opacity = '0';
|
||
updateStudyProgress();
|
||
updateSQDays(card);
|
||
}
|
||
|
||
function updateStudyProgress() {
|
||
const total = _studyCards.length;
|
||
const done = _studyIdx;
|
||
document.getElementById('study-prog').style.width = (done / total * 100) + '%';
|
||
document.getElementById('study-counter').textContent = `${done + 1} / ${total}`;
|
||
}
|
||
|
||
function flipCard() {
|
||
if (_studyFlipped) return;
|
||
_studyFlipped = true;
|
||
document.getElementById('study-card').classList.add('flipped');
|
||
document.getElementById('study-btns').classList.add('visible');
|
||
document.getElementById('study-flip-hint').style.display = 'none';
|
||
}
|
||
|
||
async function answer(quality) {
|
||
const card = _studyCards[_studyIdx];
|
||
if (!card) return;
|
||
// track session stats
|
||
if (quality === 0) _sessionStats.again++;
|
||
else if (quality === 3) _sessionStats.hard++;
|
||
else if (quality === 4) _sessionStats.good++;
|
||
else if (quality === 5) _sessionStats.easy++;
|
||
// send review
|
||
await LS.api(`/flashcards/cards/${card.id}/review`, {
|
||
method: 'POST', body: JSON.stringify({ quality })
|
||
}).catch(()=>{});
|
||
// animate swipe
|
||
const el = document.getElementById('study-card');
|
||
el.classList.add(quality >= 3 ? 'swipe-right' : 'swipe-left');
|
||
setTimeout(() => {
|
||
_studyIdx++;
|
||
if (_studyIdx >= _studyCards.length) finishStudy();
|
||
else showStudyCard();
|
||
}, 380);
|
||
}
|
||
|
||
function finishStudy() {
|
||
document.getElementById('study-scene').style.display = 'none';
|
||
document.getElementById('study-btns').classList.remove('visible');
|
||
document.getElementById('study-flip-hint').style.display = 'none';
|
||
document.getElementById('study-done').style.display = 'block';
|
||
const s = _sessionStats;
|
||
const total = s.again + s.hard + s.good + s.easy;
|
||
document.getElementById('study-done-sub').textContent =
|
||
`Просмотрено ${total} карточек`;
|
||
document.getElementById('study-session-stats').innerHTML = [
|
||
{ n: s.again, l: 'Снова', c: '#DC2626' },
|
||
{ n: s.hard, l: 'Трудно', c: '#D97706' },
|
||
{ n: s.good, l: 'Знаю', c: '#2563EB' },
|
||
{ n: s.easy, l: 'Легко', c: '#16A34A' },
|
||
].map(x => `<div class="ss-stat">
|
||
<div class="ss-stat-n" style="color:${x.c}">${x.n}</div>
|
||
<div class="ss-stat-l">${x.l}</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
/* ── estimated next interval preview for sq buttons ── */
|
||
function updateSQDays(card) {
|
||
const ef = card.ease_factor || 2.5;
|
||
const iv = card.interval_days || 1;
|
||
const rep = card.repetitions || 0;
|
||
const preview = (q) => {
|
||
if (q < 3) return '<1 мин';
|
||
let niv;
|
||
if (rep === 0) niv = 1;
|
||
else if (rep === 1) niv = 6;
|
||
else niv = Math.round(iv * ef);
|
||
const nef = Math.max(1.3, ef + 0.1 - (5 - q) * (0.08 + (5 - q) * 0.02));
|
||
let n2 = (q === 3 ? Math.max(1, niv - 2) : q === 4 ? niv : Math.round(niv * nef));
|
||
return n2 <= 1 ? '1 день' : n2 + ' дн.';
|
||
};
|
||
document.getElementById('sq-days-0').textContent = '<1 мин';
|
||
document.getElementById('sq-days-3').textContent = preview(3);
|
||
document.getElementById('sq-days-4').textContent = preview(4);
|
||
document.getElementById('sq-days-5').textContent = preview(5);
|
||
}
|
||
|
||
/* ── touch/mouse swipe ── */
|
||
function bindSwipe() {
|
||
const scene = document.getElementById('study-scene');
|
||
let startX = null, curX = null;
|
||
const THRESHOLD = 80;
|
||
|
||
const onStart = e => { startX = (e.touches ? e.touches[0] : e).clientX; curX = startX; };
|
||
const onMove = e => {
|
||
if (startX === null) return;
|
||
curX = (e.touches ? e.touches[0] : e).clientX;
|
||
const dx = curX - startX;
|
||
const card = document.getElementById('study-card');
|
||
if (Math.abs(dx) < 10) return;
|
||
card.style.transform = `rotate(${dx * 0.04}deg) translateX(${dx * 0.3}px)`;
|
||
document.getElementById('ind-right').style.opacity = dx > 30 ? Math.min(1, (dx - 30) / 50) : '0';
|
||
document.getElementById('ind-left').style.opacity = dx < -30 ? Math.min(1, (-dx - 30) / 50) : '0';
|
||
if (e.cancelable) e.preventDefault();
|
||
};
|
||
const onEnd = () => {
|
||
if (startX === null) return;
|
||
const dx = curX - startX;
|
||
const card = document.getElementById('study-card');
|
||
card.style.transform = '';
|
||
if (dx > THRESHOLD && _studyFlipped) answer(5);
|
||
else if (dx < -THRESHOLD) { if (!_studyFlipped) flipCard(); else answer(0); }
|
||
startX = null; curX = null;
|
||
document.getElementById('ind-right').style.opacity = '0';
|
||
document.getElementById('ind-left').style.opacity = '0';
|
||
};
|
||
scene.addEventListener('mousedown', onStart);
|
||
scene.addEventListener('mousemove', onMove);
|
||
scene.addEventListener('mouseup', onEnd);
|
||
scene.addEventListener('mouseleave', onEnd);
|
||
scene.addEventListener('touchstart', onStart, { passive: true });
|
||
scene.addEventListener('touchmove', onMove, { passive: false });
|
||
scene.addEventListener('touchend', onEnd);
|
||
}
|
||
|
||
/* ════ Deck modals ════ */
|
||
function buildColorPicker() {
|
||
document.getElementById('color-picker').innerHTML = COLORS.map(c =>
|
||
`<div class="cp-swatch${c===_deckColor?' active':''}" style="background:${c}"
|
||
onclick="selectColor('${c}',this)" data-color="${c}"></div>`
|
||
).join('');
|
||
}
|
||
|
||
function selectColor(color, el) {
|
||
_deckColor = color;
|
||
document.querySelectorAll('.cp-swatch').forEach(s => s.classList.remove('active'));
|
||
el.classList.add('active');
|
||
}
|
||
|
||
function openNewDeckModal() {
|
||
_editingDeckId = null;
|
||
document.getElementById('modal-deck-title').textContent = 'Новая колода';
|
||
document.getElementById('modal-deck-name').value = '';
|
||
document.getElementById('modal-deck-desc').value = '';
|
||
_deckColor = '#9B5DE5';
|
||
buildColorPicker();
|
||
document.getElementById('modal-deck').classList.add('open');
|
||
setTimeout(() => document.getElementById('modal-deck-name').focus(), 50);
|
||
}
|
||
|
||
function openEditDeckModal() {
|
||
if (!_curDeck) return;
|
||
_editingDeckId = _curDeck.id;
|
||
document.getElementById('modal-deck-title').textContent = 'Редактировать колоду';
|
||
document.getElementById('modal-deck-name').value = _curDeck.title;
|
||
document.getElementById('modal-deck-desc').value = _curDeck.description || '';
|
||
_deckColor = _curDeck.color || '#9B5DE5';
|
||
buildColorPicker();
|
||
document.getElementById('modal-deck').classList.add('open');
|
||
}
|
||
|
||
async function saveDeckModal() {
|
||
const title = document.getElementById('modal-deck-name').value.trim();
|
||
const desc = document.getElementById('modal-deck-desc').value.trim();
|
||
if (!title) { document.getElementById('modal-deck-name').focus(); return; }
|
||
if (_editingDeckId) {
|
||
await LS.api(`/flashcards/decks/${_editingDeckId}`, {
|
||
method: 'PUT', body: JSON.stringify({ title, description: desc, color: _deckColor })
|
||
}).catch(()=>{});
|
||
_curDeck.title = title; _curDeck.description = desc; _curDeck.color = _deckColor;
|
||
document.getElementById('cards-deck-title').textContent = title;
|
||
const d = _decks.find(x => x.id === _editingDeckId);
|
||
if (d) { d.title = title; d.description = desc; d.color = _deckColor; }
|
||
} else {
|
||
const deck = await LS.api('/flashcards/decks', {
|
||
method: 'POST', body: JSON.stringify({ title, description: desc, color: _deckColor })
|
||
}).catch(()=>null);
|
||
if (deck) { _decks.unshift(deck); renderDecks(); }
|
||
}
|
||
closeModal('modal-deck');
|
||
}
|
||
|
||
async function confirmDeleteDeck() {
|
||
if (!_curDeck) return;
|
||
if (!await LS.confirm(`Удалить колоду «${_curDeck.title}» и все карточки?`, { title: 'Удаление колоды', confirmText: 'Удалить', danger: true })) return;
|
||
await LS.api(`/flashcards/decks/${_curDeck.id}`, { method: 'DELETE' }).catch(()=>{});
|
||
_decks = _decks.filter(d => d.id !== _curDeck.id);
|
||
_curDeck = null; _cards = [];
|
||
showDecks();
|
||
}
|
||
|
||
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
||
|
||
|
||
</script>
|
||
<script src="/js/mobile.js"></script>
|
||
</body>
|
||
</html>
|