Files
revfulop-calendar/public/index.html
T
2026-02-07 08:21:46 +01:00

503 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
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="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Révfülöp · Nyaraló Naptár</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'DM Sans', sans-serif;
background: #F5F0EB;
color: #3A332D;
min-height: 100vh;
padding: 20px 16px;
}
.container { max-width: 520px; margin: 0 auto; }
/* Header */
.header { text-align: center; margin-bottom: 28px; }
.header-sub { font-size: 11px; letter-spacing: 0.2em; color: #B0A89E; font-weight: 500; text-transform: uppercase; margin-bottom: 4px; }
.header h1 { font-family: 'Playfair Display', serif; font-size: 28px; font-weight: 700; color: #3A332D; line-height: 1.2; }
.header-line { width: 40px; height: 2px; background: #C17F59; margin: 10px auto 0; border-radius: 1px; }
/* Cards */
.card { background: #FFF; border-radius: 14px; padding: 16px; margin-bottom: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
.card-title { font-size: 11px; font-weight: 600; color: #8B7E74; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.08em; }
/* Controls */
.controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.controls select, .controls input {
padding: 7px 10px; border-radius: 8px; border: 1px solid #E8E0D8;
background: #FDFBF9; font-size: 13px; font-family: 'DM Sans', sans-serif;
color: #3A332D; flex: 1; min-width: 100px;
}
.hint { font-size: 11px; color: #B0A89E; margin-top: 8px; }
/* Calendar */
.cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.cal-nav button {
border: none; background: #F5F0EB; border-radius: 8px; width: 32px; height: 32px;
cursor: pointer; font-size: 16px; color: #5C524A; display: flex; align-items: center; justify-content: center;
}
.cal-nav button:hover { background: #EAE3DB; }
.cal-month { font-size: 16px; font-weight: 600; color: #3A332D; }
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.cal-day-header { text-align: center; padding: 6px 0; font-size: 11px; font-weight: 600; color: #8B7E74; letter-spacing: 0.05em; }
.cal-day {
position: relative; min-height: 48px; padding: 3px; border-radius: 6px;
background: #FDFBF9; cursor: pointer; border: 1px solid transparent;
transition: all 0.15s ease;
}
.cal-day:hover { background: #F0EAE3; }
.cal-day.empty { background: transparent; cursor: default; }
.cal-day.today { border: 2px solid #C17F59; }
.cal-day.in-selection { background: #D4C5B9; }
.cal-day-num { font-size: 12px; font-weight: 400; color: #5C524A; text-align: right; padding-right: 2px; display: block; }
.cal-day.today .cal-day-num { font-weight: 700; color: #C17F59; }
.cal-day-dots { display: flex; flex-wrap: wrap; gap: 2px; margin-top: 2px; }
.dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
.dot.planned { opacity: 0.5; border: 1.5px dashed; background: transparent !important; }
/* Legend */
.legend { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 14px; padding: 8px 0; }
.legend-item { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #5C524A; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; }
.legend-row { width: 100%; display: flex; justify-content: center; gap: 16px; margin-top: 2px; }
.legend-row span { font-size: 10px; color: #B0A89E; }
/* Booking list */
.booking-item {
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
border-radius: 8px; background: #FDFBF9; margin-bottom: 6px;
}
.booking-color { width: 3px; height: 100%; min-height: 36px; border-radius: 2px; flex-shrink: 0; }
.booking-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.booking-info { flex: 1; min-width: 0; }
.booking-name { font-size: 13px; font-weight: 600; color: #3A332D; }
.booking-badge {
display: inline-block; margin-left: 8px; font-size: 10px; font-weight: 500;
padding: 1px 6px; border-radius: 10px;
}
.booking-badge.confirmed { background: #81B29A22; color: #5A9A78; }
.booking-badge.planned { background: #F2CC8F33; color: #C49A4A; }
.booking-dates { font-size: 11px; color: #8B7E74; margin-top: 2px; }
.booking-actions button {
border: none; background: none; cursor: pointer; font-size: 14px; padding: 4px; border-radius: 4px;
}
.booking-actions .toggle { color: #B0A89E; }
.booking-actions .delete { color: #D4756B; }
.empty-state { text-align: center; padding: 30px; color: #B0A89E; font-style: italic; }
/* Comments */
.comments-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; max-height: 300px; overflow-y: auto; }
.comment {
padding: 10px 14px; border-radius: 10px; background: #FDFBF9;
border-left: 3px solid #ccc;
}
.comment-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
.comment-author { font-size: 12px; font-weight: 600; }
.comment-time { font-size: 10px; color: #B0A89E; }
.comment-text { font-size: 13px; color: #5C524A; line-height: 1.5; }
.comment-form { display: flex; gap: 8px; align-items: flex-end; }
.comment-form select { width: 100px; }
.comment-form input {
flex: 1; padding: 8px 12px; border-radius: 8px; border: 1px solid #E8E0D8;
background: #FFF; font-size: 13px; font-family: 'DM Sans', sans-serif;
color: #3A332D; outline: none;
}
.comment-form input:focus { border-color: #C17F59; }
.btn-send {
padding: 8px 16px; border-radius: 8px; border: none;
background: #C17F59; color: #FFF; font-size: 12px; font-weight: 600;
cursor: pointer; font-family: 'DM Sans', sans-serif; white-space: nowrap;
}
.btn-send:hover { background: #A96A4A; }
/* Login */
.login-overlay {
position: fixed; inset: 0; background: #F5F0EB;
display: flex; align-items: center; justify-content: center; z-index: 100;
}
.login-box { text-align: center; }
.login-box input {
padding: 10px 16px; border-radius: 8px; border: 1px solid #E8E0D8;
font-size: 14px; font-family: 'DM Sans', sans-serif; width: 220px;
margin: 12px 0; text-align: center;
}
.login-box .btn-send { display: block; width: 220px; margin: 0 auto; padding: 10px; }
.login-error { font-size: 12px; color: #D4756B; margin-top: 8px; }
.footer { text-align: center; font-size: 10px; color: #C8BFB5; padding: 0 0 20px; }
</style>
</head>
<body>
<div id="login-screen" class="login-overlay" style="display:none;">
<div class="login-box">
<div class="header-sub">Révfülöp · Balaton</div>
<h1 style="font-family:'Playfair Display',serif;font-size:28px;font-weight:700;color:#3A332D;margin:4px 0 16px;">Nyaraló Naptár</h1>
<div class="header-line" style="margin:0 auto 20px;"></div>
<input type="password" id="login-password" placeholder="Jelszó" onkeydown="if(event.key==='Enter')doLogin()">
<button class="btn-send" onclick="doLogin()">Belépés</button>
<div id="login-error" class="login-error"></div>
</div>
</div>
<div id="app" class="container" style="display:none;">
<div class="header">
<div class="header-sub">Révfülöp · Balaton</div>
<h1>Nyaraló Naptár</h1>
<div class="header-line"></div>
</div>
<!-- New Booking Controls -->
<div class="card">
<div class="card-title">Új foglalás</div>
<div class="controls">
<select id="sel-member"></select>
<select id="sel-status">
<option value="planned">○ Tervezett</option>
<option value="confirmed">✓ Megerősített</option>
</select>
</div>
<div class="hint" id="sel-hint">Kattints a kezdő dátumra a naptárban ↓</div>
</div>
<!-- Calendar -->
<div class="card">
<div class="cal-nav">
<button onclick="prevMonth()"></button>
<div class="cal-month" id="cal-month-label"></div>
<button onclick="nextMonth()"></button>
</div>
<div class="cal-grid" id="cal-grid"></div>
</div>
<!-- Legend -->
<div class="legend" id="legend"></div>
<!-- Bookings List -->
<div class="card">
<div class="card-title">Foglalások</div>
<div id="bookings-list"></div>
</div>
<!-- Comments -->
<div class="card">
<div class="card-title">Hozzászólások</div>
<div class="comments-list" id="comments-list"></div>
<div class="comment-form">
<select id="comment-member" style="padding:7px 10px;border-radius:8px;border:1px solid #E8E0D8;background:#FDFBF9;font-size:12px;font-family:'DM Sans',sans-serif;color:#3A332D;"></select>
<input type="text" id="comment-text" placeholder="Hozzászólás írása..." onkeydown="if(event.key==='Enter')addComment()">
<button class="btn-send" onclick="addComment()">Küldés</button>
</div>
</div>
<div class="footer">revfulop.dooplex.hu</div>
</div>
<script>
const MONTHS_HU = ["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"];
const DAYS_HU = ["H","K","Sze","Cs","P","Szo","V"];
let members = [];
let bookings = [];
let comments = [];
let currentYear, currentMonth;
let selectionStart = null;
let isSelecting = false;
let authToken = localStorage.getItem('revfulop_token') || '';
// API helpers
async function api(method, path, body) {
const opts = {
method,
headers: { 'Content-Type': 'application/json' },
};
if (authToken) opts.headers['X-Auth-Token'] = authToken;
if (body) opts.body = JSON.stringify(body);
const res = await fetch('/api' + path, opts);
if (res.status === 401) {
authToken = '';
localStorage.removeItem('revfulop_token');
showLogin();
throw new Error('Unauthorized');
}
return res.json();
}
// Auth
async function checkAuth() {
const status = await fetch('/api/auth-status').then(r => r.json());
if (!status.authEnabled) {
showApp();
return;
}
if (authToken) {
try {
await api('GET', '/members');
showApp();
} catch {
showLogin();
}
} else {
showLogin();
}
}
function showLogin() {
document.getElementById('login-screen').style.display = 'flex';
document.getElementById('app').style.display = 'none';
}
function showApp() {
document.getElementById('login-screen').style.display = 'none';
document.getElementById('app').style.display = 'block';
init();
}
async function doLogin() {
const pw = document.getElementById('login-password').value;
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pw }),
}).then(r => r.json());
if (res.success) {
authToken = res.token;
localStorage.setItem('revfulop_token', authToken);
showApp();
} else {
document.getElementById('login-error').textContent = 'Hibás jelszó';
}
} catch {
document.getElementById('login-error').textContent = 'Hiba történt';
}
}
// Init
async function init() {
const now = new Date();
currentYear = now.getFullYear();
currentMonth = now.getMonth();
members = await api('GET', '/members');
await loadData();
// Populate selects
const selMember = document.getElementById('sel-member');
const commentMember = document.getElementById('comment-member');
selMember.innerHTML = '';
commentMember.innerHTML = '';
members.forEach(m => {
selMember.innerHTML += `<option value="${m.id}">${m.name}</option>`;
commentMember.innerHTML += `<option value="${m.id}">${m.name}</option>`;
});
// Legend
const legend = document.getElementById('legend');
legend.innerHTML = members.map(m =>
`<div class="legend-item"><div class="legend-dot" style="background:${m.color}"></div>${m.name}</div>`
).join('') + `
<div class="legend-row">
<div class="legend-item"><div class="legend-dot" style="background:#999;opacity:0.5;border:1.5px dashed #999;box-sizing:border-box;"></div><span>tervezett</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#999"></div><span>megerősített</span></div>
</div>`;
render();
}
async function loadData() {
bookings = await api('GET', '/bookings');
comments = await api('GET', '/comments');
}
function render() {
renderCalendar();
renderBookings();
renderComments();
}
// Calendar
function prevMonth() { if (currentMonth === 0) { currentMonth = 11; currentYear--; } else currentMonth--; renderCalendar(); }
function nextMonth() { if (currentMonth === 11) { currentMonth = 0; currentYear++; } else currentMonth++; renderCalendar(); }
function dateKey(y, m, d) {
return `${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
}
function inRange(dk, start, end) { return dk >= start && dk <= end; }
function renderCalendar() {
document.getElementById('cal-month-label').textContent = `${MONTHS_HU[currentMonth]} ${currentYear}`;
const firstDay = new Date(currentYear, currentMonth, 1);
const lastDay = new Date(currentYear, currentMonth + 1, 0);
const startOffset = (firstDay.getDay() + 6) % 7;
const today = new Date(); today.setHours(0,0,0,0);
const todayKey = dateKey(today.getFullYear(), today.getMonth(), today.getDate());
let html = DAYS_HU.map(d => `<div class="cal-day-header">${d}</div>`).join('');
for (let i = 0; i < startOffset; i++) html += '<div class="cal-day empty"></div>';
for (let d = 1; d <= lastDay.getDate(); d++) {
const dk = dateKey(currentYear, currentMonth, d);
const isToday = dk === todayKey;
const dayBookings = bookings.filter(b => inRange(dk, b.start_date, b.end_date));
let classes = 'cal-day';
if (isToday) classes += ' today';
const dots = dayBookings.map(b => {
const m = members.find(x => x.id === b.member_id);
const color = m ? m.color : '#999';
if (b.status === 'planned') {
return `<div class="dot planned" style="border-color:${color}" title="${m?.name || ''} (tervezett)"></div>`;
}
return `<div class="dot" style="background:${color}" title="${m?.name || ''} (megerősített)"></div>`;
}).join('');
html += `<div class="${classes}" onclick="dayClick('${dk}')" onmouseenter="dayHover('${dk}')">
<span class="cal-day-num">${d}</span>
<div class="cal-day-dots">${dots}</div>
</div>`;
}
document.getElementById('cal-grid').innerHTML = html;
}
function dayClick(dk) {
if (!isSelecting) {
selectionStart = dk;
isSelecting = true;
document.getElementById('sel-hint').textContent = '📍 Kattints a befejező dátumra';
highlightSelection(dk, dk);
} else {
const start = selectionStart < dk ? selectionStart : dk;
const end = selectionStart < dk ? dk : selectionStart;
isSelecting = false;
selectionStart = null;
document.getElementById('sel-hint').textContent = 'Kattints a kezdő dátumra a naptárban ↓';
addBooking(start, end);
}
}
function dayHover(dk) {
if (isSelecting && selectionStart) {
highlightSelection(selectionStart, dk);
}
}
function highlightSelection(s, e) {
const start = s < e ? s : e;
const end = s < e ? e : s;
document.querySelectorAll('.cal-day').forEach(el => {
el.classList.remove('in-selection');
const numEl = el.querySelector('.cal-day-num');
if (!numEl) return;
const d = parseInt(numEl.textContent);
const dk = dateKey(currentYear, currentMonth, d);
if (inRange(dk, start, end)) el.classList.add('in-selection');
});
}
// Bookings
async function addBooking(start, end) {
const memberId = document.getElementById('sel-member').value;
const status = document.getElementById('sel-status').value;
await api('POST', '/bookings', { member_id: memberId, start_date: start, end_date: end, status });
await loadData();
render();
}
async function toggleBooking(id) {
const b = bookings.find(x => x.id === id);
if (!b) return;
await api('PUT', `/bookings/${id}`, { status: b.status === 'planned' ? 'confirmed' : 'planned' });
await loadData();
render();
}
async function deleteBooking(id) {
await api('DELETE', `/bookings/${id}`);
await loadData();
render();
}
function renderBookings() {
const list = document.getElementById('bookings-list');
if (bookings.length === 0) {
list.innerHTML = '<div class="empty-state">Még nincs foglalás. Kattints a naptárra egy új hozzáadásához!</div>';
return;
}
list.innerHTML = bookings.map(b => {
const m = members.find(x => x.id === b.member_id);
const color = m ? m.color : '#999';
const start = new Date(b.start_date);
const end = new Date(b.end_date);
const nights = Math.round((end - start) / 86400000);
return `<div class="booking-item" style="border-left:3px solid ${color}">
<div class="booking-dot" style="background:${color};opacity:${b.status==='planned'?0.5:1}"></div>
<div class="booking-info">
<div class="booking-name">${m?.name || b.member_id}
<span class="booking-badge ${b.status}">${b.status === 'confirmed' ? '✓ megerősített' : '○ tervezett'}</span>
</div>
<div class="booking-dates">${b.start_date}${b.end_date} (${nights} éj)</div>
</div>
<div class="booking-actions">
<button class="toggle" onclick="toggleBooking(${b.id})" title="Státusz váltás">↻</button>
<button class="delete" onclick="deleteBooking(${b.id})" title="Törlés">✕</button>
</div>
</div>`;
}).join('');
}
// Comments
async function addComment() {
const memberId = document.getElementById('comment-member').value;
const text = document.getElementById('comment-text').value.trim();
if (!text) return;
await api('POST', '/comments', { member_id: memberId, text });
document.getElementById('comment-text').value = '';
await loadData();
renderComments();
}
function renderComments() {
const list = document.getElementById('comments-list');
if (comments.length === 0) {
list.innerHTML = '<div class="empty-state">Még nincsenek hozzászólások.</div>';
return;
}
list.innerHTML = comments.map(c => {
const m = members.find(x => x.id === c.member_id);
const d = new Date(c.created_at);
const dateStr = d.toLocaleDateString('hu-HU') + ' ' + d.toLocaleTimeString('hu-HU', { hour: '2-digit', minute: '2-digit' });
return `<div class="comment" style="border-left-color:${m?.color || '#ccc'}">
<div class="comment-header">
<span class="comment-author" style="color:${m?.color || '#666'}">${m?.name || c.member_id}</span>
<span class="comment-time">${dateStr}</span>
</div>
<div class="comment-text">${escapeHtml(c.text)}</div>
</div>`;
}).join('');
}
function escapeHtml(t) {
const d = document.createElement('div');
d.textContent = t;
return d.innerHTML;
}
// Start
checkAuth();
</script>
</body>
</html>