updated features
This commit is contained in:
+127
-37
@@ -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>`;
|
||||
|
||||
Reference in New Issue
Block a user