updated features

This commit is contained in:
2026-02-07 09:05:22 +01:00
parent a1e41a7692
commit d83a3e0542
2 changed files with 179 additions and 53 deletions
+127 -37
View File
@@ -23,6 +23,22 @@
.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; }
/* User bar */
.user-bar {
display: flex; align-items: center; justify-content: center; gap: 10px;
margin-bottom: 18px; padding: 8px 14px; border-radius: 10px;
background: #FFF; box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.user-bar-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.user-bar-name { font-size: 13px; font-weight: 600; color: #3A332D; }
.user-bar-label { font-size: 11px; color: #B0A89E; }
.btn-logout {
margin-left: auto; border: none; background: #F5F0EB; border-radius: 6px;
padding: 4px 10px; font-size: 11px; font-weight: 500; color: #8B7E74;
cursor: pointer; font-family: 'DM Sans', sans-serif;
}
.btn-logout:hover { background: #EAE3DB; color: #5C524A; }
/* 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; }
@@ -58,9 +74,7 @@
.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 { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot.planned { opacity: 0.5; border: 1.5px dashed; background: transparent !important; }
/* Legend */
@@ -75,7 +89,6 @@
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; }
@@ -94,17 +107,22 @@
.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; }
.comments-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; max-height: 400px; overflow-y: auto; }
.comment {
padding: 10px 14px; border-radius: 10px; background: #FDFBF9;
border-left: 3px solid #ccc;
border-left: 3px solid #ccc; position: relative;
}
.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-delete {
border: none; background: none; cursor: pointer; font-size: 12px;
color: #D4756B; padding: 2px 4px; border-radius: 4px; margin-left: 8px;
opacity: 0.6;
}
.comment-delete:hover { opacity: 1; }
.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;
@@ -123,14 +141,24 @@
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 {
.login-box { text-align: center; width: 280px; }
.login-box input, .login-box select {
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;
font-size: 14px; font-family: 'DM Sans', sans-serif; width: 100%;
margin-bottom: 10px; text-align: center; background: #FFF; color: #3A332D;
}
.login-box .btn-send { display: block; width: 220px; margin: 0 auto; padding: 10px; }
.login-box .btn-send { display: block; width: 100%; padding: 10px; }
.login-error { font-size: 12px; color: #D4756B; margin-top: 8px; }
.login-members { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 16px 0; }
.login-member-btn {
display: flex; align-items: center; gap: 6px;
padding: 8px 14px; border-radius: 10px; border: 2px solid #E8E0D8;
background: #FFF; cursor: pointer; font-family: 'DM Sans', sans-serif;
font-size: 13px; font-weight: 500; color: #5C524A; transition: all 0.15s ease;
}
.login-member-btn:hover { border-color: #C17F59; }
.login-member-btn.selected { border-color: #C17F59; background: #FDF6F0; font-weight: 600; }
.login-member-dot { width: 10px; height: 10px; border-radius: 50%; }
.footer { text-align: center; font-size: 10px; color: #C8BFB5; padding: 0 0 20px; }
</style>
@@ -140,8 +168,10 @@
<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>
<h1 style="font-family:'Playfair Display',serif;font-size:28px;font-weight:700;color:#3A332D;margin:4px 0 8px;">Nyaraló Naptár</h1>
<div class="header-line" style="margin:0 auto 12px;"></div>
<div style="font-size:12px;color:#8B7E74;margin-bottom:4px;">Ki vagy?</div>
<div class="login-members" id="login-members"></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>
@@ -155,11 +185,18 @@
<div class="header-line"></div>
</div>
<!-- User bar -->
<div class="user-bar" id="user-bar">
<div class="user-bar-dot" id="user-bar-dot"></div>
<span class="user-bar-name" id="user-bar-name"></span>
<span class="user-bar-label">bejelentkezve</span>
<button class="btn-logout" onclick="doLogout()">Kijelentkezés</button>
</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>
@@ -192,7 +229,6 @@
<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>
@@ -212,6 +248,8 @@ let currentYear, currentMonth;
let selectionStart = null;
let isSelecting = false;
let authToken = localStorage.getItem('revfulop_token') || '';
let currentMemberId = localStorage.getItem('revfulop_member') || '';
let selectedLoginMember = '';
// API helpers
async function api(method, path, body) {
@@ -224,7 +262,9 @@ async function api(method, path, body) {
const res = await fetch('/api' + path, opts);
if (res.status === 401) {
authToken = '';
currentMemberId = '';
localStorage.removeItem('revfulop_token');
localStorage.removeItem('revfulop_member');
showLogin();
throw new Error('Unauthorized');
}
@@ -233,14 +273,18 @@ async function api(method, path, body) {
// Auth
async function checkAuth() {
// Always load members first (available without auth)
members = await fetch('/api/members').then(r => r.json());
buildLoginMembers();
const status = await fetch('/api/auth-status').then(r => r.json());
if (!status.authEnabled) {
showApp();
return;
}
if (authToken) {
if (authToken && currentMemberId) {
try {
await api('GET', '/members');
await api('GET', '/me');
showApp();
} catch {
showLogin();
@@ -250,56 +294,93 @@ async function checkAuth() {
}
}
function buildLoginMembers() {
const container = document.getElementById('login-members');
container.innerHTML = members.map(m =>
`<button class="login-member-btn" data-id="${m.id}" onclick="selectLoginMember('${m.id}')">
<div class="login-member-dot" style="background:${m.color}"></div>
${m.name}
</button>`
).join('');
}
function selectLoginMember(id) {
selectedLoginMember = id;
document.querySelectorAll('.login-member-btn').forEach(btn => {
btn.classList.toggle('selected', btn.dataset.id === id);
});
}
function showLogin() {
document.getElementById('login-screen').style.display = 'flex';
document.getElementById('app').style.display = 'none';
document.getElementById('login-error').textContent = '';
document.getElementById('login-password').value = '';
selectedLoginMember = '';
document.querySelectorAll('.login-member-btn').forEach(b => b.classList.remove('selected'));
}
function showApp() {
document.getElementById('login-screen').style.display = 'none';
document.getElementById('app').style.display = 'block';
updateUserBar();
init();
}
function updateUserBar() {
const member = members.find(m => m.id === currentMemberId);
const bar = document.getElementById('user-bar');
if (!member) {
bar.style.display = 'none';
return;
}
bar.style.display = 'flex';
document.getElementById('user-bar-dot').style.background = member.color;
document.getElementById('user-bar-name').textContent = member.name;
}
async function doLogin() {
if (!selectedLoginMember) {
document.getElementById('login-error').textContent = 'Válassz egy családtagot!';
return;
}
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 }),
body: JSON.stringify({ password: pw, memberId: selectedLoginMember }),
}).then(r => r.json());
if (res.success) {
authToken = res.token;
currentMemberId = res.memberId;
localStorage.setItem('revfulop_token', authToken);
localStorage.setItem('revfulop_member', currentMemberId);
showApp();
} else {
document.getElementById('login-error').textContent = 'Hibás jelszó';
document.getElementById('login-error').textContent = res.error || 'Hibás jelszó';
}
} catch {
document.getElementById('login-error').textContent = 'Hiba történt';
}
}
function doLogout() {
authToken = '';
currentMemberId = '';
localStorage.removeItem('revfulop_token');
localStorage.removeItem('revfulop_member');
showLogin();
}
// 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 =>
@@ -410,9 +491,8 @@ function highlightSelection(s, e) {
// 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 api('POST', '/bookings', { start_date: start, end_date: end, status });
await loadData();
render();
}
@@ -461,15 +541,20 @@ function renderBookings() {
// 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 });
await api('POST', '/comments', { text });
document.getElementById('comment-text').value = '';
await loadData();
renderComments();
}
async function deleteComment(id) {
await api('DELETE', `/comments/${id}`);
await loadData();
renderComments();
}
function renderComments() {
const list = document.getElementById('comments-list');
if (comments.length === 0) {
@@ -478,12 +563,17 @@ function renderComments() {
}
list.innerHTML = comments.map(c => {
const m = members.find(x => x.id === c.member_id);
const d = new Date(c.created_at);
const d = new Date(c.created_at + 'Z');
const dateStr = d.toLocaleDateString('hu-HU') + ' ' + d.toLocaleTimeString('hu-HU', { hour: '2-digit', minute: '2-digit' });
const isOwn = c.member_id === currentMemberId;
const deleteBtn = isOwn ? `<button class="comment-delete" onclick="deleteComment(${c.id})" title="Törlés">✕</button>` : '';
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>
<span class="comment-time">
${dateStr}
${deleteBtn}
</span>
</div>
<div class="comment-text">${escapeHtml(c.text)}</div>
</div>`;