Files
verstak/contrib/plugins/calendar/panels/calendar.html
T
mirivlad b80941f908 feat: плагин-система Lua + Calendar reference plugin
- Lua VM runtime: gopher-lua с песочницей, хуки on_init/on_tick/on_shutdown
- API: verstak.node.* / verstak.db.* / verstak.config.* / verstak.state.*
- API: verstak.worklog.* / verstak.activity.* / verstak.file.*
- API: verstak.schedule.* / verstak.http.* / verstak.ui.*
- Менеджер плагинов: жизненный цикл, инициализация, шаблоны
- Scheduler: фоновые задачи с интервалами
- PluginPage.svelte: контейнер для iframe-панелей плагинов
- Calendar plugin: миграция, категории CRUD, события CRUD
- Calendar: расширенный рекарренс (daily/weekly/monthly/yearly)
- Calendar: связь с узлами Верстака, напоминания, HTTP-праздники
- Calendar: Lua-тест-сьют (15 тестов), Go-интеграционный тест
- fix: query_row использует реальные Column() вместо guessColumns
2026-06-07 14:59:46 +08:00

146 lines
5.5 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>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1b1e; color: #e4e4e7; padding: 20px; }
h1 { font-size: 24px; margin-bottom: 16px; color: #fff; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.nav-btn { background: #27272a; border: 1px solid #3f3f46; color: #e4e4e7;
padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; }
.nav-btn:hover { background: #3f3f46; }
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.day-header { text-align: center; font-size: 12px; color: #a1a1aa; padding: 8px 0;
font-weight: 600; text-transform: uppercase; }
.day { aspect-ratio: 1; display: flex; flex-direction: column; align-items: center;
justify-content: center; border-radius: 8px; cursor: pointer; font-size: 14px;
background: #27272a; border: 1px solid #3f3f46; position: relative; }
.day:hover { background: #3f3f46; }
.day.other { opacity: 0.3; }
.day.today { border-color: #3b82f6; background: #1e3a5f; }
.day.has-event::after { content: ''; position: absolute; bottom: 6px; width: 6px; height: 6px;
border-radius: 50%; background: #3b82f6; }
.day.selected { border-color: #8b5cf6; box-shadow: 0 0 0 2px #8b5cf6; }
.events { margin-top: 20px; }
.event { padding: 12px; background: #27272a; border: 1px solid #3f3f46;
border-radius: 8px; margin-bottom: 8px; display: flex; justify-content: space-between; }
.event-title { font-weight: 500; }
.event-time { color: #a1a1aa; font-size: 13px; }
.event-actions { display: flex; gap: 8px; }
.event-actions button { background: none; border: none; color: #a1a1aa;
cursor: pointer; padding: 2px 6px; border-radius: 4px; }
.event-actions button:hover { background: #3f3f46; color: #ef4444; }
</style>
</head>
<body>
<div class="header">
<button class="nav-btn" onclick="prevMonth()"></button>
<h1 id="month-title">Июнь 2025</h1>
<button class="nav-btn" onclick="nextMonth()"></button>
</div>
<div class="calendar-grid" id="calendar"></div>
<div class="events" id="events"></div>
<script>
const MONTHS = ['Январь','Февраль','Март','Апрель','Май','Июнь',
'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
const DAYS = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
let currentDate = new Date();
let selectedDate = null;
let events = [];
function render() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
document.getElementById('month-title').textContent = MONTHS[month] + ' ' + year;
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDay = firstDay.getDay() || 7; // Mon=1, Sun=7
const grid = document.getElementById('calendar');
grid.innerHTML = DAYS.map(d => `<div class="day-header">${d}</div>`).join('');
// Previous month padding
const prevLastDay = new Date(year, month, 0).getDate();
for (let i = startDay - 2; i >= 0; i--) {
grid.innerHTML += `<div class="day other">${prevLastDay - i}</div>`;
}
// Current month
const today = new Date();
for (let d = 1; d <= lastDay.getDate(); d++) {
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const isToday = today.getFullYear() === year && today.getMonth() === month && today.getDate() === d;
const hasEvent = events.some(e => e.date === dateStr);
const isSelected = selectedDate === dateStr;
let cls = 'day';
if (isToday) cls += ' today';
if (hasEvent) cls += ' has-event';
if (isSelected) cls += ' selected';
grid.innerHTML += `<div class="${cls}" onclick="selectDate('${dateStr}')">${d}</div>`;
}
renderEvents();
}
function renderEvents() {
const el = document.getElementById('events');
if (!selectedDate) { el.innerHTML = '<p style="color:#a1a1aa">Выберите дату</p>'; return; }
const dayEvents = events.filter(e => e.date === selectedDate);
if (dayEvents.length === 0) {
el.innerHTML = '<p style="color:#a1a1aa">Нет событий на этот день</p>';
return;
}
el.innerHTML = '<h2 style="font-size:18px;margin-bottom:12px">События</h2>';
dayEvents.forEach(ev => {
const time = ev.time || 'весь день';
el.innerHTML += `
<div class="event">
<div>
<div class="event-title">${ev.title}</div>
<div class="event-time">${time}${ev.location ? ' · ' + ev.location : ''}</div>
</div>
<div class="event-actions">
<button onclick="deleteEvent('${ev.id}')">✕</button>
</div>
</div>`;
});
}
function selectDate(dateStr) {
selectedDate = dateStr;
render();
}
function prevMonth() {
currentDate.setMonth(currentDate.getMonth() - 1);
selectedDate = null;
render();
}
function nextMonth() {
currentDate.setMonth(currentDate.getMonth() + 1);
selectedDate = null;
render();
}
async function loadEvents() {
// In production, this would call verstak HTTP endpoint or Wails binding
// For now, data is injected via __VERSTAK_DATA__
events = window.__VERSTAK_DATA__ || [];
render();
}
loadEvents();
</script>
</body>
</html>