added public calendar
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
<!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ó Elérhetőség</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,600;1,600&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: 24px 16px;
|
||||||
|
}
|
||||||
|
.container { max-width: 480px; margin: 0 auto; }
|
||||||
|
|
||||||
|
.header { text-align: center; margin-bottom: 24px; }
|
||||||
|
.header-sub { font-size: 12px; 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: 600; color: #3A332D; line-height: 1.2; }
|
||||||
|
.header-line { width: 40px; height: 2px; background: #C17F59; margin: 10px auto 0; border-radius: 1px; }
|
||||||
|
.header-desc { font-size: 14px; color: #8B7E74; margin-top: 10px; line-height: 1.5; }
|
||||||
|
|
||||||
|
.card { background: #FFF; border-radius: 14px; padding: 16px; margin-bottom: 14px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
|
||||||
|
|
||||||
|
.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: 36px; height: 36px;
|
||||||
|
cursor: pointer; font-size: 18px; color: #5C524A; display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.cal-nav button:hover { background: #EAE3DB; }
|
||||||
|
.cal-month { font-size: 18px; font-weight: 600; color: #3A332D; }
|
||||||
|
|
||||||
|
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
|
||||||
|
.cal-day-header { text-align: center; padding: 6px 0; font-size: 12px; font-weight: 600; color: #8B7E74; letter-spacing: 0.05em; }
|
||||||
|
.cal-day {
|
||||||
|
position: relative; min-height: 44px; padding: 6px 4px; border-radius: 8px;
|
||||||
|
background: #F0F7F0; border: 1px solid transparent;
|
||||||
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
.cal-day.empty { background: transparent; }
|
||||||
|
.cal-day.today { border: 2px solid #C17F59; }
|
||||||
|
.cal-day.occupied { background: #F5E6E0; }
|
||||||
|
.cal-day.past { background: #F0EDEA; opacity: 0.6; }
|
||||||
|
.cal-day-num { font-size: 15px; font-weight: 500; color: #3A332D; }
|
||||||
|
.cal-day.today .cal-day-num { font-weight: 700; color: #C17F59; }
|
||||||
|
.cal-day.past .cal-day-num { color: #B0A89E; }
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex; justify-content: center; gap: 20px; margin-bottom: 14px; padding: 8px 0;
|
||||||
|
}
|
||||||
|
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #5C524A; }
|
||||||
|
.legend-swatch { width: 16px; height: 16px; border-radius: 4px; }
|
||||||
|
.legend-swatch.free { background: #F0F7F0; }
|
||||||
|
.legend-swatch.occupied { background: #F5E6E0; }
|
||||||
|
|
||||||
|
.footer { text-align: center; font-size: 11px; color: #C8BFB5; padding: 10px 0 20px; }
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.header h1 { font-size: 24px; }
|
||||||
|
.cal-day { min-height: 40px; }
|
||||||
|
.cal-day-num { font-size: 14px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-sub">Révfülöp · Balaton</div>
|
||||||
|
<h1>Nyaraló Elérhetőség</h1>
|
||||||
|
<div class="header-line"></div>
|
||||||
|
<div class="header-desc">Mikor szabad a ház? Nézd meg a naptárt!</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item"><div class="legend-swatch free"></div> Szabad</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch occupied"></div> Foglalt</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 currentYear, currentMonth;
|
||||||
|
let bookings = [];
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const now = new Date();
|
||||||
|
currentYear = now.getFullYear();
|
||||||
|
currentMonth = now.getMonth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
bookings = await fetch('/api/public/bookings').then(r => r.json());
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load bookings:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevMonth() {
|
||||||
|
if (currentMonth === 0) { currentMonth = 11; currentYear--; } else currentMonth--;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function nextMonth() {
|
||||||
|
if (currentMonth === 11) { currentMonth = 0; currentYear++; } else currentMonth++;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isOccupied(dk) {
|
||||||
|
return bookings.some(b => inRange(dk, b.start_date, b.end_date));
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
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 dayDate = new Date(currentYear, currentMonth, d);
|
||||||
|
const isPast = dayDate < today;
|
||||||
|
const occupied = isOccupied(dk);
|
||||||
|
|
||||||
|
let classes = 'cal-day';
|
||||||
|
if (isToday) classes += ' today';
|
||||||
|
if (isPast) classes += ' past';
|
||||||
|
else if (occupied) classes += ' occupied';
|
||||||
|
|
||||||
|
html += `<div class="${classes}"><span class="cal-day-num">${d}</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('cal-grid').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -76,8 +76,8 @@ function getMember(req) {
|
|||||||
function authMiddleware(req, res, next) {
|
function authMiddleware(req, res, next) {
|
||||||
if (!AUTH_ENABLED) return next();
|
if (!AUTH_ENABLED) return next();
|
||||||
|
|
||||||
// Skip auth for login, auth-status, and members endpoints
|
// Skip auth for login, auth-status, members, config, and public endpoints
|
||||||
if (req.path === '/api/login' || req.path === '/api/auth-status' || req.path === '/api/members' || req.path === '/api/config') return next();
|
if (req.path === '/api/login' || req.path === '/api/auth-status' || req.path === '/api/members' || req.path === '/api/config' || req.path === '/api/public/bookings' || req.path === '/public') 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)) {
|
||||||
@@ -198,6 +198,17 @@ app.delete('/api/comments/:id', (req, res) => {
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Public bookings endpoint - returns only dates, no member info
|
||||||
|
app.get('/api/public/bookings', (req, res) => {
|
||||||
|
const bookings = db.prepare('SELECT start_date, end_date FROM bookings ORDER BY start_date').all();
|
||||||
|
res.json(bookings);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Public calendar page (no auth)
|
||||||
|
app.get('/public', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'public.html'));
|
||||||
|
});
|
||||||
|
|
||||||
// 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