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 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; }
|
.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 */
|
/* Cards */
|
||||||
.card { background: #FFF; border-radius: 14px; padding: 16px; margin-bottom: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
.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; }
|
.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-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.today .cal-day-num { font-weight: 700; color: #C17F59; }
|
||||||
.cal-day-dots { display: flex; flex-wrap: wrap; gap: 2px; margin-top: 2px; }
|
.cal-day-dots { display: flex; flex-wrap: wrap; gap: 2px; margin-top: 2px; }
|
||||||
.dot {
|
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.dot.planned { opacity: 0.5; border: 1.5px dashed; background: transparent !important; }
|
.dot.planned { opacity: 0.5; border: 1.5px dashed; background: transparent !important; }
|
||||||
|
|
||||||
/* Legend */
|
/* Legend */
|
||||||
@@ -75,7 +89,6 @@
|
|||||||
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
|
display: flex; align-items: center; gap: 10px; padding: 8px 12px;
|
||||||
border-radius: 8px; background: #FDFBF9; margin-bottom: 6px;
|
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-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.booking-info { flex: 1; min-width: 0; }
|
.booking-info { flex: 1; min-width: 0; }
|
||||||
.booking-name { font-size: 13px; font-weight: 600; color: #3A332D; }
|
.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; }
|
.empty-state { text-align: center; padding: 30px; color: #B0A89E; font-style: italic; }
|
||||||
|
|
||||||
/* Comments */
|
/* 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 {
|
.comment {
|
||||||
padding: 10px 14px; border-radius: 10px; background: #FDFBF9;
|
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-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
||||||
.comment-author { font-size: 12px; font-weight: 600; }
|
.comment-author { font-size: 12px; font-weight: 600; }
|
||||||
.comment-time { font-size: 10px; color: #B0A89E; }
|
.comment-time { font-size: 10px; color: #B0A89E; }
|
||||||
.comment-text { font-size: 13px; color: #5C524A; line-height: 1.5; }
|
.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 { display: flex; gap: 8px; align-items: flex-end; }
|
||||||
.comment-form select { width: 100px; }
|
|
||||||
.comment-form input {
|
.comment-form input {
|
||||||
flex: 1; padding: 8px 12px; border-radius: 8px; border: 1px solid #E8E0D8;
|
flex: 1; padding: 8px 12px; border-radius: 8px; border: 1px solid #E8E0D8;
|
||||||
background: #FFF; font-size: 13px; font-family: 'DM Sans', sans-serif;
|
background: #FFF; font-size: 13px; font-family: 'DM Sans', sans-serif;
|
||||||
@@ -123,14 +141,24 @@
|
|||||||
position: fixed; inset: 0; background: #F5F0EB;
|
position: fixed; inset: 0; background: #F5F0EB;
|
||||||
display: flex; align-items: center; justify-content: center; z-index: 100;
|
display: flex; align-items: center; justify-content: center; z-index: 100;
|
||||||
}
|
}
|
||||||
.login-box { text-align: center; }
|
.login-box { text-align: center; width: 280px; }
|
||||||
.login-box input {
|
.login-box input, .login-box select {
|
||||||
padding: 10px 16px; border-radius: 8px; border: 1px solid #E8E0D8;
|
padding: 10px 16px; border-radius: 8px; border: 1px solid #E8E0D8;
|
||||||
font-size: 14px; font-family: 'DM Sans', sans-serif; width: 220px;
|
font-size: 14px; font-family: 'DM Sans', sans-serif; width: 100%;
|
||||||
margin: 12px 0; text-align: center;
|
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-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; }
|
.footer { text-align: center; font-size: 10px; color: #C8BFB5; padding: 0 0 20px; }
|
||||||
</style>
|
</style>
|
||||||
@@ -140,8 +168,10 @@
|
|||||||
<div id="login-screen" class="login-overlay" style="display:none;">
|
<div id="login-screen" class="login-overlay" style="display:none;">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<div class="header-sub">Révfülöp · Balaton</div>
|
<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>
|
<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 20px;"></div>
|
<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()">
|
<input type="password" id="login-password" placeholder="Jelszó" onkeydown="if(event.key==='Enter')doLogin()">
|
||||||
<button class="btn-send" onclick="doLogin()">Belépés</button>
|
<button class="btn-send" onclick="doLogin()">Belépés</button>
|
||||||
<div id="login-error" class="login-error"></div>
|
<div id="login-error" class="login-error"></div>
|
||||||
@@ -155,11 +185,18 @@
|
|||||||
<div class="header-line"></div>
|
<div class="header-line"></div>
|
||||||
</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 -->
|
<!-- New Booking Controls -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Új foglalás</div>
|
<div class="card-title">Új foglalás</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<select id="sel-member"></select>
|
|
||||||
<select id="sel-status">
|
<select id="sel-status">
|
||||||
<option value="planned">○ Tervezett</option>
|
<option value="planned">○ Tervezett</option>
|
||||||
<option value="confirmed">✓ Megerősített</option>
|
<option value="confirmed">✓ Megerősített</option>
|
||||||
@@ -192,7 +229,6 @@
|
|||||||
<div class="card-title">Hozzászólások</div>
|
<div class="card-title">Hozzászólások</div>
|
||||||
<div class="comments-list" id="comments-list"></div>
|
<div class="comments-list" id="comments-list"></div>
|
||||||
<div class="comment-form">
|
<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()">
|
<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>
|
<button class="btn-send" onclick="addComment()">Küldés</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,6 +248,8 @@ let currentYear, currentMonth;
|
|||||||
let selectionStart = null;
|
let selectionStart = null;
|
||||||
let isSelecting = false;
|
let isSelecting = false;
|
||||||
let authToken = localStorage.getItem('revfulop_token') || '';
|
let authToken = localStorage.getItem('revfulop_token') || '';
|
||||||
|
let currentMemberId = localStorage.getItem('revfulop_member') || '';
|
||||||
|
let selectedLoginMember = '';
|
||||||
|
|
||||||
// API helpers
|
// API helpers
|
||||||
async function api(method, path, body) {
|
async function api(method, path, body) {
|
||||||
@@ -224,7 +262,9 @@ async function api(method, path, body) {
|
|||||||
const res = await fetch('/api' + path, opts);
|
const res = await fetch('/api' + path, opts);
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
authToken = '';
|
authToken = '';
|
||||||
|
currentMemberId = '';
|
||||||
localStorage.removeItem('revfulop_token');
|
localStorage.removeItem('revfulop_token');
|
||||||
|
localStorage.removeItem('revfulop_member');
|
||||||
showLogin();
|
showLogin();
|
||||||
throw new Error('Unauthorized');
|
throw new Error('Unauthorized');
|
||||||
}
|
}
|
||||||
@@ -233,14 +273,18 @@ async function api(method, path, body) {
|
|||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
async function checkAuth() {
|
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());
|
const status = await fetch('/api/auth-status').then(r => r.json());
|
||||||
if (!status.authEnabled) {
|
if (!status.authEnabled) {
|
||||||
showApp();
|
showApp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (authToken) {
|
if (authToken && currentMemberId) {
|
||||||
try {
|
try {
|
||||||
await api('GET', '/members');
|
await api('GET', '/me');
|
||||||
showApp();
|
showApp();
|
||||||
} catch {
|
} catch {
|
||||||
showLogin();
|
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() {
|
function showLogin() {
|
||||||
document.getElementById('login-screen').style.display = 'flex';
|
document.getElementById('login-screen').style.display = 'flex';
|
||||||
document.getElementById('app').style.display = 'none';
|
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() {
|
function showApp() {
|
||||||
document.getElementById('login-screen').style.display = 'none';
|
document.getElementById('login-screen').style.display = 'none';
|
||||||
document.getElementById('app').style.display = 'block';
|
document.getElementById('app').style.display = 'block';
|
||||||
|
updateUserBar();
|
||||||
init();
|
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() {
|
async function doLogin() {
|
||||||
|
if (!selectedLoginMember) {
|
||||||
|
document.getElementById('login-error').textContent = 'Válassz egy családtagot!';
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pw = document.getElementById('login-password').value;
|
const pw = document.getElementById('login-password').value;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/login', {
|
const res = await fetch('/api/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ password: pw }),
|
body: JSON.stringify({ password: pw, memberId: selectedLoginMember }),
|
||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
authToken = res.token;
|
authToken = res.token;
|
||||||
|
currentMemberId = res.memberId;
|
||||||
localStorage.setItem('revfulop_token', authToken);
|
localStorage.setItem('revfulop_token', authToken);
|
||||||
|
localStorage.setItem('revfulop_member', currentMemberId);
|
||||||
showApp();
|
showApp();
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('login-error').textContent = 'Hibás jelszó';
|
document.getElementById('login-error').textContent = res.error || 'Hibás jelszó';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
document.getElementById('login-error').textContent = 'Hiba történt';
|
document.getElementById('login-error').textContent = 'Hiba történt';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function doLogout() {
|
||||||
|
authToken = '';
|
||||||
|
currentMemberId = '';
|
||||||
|
localStorage.removeItem('revfulop_token');
|
||||||
|
localStorage.removeItem('revfulop_member');
|
||||||
|
showLogin();
|
||||||
|
}
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
async function init() {
|
async function init() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
currentYear = now.getFullYear();
|
currentYear = now.getFullYear();
|
||||||
currentMonth = now.getMonth();
|
currentMonth = now.getMonth();
|
||||||
|
|
||||||
members = await api('GET', '/members');
|
|
||||||
await loadData();
|
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
|
// Legend
|
||||||
const legend = document.getElementById('legend');
|
const legend = document.getElementById('legend');
|
||||||
legend.innerHTML = members.map(m =>
|
legend.innerHTML = members.map(m =>
|
||||||
@@ -410,9 +491,8 @@ function highlightSelection(s, e) {
|
|||||||
|
|
||||||
// Bookings
|
// Bookings
|
||||||
async function addBooking(start, end) {
|
async function addBooking(start, end) {
|
||||||
const memberId = document.getElementById('sel-member').value;
|
|
||||||
const status = document.getElementById('sel-status').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();
|
await loadData();
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
@@ -461,15 +541,20 @@ function renderBookings() {
|
|||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
async function addComment() {
|
async function addComment() {
|
||||||
const memberId = document.getElementById('comment-member').value;
|
|
||||||
const text = document.getElementById('comment-text').value.trim();
|
const text = document.getElementById('comment-text').value.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
await api('POST', '/comments', { member_id: memberId, text });
|
await api('POST', '/comments', { text });
|
||||||
document.getElementById('comment-text').value = '';
|
document.getElementById('comment-text').value = '';
|
||||||
await loadData();
|
await loadData();
|
||||||
renderComments();
|
renderComments();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteComment(id) {
|
||||||
|
await api('DELETE', `/comments/${id}`);
|
||||||
|
await loadData();
|
||||||
|
renderComments();
|
||||||
|
}
|
||||||
|
|
||||||
function renderComments() {
|
function renderComments() {
|
||||||
const list = document.getElementById('comments-list');
|
const list = document.getElementById('comments-list');
|
||||||
if (comments.length === 0) {
|
if (comments.length === 0) {
|
||||||
@@ -478,12 +563,17 @@ function renderComments() {
|
|||||||
}
|
}
|
||||||
list.innerHTML = comments.map(c => {
|
list.innerHTML = comments.map(c => {
|
||||||
const m = members.find(x => x.id === c.member_id);
|
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 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'}">
|
return `<div class="comment" style="border-left-color:${m?.color || '#ccc'}">
|
||||||
<div class="comment-header">
|
<div class="comment-header">
|
||||||
<span class="comment-author" style="color:${m?.color || '#666'}">${m?.name || c.member_id}</span>
|
<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>
|
||||||
<div class="comment-text">${escapeHtml(c.text)}</div>
|
<div class="comment-text">${escapeHtml(c.text)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ const DB_PATH = process.env.DB_PATH || '/data/revfulop.db';
|
|||||||
// Simple auth config (set SIMPLE_AUTH_PASSWORD env var to enable)
|
// Simple auth config (set SIMPLE_AUTH_PASSWORD env var to enable)
|
||||||
const AUTH_PASSWORD = process.env.SIMPLE_AUTH_PASSWORD || '';
|
const AUTH_PASSWORD = process.env.SIMPLE_AUTH_PASSWORD || '';
|
||||||
const AUTH_ENABLED = AUTH_PASSWORD.length > 0;
|
const AUTH_ENABLED = AUTH_PASSWORD.length > 0;
|
||||||
const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex');
|
|
||||||
|
|
||||||
// Family members config (can be overridden via FAMILY_MEMBERS env var as JSON)
|
// Family members config (can be overridden via FAMILY_MEMBERS env var as JSON)
|
||||||
const DEFAULT_MEMBERS = [
|
const DEFAULT_MEMBERS = [
|
||||||
|
{ id: "katinka", name: "Katinka", color: "#513eff" },
|
||||||
{ id: "orsi", name: "Orsi", color: "#a15dd8" },
|
{ id: "orsi", name: "Orsi", color: "#a15dd8" },
|
||||||
{ id: "lili", name: "Lili", color: "#ffe70c" },
|
{ id: "lili", name: "Lili", color: "#ffe70c" },
|
||||||
{ id: "mama", name: "Mama", color: "#513eff" },
|
{ id: "bazsi", name: "Bazsi", color: "#2db84a" },
|
||||||
];
|
];
|
||||||
|
|
||||||
let FAMILY_MEMBERS;
|
let FAMILY_MEMBERS;
|
||||||
@@ -48,27 +48,35 @@ db.exec(`
|
|||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Session tokens (in-memory, simple approach)
|
// Session tokens: token -> { created, memberId }
|
||||||
const sessions = new Map();
|
const sessions = new Map();
|
||||||
|
|
||||||
// Simple auth middleware
|
// Auth helper: get logged-in member from token
|
||||||
|
function getMember(req) {
|
||||||
|
const token = req.headers['x-auth-token'];
|
||||||
|
if (!token) return null;
|
||||||
|
const session = sessions.get(token);
|
||||||
|
if (!session) return null;
|
||||||
|
return session.memberId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth middleware
|
||||||
function authMiddleware(req, res, next) {
|
function authMiddleware(req, res, next) {
|
||||||
if (!AUTH_ENABLED) return next();
|
if (!AUTH_ENABLED) return next();
|
||||||
|
|
||||||
// Skip auth for login endpoint and static assets
|
// Skip auth for login, auth-status, and members endpoints
|
||||||
if (req.path === '/api/login' || req.path === '/api/auth-status') return next();
|
if (req.path === '/api/login' || req.path === '/api/auth-status' || req.path === '/api/members') return next();
|
||||||
|
|
||||||
const token = req.headers['x-auth-token'];
|
const token = req.headers['x-auth-token'];
|
||||||
if (token && sessions.has(token)) {
|
if (token && sessions.has(token)) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// For API routes, return 401
|
|
||||||
if (req.path.startsWith('/api/')) {
|
if (req.path.startsWith('/api/')) {
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
next(); // Let static files through (frontend handles auth UI)
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(authMiddleware);
|
app.use(authMiddleware);
|
||||||
@@ -79,22 +87,36 @@ app.get('/api/auth-status', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/login', (req, res) => {
|
app.post('/api/login', (req, res) => {
|
||||||
if (!AUTH_ENABLED) return res.json({ success: true, token: 'none' });
|
if (!AUTH_ENABLED) return res.json({ success: true, token: 'none', memberId: null });
|
||||||
|
|
||||||
|
const { password, memberId } = req.body;
|
||||||
|
|
||||||
|
// Validate member exists
|
||||||
|
if (!memberId || !FAMILY_MEMBERS.find(m => m.id === memberId)) {
|
||||||
|
return res.status(400).json({ success: false, error: 'Válassz egy családtagot' });
|
||||||
|
}
|
||||||
|
|
||||||
const { password } = req.body;
|
|
||||||
if (password === AUTH_PASSWORD) {
|
if (password === AUTH_PASSWORD) {
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
sessions.set(token, { created: Date.now() });
|
sessions.set(token, { created: Date.now(), memberId });
|
||||||
// Clean old sessions (>24h)
|
// Clean old sessions (>7 days)
|
||||||
for (const [t, s] of sessions) {
|
for (const [t, s] of sessions) {
|
||||||
if (Date.now() - s.created > 86400000) sessions.delete(t);
|
if (Date.now() - s.created > 7 * 86400000) sessions.delete(t);
|
||||||
}
|
}
|
||||||
res.json({ success: true, token });
|
res.json({ success: true, token, memberId });
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json({ success: false, error: 'Hibás jelszó' });
|
res.status(401).json({ success: false, error: 'Hibás jelszó' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Who am I endpoint
|
||||||
|
app.get('/api/me', (req, res) => {
|
||||||
|
const memberId = getMember(req);
|
||||||
|
if (!memberId) return res.json({ memberId: null });
|
||||||
|
const member = FAMILY_MEMBERS.find(m => m.id === memberId);
|
||||||
|
res.json({ memberId, member });
|
||||||
|
});
|
||||||
|
|
||||||
// Members endpoint
|
// Members endpoint
|
||||||
app.get('/api/members', (req, res) => {
|
app.get('/api/members', (req, res) => {
|
||||||
res.json(FAMILY_MEMBERS);
|
res.json(FAMILY_MEMBERS);
|
||||||
@@ -107,7 +129,9 @@ app.get('/api/bookings', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/bookings', (req, res) => {
|
app.post('/api/bookings', (req, res) => {
|
||||||
const { member_id, start_date, end_date, status } = req.body;
|
const { start_date, end_date, status } = req.body;
|
||||||
|
// Use logged-in member if auth enabled, otherwise accept from body
|
||||||
|
const member_id = AUTH_ENABLED ? getMember(req) : req.body.member_id;
|
||||||
if (!member_id || !start_date || !end_date) {
|
if (!member_id || !start_date || !end_date) {
|
||||||
return res.status(400).json({ error: 'Missing fields' });
|
return res.status(400).json({ error: 'Missing fields' });
|
||||||
}
|
}
|
||||||
@@ -135,7 +159,8 @@ app.get('/api/comments', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/comments', (req, res) => {
|
app.post('/api/comments', (req, res) => {
|
||||||
const { member_id, text } = req.body;
|
const { text } = req.body;
|
||||||
|
const member_id = AUTH_ENABLED ? getMember(req) : req.body.member_id;
|
||||||
if (!member_id || !text) {
|
if (!member_id || !text) {
|
||||||
return res.status(400).json({ error: 'Missing fields' });
|
return res.status(400).json({ error: 'Missing fields' });
|
||||||
}
|
}
|
||||||
@@ -145,6 +170,17 @@ app.post('/api/comments', (req, res) => {
|
|||||||
res.json({ id: result.lastInsertRowid });
|
res.json({ id: result.lastInsertRowid });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.delete('/api/comments/:id', (req, res) => {
|
||||||
|
const memberId = AUTH_ENABLED ? getMember(req) : req.body.member_id;
|
||||||
|
const comment = db.prepare('SELECT * FROM comments WHERE id = ?').get(req.params.id);
|
||||||
|
if (!comment) return res.status(404).json({ error: 'Not found' });
|
||||||
|
if (AUTH_ENABLED && comment.member_id !== memberId) {
|
||||||
|
return res.status(403).json({ error: 'Csak a saját hozzászólásod törölheted' });
|
||||||
|
}
|
||||||
|
db.prepare('DELETE FROM comments WHERE id = ?').run(req.params.id);
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
// Serve static frontend
|
// Serve static frontend
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
app.get('*', (req, res) => {
|
app.get('*', (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user