367 lines
12 KiB
HTML
367 lines
12 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Моё расписание на неделю</title>
|
||
<style>
|
||
* { margin: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
.container { max-width: 1400px; margin: 0 auto; }
|
||
h1 { color: #fff; text-align: center; margin-bottom: 20px; font-size: clamp(24px, 4vw, 36px); }
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 15px;
|
||
margin-bottom: 25px;
|
||
}
|
||
.stat-card {
|
||
background: rgba(255,255,255,0.95);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
text-align: center;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
}
|
||
.stat-card .num { font-size: 32px; font-weight: bold; color: #667eea; }
|
||
.stat-card .label { color: #666; font-size: 14px; margin-top: 5px; }
|
||
|
||
.add-btn {
|
||
display: block;
|
||
margin: 0 auto 25px;
|
||
padding: 14px 40px;
|
||
background: #fff;
|
||
color: #667eea;
|
||
border: none;
|
||
border-radius: 30px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||
transition: transform 0.2s;
|
||
}
|
||
.add-btn:hover { transform: scale(1.05); }
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0; left: 0;
|
||
width: 100%; height: 100%;
|
||
background: rgba(0,0,0,0.6);
|
||
z-index: 1000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
.modal.on { display: flex; }
|
||
.modal-content {
|
||
background: #fff;
|
||
border-radius: 16px;
|
||
padding: 30px;
|
||
width: 90%;
|
||
max-width: 450px;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
}
|
||
.modal-content h2 { margin-bottom: 20px; color: #333; }
|
||
.form-group { margin-bottom: 15px; }
|
||
.form-group label { display: block; margin-bottom: 5px; color: #555; font-weight: 500; }
|
||
.form-group input, .form-group select, .form-group textarea {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
font-size: 15px;
|
||
}
|
||
.form-group textarea { resize: vertical; min-height: 80px; }
|
||
.form-actions { display: flex; gap: 10px; margin-top: 20px; }
|
||
.form-actions button {
|
||
flex: 1;
|
||
padding: 12px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
}
|
||
.btn-save { background: #667eea; color: #fff; }
|
||
.btn-cancel { background: #e0e0e0; color: #333; }
|
||
|
||
.schedule-grid {
|
||
background: rgba(255,255,255,0.95);
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
overflow-x: auto;
|
||
}
|
||
table { width: 100%; border-collapse: collapse; min-width: 800px; }
|
||
th, td {
|
||
padding: 12px 8px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
}
|
||
th {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #fff;
|
||
font-weight: 600;
|
||
position: sticky;
|
||
top: 0;
|
||
}
|
||
tr:hover { background: #f5f5f5; }
|
||
.day-badge {
|
||
display: inline-block;
|
||
padding: 4px 10px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
.day-1 { background: #ffe0e0; color: #c62828; }
|
||
.day-2 { background: #fff3e0; color: #ef6c00; }
|
||
.day-3 { background: #e8f5e9; color: #2e7d32; }
|
||
.day-4 { background: #e3f2fd; color: #1565c0; }
|
||
.day-5 { background: #f3e5f5; color: #6a1b9a; }
|
||
.day-6 { background: #fff8e1; color: #f9a825; }
|
||
.day-0 { background: #fce4ec; color: #c2185b; }
|
||
.event-title { font-weight: 600; color: #333; }
|
||
.event-time { color: #666; font-size: 14px; }
|
||
.event-location { color: #888; font-size: 13px; }
|
||
.actions { display: flex; gap: 8px; }
|
||
.actions button {
|
||
padding: 6px 12px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
.btn-edit { background: #e3f2fd; color: #1565c0; }
|
||
.btn-delete { background: #ffebee; color: #c62828; }
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: #888;
|
||
}
|
||
.empty-state p { margin-top: 15px; font-size: 16px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>📅 Моё расписание на неделю</h1>
|
||
|
||
<div class="stats">
|
||
<div class="stat-card">
|
||
<div class="num" id="stat-total">0</div>
|
||
<div class="label">Всего событий</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="num" id="stat-today">0</div>
|
||
<div class="label">Сегодня</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="num" id="stat-week">0</div>
|
||
<div class="label">На этой неделе</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="num" id="stat-days">0</div>
|
||
<div class="label">Дней занято</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="add-btn" onclick="openModal()">+ Добавить событие</button>
|
||
|
||
<div class="schedule-grid">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>День</th>
|
||
<th>Время</th>
|
||
<th>Событие</th>
|
||
<th>Место</th>
|
||
<th>Заметки</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="events-body"></tbody>
|
||
</table>
|
||
<div class="empty-state" id="empty-state">
|
||
<div style="font-size: 48px;">📝</div>
|
||
<p>Пока нет событий. Добавьте первое!</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal" id="modal">
|
||
<div class="modal-content">
|
||
<h2 id="modal-title">Добавить событие</h2>
|
||
<form id="event-form">
|
||
<input type="hidden" id="event-id">
|
||
<div class="form-group">
|
||
<label>Название *</label>
|
||
<input type="text" id="event-title" required placeholder="Встреча, урок, тренировка...">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>День недели *</label>
|
||
<select id="event-day" required>
|
||
<option value="1">Понедельник</option>
|
||
<option value="2">Вторник</option>
|
||
<option value="3">Среда</option>
|
||
<option value="4">Четверг</option>
|
||
<option value="5">Пятница</option>
|
||
<option value="6">Суббота</option>
|
||
<option value="0">Воскресенье</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Время начала *</label>
|
||
<input type="time" id="event-time" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Место</label>
|
||
<input type="text" id="event-location" placeholder="Офис, онлайн, зал...">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Заметки</label>
|
||
<textarea id="event-notes" placeholder="Детали, контакты, подготовка..."></textarea>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn-cancel" onclick="closeModal()">Отмена</button>
|
||
<button type="submit" class="btn-save">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const days = { 1: 'Пн', 2: 'Вт', 3: 'Ср', 4: 'Чт', 5: 'Пт', 6: 'Сб', 0: 'Вс' };
|
||
let events = [];
|
||
let editingId = null;
|
||
|
||
async function load() {
|
||
const data = await fetch('/api/events').then(r => r.json());
|
||
events = data.events || [];
|
||
render();
|
||
updateStats();
|
||
}
|
||
|
||
function render() {
|
||
const tbody = document.getElementById('events-body');
|
||
const empty = document.getElementById('empty-state');
|
||
|
||
if (events.length === 0) {
|
||
tbody.innerHTML = '';
|
||
empty.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
empty.style.display = 'none';
|
||
events.sort((a, b) => {
|
||
if (a.day !== b.day) return a.day - b.day;
|
||
return a.time.localeCompare(b.time);
|
||
});
|
||
|
||
tbody.innerHTML = events.map(e => `
|
||
<tr>
|
||
<td><span class="day-badge day-${e.day}">${days[e.day]}</span></td>
|
||
<td class="event-time">${e.time}</td>
|
||
<td class="event-title">${escapeHtml(e.title)}</td>
|
||
<td class="event-location">${escapeHtml(e.location || '—')}</td>
|
||
<td class="event-location">${escapeHtml(e.notes || '—')}</td>
|
||
<td class="actions">
|
||
<button class="btn-edit" onclick="editEvent(${e.id})">✏️</button>
|
||
<button class="btn-delete" onclick="deleteEvent(${e.id})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function updateStats() {
|
||
const today = new Date().getDay();
|
||
today = today === 0 ? 7 : today;
|
||
|
||
document.getElementById('stat-total').textContent = events.length;
|
||
document.getElementById('stat-today').textContent = events.filter(e => e.day === today).length;
|
||
document.getElementById('stat-week').textContent = events.length;
|
||
|
||
const uniqueDays = new Set(events.map(e => e.day)).size;
|
||
document.getElementById('stat-days').textContent = uniqueDays;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function openModal() {
|
||
document.getElementById('modal-title').textContent = 'Добавить событие';
|
||
document.getElementById('event-form').reset();
|
||
document.getElementById('event-id').value = '';
|
||
editingId = null;
|
||
document.getElementById('modal').classList.add('on');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modal').classList.remove('on');
|
||
}
|
||
|
||
function editEvent(id) {
|
||
const e = events.find(ev => ev.id === id);
|
||
if (!e) return;
|
||
|
||
editingId = id;
|
||
document.getElementById('modal-title').textContent = 'Редактировать событие';
|
||
document.getElementById('event-id').value = e.id;
|
||
document.getElementById('event-title').value = e.title;
|
||
document.getElementById('event-day').value = e.day;
|
||
document.getElementById('event-time').value = e.time;
|
||
document.getElementById('event-location').value = e.location || '';
|
||
document.getElementById('event-notes').value = e.notes || '';
|
||
document.getElementById('modal').classList.add('on');
|
||
}
|
||
|
||
async function deleteEvent(id) {
|
||
if (!confirm('Удалить это событие?')) return;
|
||
await fetch('/api/events/' + id, { method: 'DELETE' });
|
||
load();
|
||
}
|
||
|
||
document.getElementById('event-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const data = {
|
||
title: document.getElementById('event-title').value,
|
||
day: parseInt(document.getElementById('event-day').value),
|
||
time: document.getElementById('event-time').value,
|
||
location: document.getElementById('event-location').value,
|
||
notes: document.getElementById('event-notes').value
|
||
};
|
||
|
||
if (editingId) {
|
||
await fetch('/api/events/' + editingId, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
await fetch('/api/events', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
closeModal();
|
||
load();
|
||
});
|
||
|
||
document.getElementById('modal').addEventListener('click', (e) => {
|
||
if (e.target.id === 'modal') closeModal();
|
||
});
|
||
|
||
load();
|
||
</script>
|
||
</body>
|
||
</html>
|