Files
CalendarReminder/CalendarReminder.Api/wwwroot/index.html
T
2026-06-05 21:39:38 +02:00

932 lines
39 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="sk">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Calendar Reminder</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%; width: 100%;
font-family: 'Segoe UI', sans-serif;
background: #0d1117; color: #e1e4e8;
overflow: hidden;
}
/* ══════════════════════════════
AUTH
══════════════════════════════ */
#auth-screen {
display: flex; align-items: center; justify-content: center;
height: 100%; background: linear-gradient(135deg, #1a1d2e, #0d1117);
}
.auth-card {
background: #161b22; border: 1px solid #30363d; border-radius: 14px;
padding: 2.25rem; width: 100%; max-width: 420px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.auth-logo { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.3rem; }
.auth-logo .logo-icon { font-size: 1.8rem; }
.auth-logo h1 { font-size: 1.45rem; color: #58a6ff; font-weight: 700; letter-spacing: -0.3px; }
.auth-card > p { color: #8b949e; font-size: 0.88rem; margin-bottom: 1.75rem; padding-left: 2.4rem; }
.tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
.tab-btn { flex: 1; padding: 0.5rem; border: 1px solid #30363d; border-radius: 7px; background: transparent; color: #8b949e; cursor: pointer; font-size: 0.9rem; transition: all 0.15s; }
.tab-btn.active { background: #1f6feb; border-color: #1f6feb; color: #fff; font-weight: 600; }
.fg { margin-bottom: 0.9rem; }
.fg label { display: block; font-size: 0.8rem; color: #8b949e; margin-bottom: 0.35rem; text-transform: uppercase; letter-spacing: 0.4px; }
.fg input { width: 100%; padding: 0.6rem 0.85rem; background: #0d1117; border: 1px solid #30363d; border-radius: 7px; color: #e1e4e8; font-size: 0.95rem; outline: none; transition: border-color 0.15s; }
.fg input:focus { border-color: #58a6ff; }
.btn-primary { width: 100%; padding: 0.72rem; background: #238636; border: none; border-radius: 7px; color: #fff; font-size: 0.95rem; font-weight: 600; cursor: pointer; margin-top: 0.25rem; transition: background 0.15s; }
.btn-primary:hover { background: #2ea043; }
.err { color: #f85149; font-size: 0.82rem; margin-top: 0.65rem; text-align: center; min-height: 1.1rem; }
/* ══════════════════════════════
APP SHELL — 3-row grid
row1: topbar (48px)
row2: app body (fill)
══════════════════════════════ */
#app-screen {
display: none;
flex-direction: column;
height: 100%;
}
/* ── TOP BAR ── */
.topbar {
height: 48px; min-height: 48px;
background: #161b22;
border-bottom: 1px solid #30363d;
display: flex; align-items: center;
padding: 0 1.5rem;
gap: 1rem;
width: 100%;
flex-shrink: 0;
z-index: 100;
}
.topbar-brand {
display: flex; align-items: center; gap: 0.55rem;
flex: 0 0 auto;
}
.topbar-brand .brand-icon { font-size: 1.2rem; }
.topbar-brand h1 {
font-size: 1rem; font-weight: 700; color: #58a6ff;
letter-spacing: -0.2px; white-space: nowrap;
}
.topbar-spacer { flex: 1; }
.topbar-user {
display: flex; align-items: center; gap: 0.9rem;
flex: 0 0 auto;
}
.topbar-avatar {
width: 28px; height: 28px; border-radius: 50%;
background: linear-gradient(135deg, #1f6feb, #388bfd);
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 700; color: #fff; flex-shrink: 0;
}
.topbar-username { font-size: 0.88rem; color: #c9d1d9; font-weight: 500; }
.btn-logout {
padding: 0.28rem 0.85rem; background: transparent;
border: 1px solid #30363d; border-radius: 6px;
color: #8b949e; cursor: pointer; font-size: 0.8rem;
transition: all 0.15s;
}
.btn-logout:hover { border-color: #f85149; color: #f85149; }
/* ── APP BODY ── */
.app-body {
flex: 1;
display: flex;
min-height: 0; /* critical for flex child overflow */
}
/* ══════════════════════════════
CALENDAR PANEL — fills remaining space
══════════════════════════════ */
.cal-panel {
flex: 1 1 0%;
min-width: 0;
border-right: 1px solid #30363d;
display: flex; flex-direction: column;
overflow: hidden;
}
.cal-header {
height: 48px; min-height: 48px;
display: flex; align-items: center; justify-content: space-between;
padding: 0 1.25rem;
border-bottom: 1px solid #30363d;
flex-shrink: 0;
}
.cal-title { font-size: 1rem; font-weight: 700; color: #c9d1d9; min-width: 200px; }
.cal-nav { display: flex; gap: 0.4rem; align-items: center; }
.btn-nav {
padding: 0.28rem 0.75rem; background: transparent; border: 1px solid #30363d;
border-radius: 6px; color: #8b949e; cursor: pointer; font-size: 0.9rem;
transition: all 0.15s; line-height: 1;
}
.btn-nav:hover { border-color: #58a6ff; color: #58a6ff; }
.btn-today {
padding: 0.28rem 0.85rem; background: #1f6feb18; border: 1px solid #1f6feb55;
border-radius: 6px; color: #58a6ff; cursor: pointer; font-size: 0.8rem; font-weight: 600;
transition: all 0.15s;
}
.btn-today:hover { background: #1f6feb33; }
/* Calendar grid */
.cal-grid-wrap {
flex: 1;
min-height: 0;
display: flex; flex-direction: column;
padding: 0.6rem;
overflow: hidden;
}
.cal-weekdays {
display: grid; grid-template-columns: repeat(7, 1fr);
margin-bottom: 0.4rem; flex-shrink: 0;
}
.cal-weekday {
text-align: center; font-size: 0.72rem; color: #6e7681;
font-weight: 700; padding: 0.25rem 0; text-transform: uppercase; letter-spacing: 0.5px;
}
.cal-days {
flex: 1;
display: grid; grid-template-columns: repeat(7, 1fr);
grid-auto-rows: 1fr;
gap: 4px;
min-height: 0;
}
.cal-day {
background: #161b22; border: 1px solid #21262d;
border-radius: 6px; padding: 0.3rem 0.4rem;
cursor: pointer; transition: border-color 0.12s;
overflow: hidden; display: flex; flex-direction: column;
min-height: 0;
}
.cal-day:hover { border-color: #388bfd55; }
.cal-day.other-month { opacity: 0.28; }
.cal-day.today { border-color: #388bfd; background: #0d1f3c; }
.cal-day.selected { border-color: #58a6ff; background: #0d1f3c; }
.cal-day.has-reminder { background: #12192a; }
.day-num {
font-size: 0.72rem; font-weight: 700; color: #6e7681;
text-align: right; flex-shrink: 0; margin-bottom: 2px;
}
.cal-day.today .day-num { color: #58a6ff; }
.cal-day-events { flex: 1; overflow: hidden; display: flex; flex-direction: column; gap: 2px; min-height: 0; }
.cal-reminder-dot {
font-size: 0.67rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
padding: 1px 5px; border-radius: 3px; flex-shrink: 0; cursor: pointer; line-height: 1.5;
}
.dot-mine { background: #0f2a0f; color: #3fb950; border-left: 2px solid #3fb950; }
.dot-shared { background: #0d1f3c; color: #79c0ff; border-left: 2px solid #388bfd; }
.dot-done { background: #161b22; color: #484f58; text-decoration: line-through; }
.dot-overdue { background: #2d1515; color: #f85149; border-left: 2px solid #f85149; }
.dot-more { font-size: 0.63rem; color: #484f58; padding: 0 4px; flex-shrink: 0; }
/* ══════════════════════════════
SIDEBAR — fixed 350px
══════════════════════════════ */
.sidebar {
flex: 0 0 350px; width: 350px; min-width: 350px;
display: flex; flex-direction: column;
overflow: hidden;
}
/* Add form */
.add-section {
border-bottom: 1px solid #30363d;
padding: 0.9rem 1rem;
flex-shrink: 0;
background: #0d1117;
}
.add-section h3 { font-size: 0.8rem; color: #8b949e; margin-bottom: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.add-section input, .add-section textarea {
width: 100%; padding: 0.48rem 0.65rem;
background: #161b22; border: 1px solid #30363d; border-radius: 6px;
color: #e1e4e8; font-size: 0.85rem; outline: none;
font-family: inherit; margin-bottom: 0.45rem;
transition: border-color 0.15s;
}
.add-section input:focus, .add-section textarea:focus { border-color: #58a6ff; }
.add-section textarea { resize: none; height: 44px; }
.date-row { display: flex; gap: 0.45rem; }
.date-row input { margin-bottom: 0; }
.date-row input[type="date"] { flex: 1 1 55%; }
.date-row input[type="time"] { flex: 1 1 45%; }
.add-section input[type="date"]::-webkit-calendar-picker-indicator,
.add-section input[type="time"]::-webkit-calendar-picker-indicator {
filter: invert(0.65) brightness(1.4);
cursor: pointer; opacity: 0.7;
}
.add-section input[type="date"]::-webkit-calendar-picker-indicator:hover,
.add-section input[type="time"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
.user-picker { position: relative; margin-bottom: 0.45rem; }
.user-picker-tags { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: 0.3rem; }
.user-tag {
display: flex; align-items: center; gap: 3px;
background: #0d1f3c; border: 1px solid #1f6feb55; border-radius: 10px;
padding: 1px 8px; font-size: 0.73rem; color: #79c0ff;
}
.user-tag button { background: none; border: none; color: #79c0ff; cursor: pointer; font-size: 0.8rem; padding: 0; line-height: 1; }
.user-picker-input {
width: 100%; padding: 0.48rem 0.65rem;
background: #161b22; border: 1px solid #30363d; border-radius: 6px;
color: #e1e4e8; font-size: 0.85rem; outline: none; cursor: pointer;
transition: border-color 0.15s;
}
.user-picker-input:focus { border-color: #58a6ff; }
.user-dropdown {
display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 200;
background: #1c1f2e; border: 1px solid #30363d; border-radius: 6px;
max-height: 150px; overflow-y: auto; margin-top: 2px;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
}
.user-dropdown.open { display: block; }
.user-option { padding: 0.42rem 0.7rem; font-size: 0.82rem; cursor: pointer; display: flex; align-items: center; justify-content: space-between; }
.user-option:hover { background: #30363d; }
.user-option.selected { color: #3fb950; }
.user-option .uname { font-weight: 600; }
.user-option .uemail { font-size: 0.73rem; color: #6e7681; }
.btn-add-reminder {
width: 100%; padding: 0.48rem; background: #238636; border: none;
border-radius: 6px; color: #fff; font-size: 0.85rem; font-weight: 600; cursor: pointer;
transition: background 0.15s;
}
.btn-add-reminder:hover { background: #2ea043; }
/* List */
.list-section { flex: 1; overflow-y: auto; padding: 0.9rem 1rem; min-height: 0; }
.list-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.55rem; }
.list-header h3 { font-size: 0.8rem; color: #8b949e; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.filters { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 0.6rem; }
.filter-btn {
padding: 0.22rem 0.6rem; border: 1px solid #30363d; border-radius: 10px;
background: transparent; color: #8b949e; cursor: pointer; font-size: 0.73rem;
transition: all 0.12s;
}
.filter-btn.active { background: #1f6feb; border-color: #1f6feb; color: #fff; }
.filter-btn:hover:not(.active) { border-color: #484f58; color: #c9d1d9; }
.reminder-item {
background: #161b22; border: 1px solid #21262d; border-radius: 7px;
padding: 0.55rem 0.7rem; margin-bottom: 0.4rem; cursor: default;
transition: border-color 0.12s;
}
.reminder-item:hover { border-color: #30363d; }
.reminder-item.overdue { border-left: 3px solid #f85149; }
.reminder-item.soon { border-left: 3px solid #e3b341; }
.reminder-item.completed { opacity: 0.4; }
.reminder-item.shared-with-me { border-left: 3px solid #388bfd; }
.ri-top { display: flex; align-items: flex-start; gap: 0.45rem; }
.check-btn {
width: 17px; height: 17px; border-radius: 50%; border: 2px solid #30363d;
background: transparent; cursor: pointer; flex-shrink: 0; margin-top: 2px;
display: flex; align-items: center; justify-content: center; font-size: 9px; color: #fff;
transition: all 0.12s;
}
.check-btn.done { background: #238636; border-color: #238636; }
.ri-title { font-size: 0.85rem; font-weight: 600; flex: 1; line-height: 1.3; }
.ri-title.done { text-decoration: line-through; color: #484f58; }
.ri-actions { display: flex; gap: 3px; opacity: 0; transition: opacity 0.12s; }
.reminder-item:hover .ri-actions { opacity: 1; }
.btn-icon { padding: 2px 5px; background: transparent; border: 1px solid #30363d; border-radius: 4px; color: #8b949e; cursor: pointer; font-size: 0.72rem; }
.btn-icon:hover { border-color: #f85149; color: #f85149; }
.ri-meta { font-size: 0.73rem; color: #6e7681; margin-top: 0.22rem; display: flex; justify-content: space-between; }
.ri-badge { display: inline-block; padding: 1px 5px; border-radius: 6px; font-size: 0.68rem; font-weight: 700; margin-left: 4px; }
.badge-red { background: #2d1515; color: #f85149; }
.badge-yellow { background: #2d2700; color: #e3b341; }
.empty-state { text-align: center; padding: 2.5rem 1rem; color: #484f58; font-size: 0.85rem; }
/* Scrollbar styling */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #30363d; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #484f58; }
/* ══════════════════════════════
EDIT MODAL
══════════════════════════════ */
.modal-overlay {
display: none; position: fixed; inset: 0;
background: rgba(0,0,0,0.75); align-items: center; justify-content: center; z-index: 300;
}
.modal-overlay.open { display: flex; }
.modal {
background: #1c1f2e; border: 1px solid #30363d; border-radius: 12px;
padding: 1.5rem; width: 100%; max-width: 440px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
.modal-head h2 { font-size: 0.95rem; color: #c9d1d9; margin: 0; }
.btn-send-email {
padding: 0.3rem 0.65rem; background: transparent;
border: 1px solid #30363d; border-radius: 6px;
color: #8b949e; cursor: pointer; font-size: 1rem;
transition: all 0.15s; line-height: 1;
}
.btn-send-email:hover { border-color: #58a6ff; color: #58a6ff; }
.btn-send-email:disabled { opacity: 0.4; cursor: not-allowed; }
.send-status { font-size: 0.78rem; margin-top: 0.4rem; min-height: 1rem; text-align: right; }
.modal .fg { margin-bottom: 0.75rem; }
.modal .fg label { display: block; font-size: 0.78rem; color: #8b949e; margin-bottom: 0.3rem; text-transform: uppercase; letter-spacing: 0.4px; }
.modal .fg input { width: 100%; padding: 0.5rem 0.7rem; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; font-size: 0.9rem; outline: none; }
.modal .fg input:focus { border-color: #58a6ff; }
.modal-actions { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1.1rem; }
.btn-cancel { padding: 0.45rem 1rem; background: transparent; border: 1px solid #30363d; border-radius: 6px; color: #8b949e; cursor: pointer; }
.btn-save { padding: 0.45rem 1.1rem; background: #1f6feb; border: none; border-radius: 6px; color: #fff; cursor: pointer; font-weight: 600; }
</style>
</head>
<body>
<!-- ══════════ AUTH ══════════ -->
<div id="auth-screen">
<div class="auth-card">
<div class="auth-logo">
<span class="logo-icon">📅</span>
<h1>Calendar Reminder</h1>
</div>
<p>Správa udalostí s emailovými notifikáciami</p>
<div class="tabs">
<button class="tab-btn active" onclick="showTab('login')">Prihlásiť sa</button>
<button class="tab-btn" onclick="showTab('register')">Registrácia</button>
</div>
<div id="login-form">
<div class="fg"><label>Email</label><input type="email" id="l-email" placeholder="vas@email.com" /></div>
<div class="fg"><label>Heslo</label><input type="password" id="l-pass" placeholder="••••••••" onkeydown="if(event.key==='Enter')doLogin()" /></div>
<button class="btn-primary" onclick="doLogin()">Prihlásiť sa</button>
<div class="err" id="l-err"></div>
</div>
<div id="register-form" style="display:none">
<div class="fg"><label>Meno</label><input type="text" id="r-name" placeholder="Ján Novák" /></div>
<div class="fg"><label>Email</label><input type="email" id="r-email" placeholder="vas@email.com" /></div>
<div class="fg"><label>Heslo</label><input type="password" id="r-pass" placeholder="min. 6 znakov" onkeydown="if(event.key==='Enter')doRegister()" /></div>
<button class="btn-primary" onclick="doRegister()">Vytvoriť účet</button>
<div class="err" id="r-err"></div>
</div>
</div>
</div>
<!-- ══════════ APP ══════════ -->
<div id="app-screen">
<!-- TOP BAR -->
<div class="topbar">
<div class="topbar-brand">
<span class="brand-icon">📅</span>
<h1>Calendar Reminder</h1>
</div>
<div class="topbar-spacer"></div>
<div class="topbar-user">
<div class="topbar-avatar" id="hdr-avatar">?</div>
<span class="topbar-username" id="hdr-user"></span>
<button class="btn-logout" onclick="doLogout()">Odhlásiť sa</button>
</div>
</div>
<!-- APP BODY -->
<div class="app-body">
<!-- CALENDAR (flex: 1) -->
<div class="cal-panel">
<div class="cal-header">
<div class="cal-nav">
<button class="btn-nav" onclick="changeMonth(-1)">&#8249;</button>
<button class="btn-nav" onclick="changeMonth(1)">&#8250;</button>
<button class="btn-today" onclick="goToday()">Dnes</button>
</div>
<div class="cal-title" id="cal-title"></div>
<div style="min-width:120px"></div>
</div>
<div class="cal-grid-wrap">
<div class="cal-weekdays">
<div class="cal-weekday">Pon</div>
<div class="cal-weekday">Uto</div>
<div class="cal-weekday">Str</div>
<div class="cal-weekday">Štv</div>
<div class="cal-weekday">Pia</div>
<div class="cal-weekday">Sob</div>
<div class="cal-weekday">Ned</div>
</div>
<div class="cal-days" id="cal-days"></div>
</div>
</div>
<!-- SIDEBAR (fixed 350px) -->
<div class="sidebar">
<div class="add-section">
<h3>Nová pripomienka</h3>
<input type="text" id="n-title" placeholder="Názov *" />
<div class="date-row">
<input type="date" id="n-date-day"
onfocus="document.getElementById('picker-dropdown').classList.remove('open')"
onchange="document.getElementById('picker-dropdown').classList.remove('open')" />
<input type="time" id="n-date-time" value="08:00"
onfocus="document.getElementById('picker-dropdown').classList.remove('open')"
onchange="this.blur()" />
</div>
<textarea id="n-desc" placeholder="Popis (voliteľné)"></textarea>
<div class="user-picker" id="user-picker">
<div class="user-picker-tags" id="picker-tags"></div>
<input class="user-picker-input" id="picker-input"
placeholder="👥 Zdieľať s používateľom..."
autocomplete="off"
onfocus="openPicker()"
oninput="searchUsers(this.value)" />
<div class="user-dropdown" id="picker-dropdown"></div>
</div>
<button class="btn-add-reminder" onclick="createReminder()">Pridať pripomienku</button>
</div>
<div class="list-section">
<div class="list-header">
<h3>Zoznam</h3>
</div>
<div class="filters">
<button class="filter-btn active" onclick="setFilter('all',this)">Všetky</button>
<button class="filter-btn" onclick="setFilter('week',this)">Tento týždeň</button>
<button class="filter-btn" onclick="setFilter('today',this)">Dnes</button>
<button class="filter-btn" onclick="setFilter('upcoming',this)">Nadchádzajúce</button>
<button class="filter-btn" onclick="setFilter('completed',this)">Dokončené</button>
</div>
<div id="reminder-list"></div>
</div>
</div>
</div>
</div>
<!-- EDIT MODAL -->
<div class="modal-overlay" id="edit-modal">
<div class="modal">
<div class="modal-head">
<h2>✏️ Upraviť pripomienku</h2>
<button class="btn-send-email" id="btn-send-email" onclick="sendReminderEmail()" title="Odoslať email zdieľaným používateľom">✉️</button>
</div>
<input type="hidden" id="e-id" />
<div class="fg"><label>Názov</label><input type="text" id="e-title" /></div>
<div class="fg"><label>Dátum a čas</label><input type="datetime-local" id="e-date" /></div>
<div class="fg"><label>Popis</label><input type="text" id="e-desc" /></div>
<div class="send-status" id="send-status"></div>
<div class="modal-actions">
<button class="btn-cancel" onclick="closeModal()">Zrušiť</button>
<button class="btn-save" onclick="saveEdit()">Uložiť</button>
</div>
</div>
</div>
<script>
const API = '/api';
const v = id => document.getElementById(id).value;
const auth = () => ({ 'Authorization': `Bearer ${token}` });
const MONTHS_SK = ['Január','Február','Marec','Apríl','Máj','Jún','Júl','August','September','Október','November','December'];
let token = localStorage.getItem('token');
let myId = parseInt(localStorage.getItem('myId') || '0');
let reminders = [];
let currentFilter = 'all';
let calYear, calMonth;
let selectedUsers = [];
let allUsers = [];
let pickerDebounce = null;
const now0 = new Date();
calYear = now0.getFullYear();
calMonth = now0.getMonth();
if (token) showApp();
// ═══ AUTH ═══
function showTab(t) {
document.querySelectorAll('.tab-btn').forEach((b,i) => b.classList.toggle('active', i===(t==='login'?0:1)));
document.getElementById('login-form').style.display = t==='login' ? 'block':'none';
document.getElementById('register-form').style.display = t==='register' ? 'block':'none';
}
async function doLogin() {
const res = await fetch(`${API}/auth/login`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ email: v('l-email'), password: v('l-pass') })
});
if (res.ok) { const d = await res.json(); saveAuth(d); showApp(); }
else document.getElementById('l-err').textContent = await res.text();
}
async function doRegister() {
const res = await fetch(`${API}/auth/register`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ username: v('r-name'), email: v('r-email'), password: v('r-pass') })
});
if (res.ok) { const d = await res.json(); saveAuth(d); showApp(); }
else document.getElementById('r-err').textContent = await res.text();
}
function saveAuth(d) {
localStorage.setItem('token', d.token);
localStorage.setItem('user', JSON.stringify(d));
try {
const payload = JSON.parse(atob(d.token.split('.')[1]));
const uid = payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'] || payload.sub;
myId = parseInt(uid);
localStorage.setItem('myId', String(myId));
} catch {}
token = d.token;
}
function doLogout() {
localStorage.clear(); token = null; myId = 0;
document.getElementById('auth-screen').style.display = 'flex';
document.getElementById('app-screen').style.display = 'none';
}
function showApp() {
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('app-screen').style.display = 'flex';
const u = JSON.parse(localStorage.getItem('user') || '{}');
const name = u.username || '';
document.getElementById('hdr-user').textContent = name;
document.getElementById('hdr-avatar').textContent = name ? name[0].toUpperCase() : '?';
loadReminders();
}
// ═══ DATA ═══
async function loadReminders() {
try {
const res = await fetch(`${API}/reminders`, { headers: auth() });
if (res.status === 401) { doLogout(); return; }
if (!res.ok) {
console.error('API error:', res.status, await res.text());
renderCalendar();
renderList();
return;
}
reminders = await res.json();
} catch (e) {
console.error('Fetch failed:', e);
reminders = [];
}
renderCalendar();
renderList();
}
// ═══ CALENDAR ═══
function renderCalendar() {
document.getElementById('cal-title').textContent = `${MONTHS_SK[calMonth]} ${calYear}`;
const today = new Date();
const firstDay = new Date(calYear, calMonth, 1);
let startDow = firstDay.getDay();
startDow = startDow === 0 ? 6 : startDow - 1;
const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate();
const daysInPrev = new Date(calYear, calMonth, 0).getDate();
const grid = document.getElementById('cal-days');
grid.innerHTML = '';
const totalCells = Math.ceil((startDow + daysInMonth) / 7) * 7;
const rowCount = totalCells / 7;
grid.style.gridTemplateRows = `repeat(${rowCount}, 1fr)`;
for (let i = 0; i < totalCells; i++) {
let dayNum, month = calMonth, year = calYear, otherMonth = false;
if (i < startDow) {
dayNum = daysInPrev - startDow + i + 1;
month = calMonth - 1; if (month < 0) { month = 11; year = calYear - 1; }
otherMonth = true;
} else if (i >= startDow + daysInMonth) {
dayNum = i - startDow - daysInMonth + 1;
month = calMonth + 1; if (month > 11) { month = 0; year = calYear + 1; }
otherMonth = true;
} else {
dayNum = i - startDow + 1;
}
const isToday = dayNum === today.getDate() && month === today.getMonth() && year === today.getFullYear();
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(dayNum).padStart(2,'0')}`;
const dayRems = reminders.filter(r => r.reminderDate.startsWith(dateStr));
const cell = document.createElement('div');
cell.className = `cal-day${otherMonth?' other-month':''}${isToday?' today':''}${dayRems.length?' has-reminder':''}`;
cell.dataset.date = dateStr;
const numEl = document.createElement('div');
numEl.className = 'day-num';
numEl.textContent = dayNum;
cell.appendChild(numEl);
const eventsEl = document.createElement('div');
eventsEl.className = 'cal-day-events';
dayRems.forEach(r => {
const rd = new Date(r.reminderDate);
const now2 = new Date();
let cls = 'dot-mine';
if (r.isCompleted) cls = 'dot-done';
else if (!r.isOwner) cls = 'dot-shared';
else if (rd < now2) cls = 'dot-overdue';
const time = rd.toLocaleTimeString('sk-SK', {hour:'2-digit',minute:'2-digit'});
const dot = document.createElement('div');
dot.className = `cal-reminder-dot ${cls}`;
dot.title = `${r.title}${time}`;
dot.textContent = `${time} ${r.title}`;
dot.onclick = e => { e.stopPropagation(); openEdit(r.id); };
eventsEl.appendChild(dot);
});
// placeholder pre "+N ďalší" — naplní sa po meraní
const morePlaceholder = document.createElement('div');
morePlaceholder.className = 'dot-more';
morePlaceholder.style.display = 'none';
eventsEl.appendChild(morePlaceholder);
cell.appendChild(eventsEl);
cell.onclick = () => filterByDay(dateStr, cell);
grid.appendChild(cell);
}
// Po vykreslení zmeraj a orizni každé políčko
requestAnimationFrame(() => trimCalendarCells());
}
function trimCalendarCells() {
document.querySelectorAll('.cal-day').forEach(cell => {
const eventsEl = cell.querySelector('.cal-day-events');
if (!eventsEl) return;
const dots = [...eventsEl.querySelectorAll('.cal-reminder-dot')];
const morePlaceholder = eventsEl.querySelector('.dot-more');
if (!dots.length) return;
// Dostupná výška pre udalosti
const numEl = cell.querySelector('.day-num');
const available = cell.clientHeight - (numEl ? numEl.offsetHeight : 0) - 6;
// Zobraz všetky, zmer koľko sa zmestí
dots.forEach(d => d.style.display = '');
if (morePlaceholder) morePlaceholder.style.display = 'none';
let used = 0;
let lastFit = dots.length - 1;
const GAP = 2;
for (let i = 0; i < dots.length; i++) {
used += dots[i].offsetHeight + GAP;
if (used > available) {
lastFit = i - 1;
break;
}
}
const hidden = dots.length - lastFit - 1;
if (hidden <= 0) return;
// Ak sa zmestí aspoň riadok "+N ďalší", schováme posledný viditeľný a nahradíme ho
// Inak schováme od lastFit
for (let i = lastFit + 1; i < dots.length; i++) {
dots[i].style.display = 'none';
}
if (morePlaceholder) {
morePlaceholder.textContent = `+${hidden} ďalší`;
morePlaceholder.style.display = '';
}
});
}
function filterByDay(dateStr, cell) {
const alreadySelected = cell.classList.contains('selected');
document.querySelectorAll('.cal-day.selected').forEach(c => c.classList.remove('selected'));
if (alreadySelected) {
// Druhé kliknutie na rovnaký deň → reset na všetky
resetToAll();
return;
}
cell.classList.add('selected');
// Filtruj list podľa dňa — vlastné aj zdieľané
renderListData(reminders.filter(r => r.reminderDate.startsWith(dateStr)));
}
function changeMonth(dir) {
calMonth += dir;
if (calMonth > 11) { calMonth = 0; calYear++; }
if (calMonth < 0) { calMonth = 11; calYear--; }
renderCalendar();
}
function goToday() {
const t = new Date();
calYear = t.getFullYear(); calMonth = t.getMonth();
renderCalendar();
}
// ═══ LIST ═══
function setFilter(f, btn) {
currentFilter = f;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Zruš výber dňa v kalendári — list teraz zobrazuje podľa filtra, nie dňa
document.querySelectorAll('.cal-day.selected').forEach(c => c.classList.remove('selected'));
renderList();
}
function resetToAll() {
currentFilter = 'all';
document.querySelectorAll('.filter-btn').forEach((b, i) => b.classList.toggle('active', i === 0));
document.querySelectorAll('.cal-day.selected').forEach(c => c.classList.remove('selected'));
renderList();
}
function renderList() {
const now = new Date();
const weekEnd = new Date(now); weekEnd.setDate(now.getDate() + 7);
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const todayEnd = new Date(todayStart); todayEnd.setDate(todayStart.getDate() + 1);
let filtered = reminders.filter(r => {
const d = new Date(r.reminderDate);
switch (currentFilter) {
case 'week': return d >= todayStart && d <= weekEnd;
case 'today': return d >= todayStart && d < todayEnd;
case 'upcoming': return !r.isCompleted && d >= now;
case 'completed': return r.isCompleted;
default: return true; // 'all' — vlastné aj zdieľané, všetky
}
});
renderListData(filtered);
}
function renderListData(list) {
const el = document.getElementById('reminder-list');
if (!list.length) {
el.innerHTML = '<div class="empty-state">📭 Žiadne pripomienky</div>';
return;
}
const now = new Date();
el.innerHTML = list.map(r => {
const d = new Date(r.reminderDate);
const diffMin = (d - now) / 60000;
let cls = '', badge = '';
if (!r.isCompleted) {
if (!r.isOwner) { cls = 'shared-with-me'; }
else if (diffMin < 0) { cls = 'overdue'; badge = '<span class="ri-badge badge-red">Po termíne</span>'; }
else if (diffMin < 60) { cls = 'soon'; badge = '<span class="ri-badge badge-yellow">Čoskoro</span>'; }
}
const dateStr = d.toLocaleDateString('sk-SK') + ' ' + d.toLocaleTimeString('sk-SK', {hour:'2-digit',minute:'2-digit'});
const shared = (r.sharedWith||[]).map(u=>`@${u.username}`).join(', ');
return `
<div class="reminder-item ${cls} ${r.isCompleted?'completed':''}">
<div class="ri-top">
<button class="check-btn ${r.isCompleted?'done':''}"
onclick="toggleComplete(${r.id},${r.isCompleted})">${r.isCompleted?'✓':''}</button>
<div class="ri-title ${r.isCompleted?'done':''}">${r.title}${badge}</div>
<div class="ri-actions">
${r.isOwner?`<button class="btn-icon" onclick="openEdit(${r.id})">✏️</button>`:''}
${r.isOwner?`<button class="btn-icon" onclick="deleteReminder(${r.id})">🗑</button>`:''}
</div>
</div>
<div class="ri-meta">
<span>🗓 ${dateStr}</span>
<span>${!r.isOwner ? `od @${r.owner}` : shared ? `👥 ${shared}` : ''}</span>
</div>
</div>`;
}).join('');
}
// ═══ CRUD ═══
async function createReminder() {
const title = v('n-title').trim();
const dateDay = v('n-date-day');
const dateTime = v('n-date-time') || '00:00';
const desc = v('n-desc').trim();
if (!title || !dateDay) { alert('Vyplňte názov a dátum'); return; }
const reminderDate = new Date(`${dateDay}T${dateTime}`).toISOString();
await fetch(`${API}/reminders`, {
method:'POST', headers:{...auth(),'Content-Type':'application/json'},
body: JSON.stringify({ title, description: desc||null, reminderDate, shareWithUserIds: selectedUsers.map(u=>u.id) })
});
document.getElementById('n-title').value = '';
document.getElementById('n-date-day').value = '';
document.getElementById('n-date-time').value = '08:00';
document.getElementById('n-desc').value = '';
selectedUsers = []; renderPickerTags();
loadReminders();
}
async function toggleComplete(id, cur) {
const r = reminders.find(x=>x.id===id); if (!r) return;
await fetch(`${API}/reminders/${id}`, {
method:'PUT', headers:{...auth(),'Content-Type':'application/json'},
body: JSON.stringify({ title:r.title, description:r.description, reminderDate:r.reminderDate, isCompleted:!cur, shareWithUserIds:(r.sharedWith||[]).map(u=>u.id) })
});
loadReminders();
}
async function deleteReminder(id) {
if (!confirm('Zmazať túto pripomienku?')) return;
await fetch(`${API}/reminders/${id}`, { method:'DELETE', headers:auth() });
loadReminders();
}
// ═══ EDIT MODAL ═══
function openEdit(id) {
const r = reminders.find(x=>x.id===id); if (!r) return;
document.getElementById('e-id').value = r.id;
document.getElementById('e-title').value = r.title;
document.getElementById('e-desc').value = r.description||'';
const d = new Date(r.reminderDate);
document.getElementById('e-date').value = new Date(d - d.getTimezoneOffset()*60000).toISOString().slice(0,16);
document.getElementById('send-status').textContent = '';
// Zobraz tlačidlo obálky iba ak má zdieľaných používateľov
const hasShared = (r.sharedWith||[]).length > 0;
const btnEmail = document.getElementById('btn-send-email');
btnEmail.style.display = hasShared ? '' : 'none';
btnEmail.disabled = false;
document.getElementById('edit-modal').classList.add('open');
}
async function sendReminderEmail() {
const id = document.getElementById('e-id').value;
const btn = document.getElementById('btn-send-email');
const status = document.getElementById('send-status');
btn.disabled = true;
status.style.color = '#8b949e';
status.textContent = 'Odosielam...';
try {
const res = await fetch(`${API}/reminders/${id}/send-email`, {
method: 'POST', headers: auth()
});
if (res.ok) {
const data = await res.json();
status.style.color = '#3fb950';
status.textContent = `✓ Email odoslaný ${data.sent} používateľ${data.sent === 1 ? 'ovi' : 'om'}`;
} else {
const txt = await res.text();
status.style.color = '#f85149';
status.textContent = `✗ ${txt}`;
btn.disabled = false;
}
} catch {
status.style.color = '#f85149';
status.textContent = '✗ Chyba pripojenia';
btn.disabled = false;
}
}
function closeModal() { document.getElementById('edit-modal').classList.remove('open'); }
async function saveEdit() {
const id = document.getElementById('e-id').value;
const r = reminders.find(x=>x.id==id);
await fetch(`${API}/reminders/${id}`, {
method:'PUT', headers:{...auth(),'Content-Type':'application/json'},
body: JSON.stringify({ title:v('e-title'), description:v('e-desc')||null, reminderDate:new Date(v('e-date')).toISOString(), isCompleted:r.isCompleted, shareWithUserIds:(r.sharedWith||[]).map(u=>u.id) })
});
closeModal(); loadReminders();
}
// ═══ USER PICKER ═══
function openPicker() { searchUsers(''); }
async function searchUsers(query) {
clearTimeout(pickerDebounce);
pickerDebounce = setTimeout(async () => {
const res = await fetch(`${API}/users?search=${encodeURIComponent(query)}`, { headers: auth() });
if (!res.ok) return;
allUsers = await res.json();
renderDropdown();
}, 200);
}
function renderDropdown() {
const dd = document.getElementById('picker-dropdown');
if (!allUsers.length) { dd.classList.remove('open'); return; }
dd.innerHTML = allUsers.map(u => {
const sel = selectedUsers.some(s=>s.id===u.id);
return `<div class="user-option ${sel?'selected':''}" onclick="toggleUser(${u.id},'${u.username}')">
<div><span class="uname">${u.username}</span>&nbsp;<span class="uemail">${u.email}</span></div>
${sel ? '<span>✓</span>' : ''}
</div>`;
}).join('');
dd.classList.add('open');
}
function toggleUser(id, username) {
const idx = selectedUsers.findIndex(u=>u.id===id);
if (idx >= 0) selectedUsers.splice(idx, 1); else selectedUsers.push({id, username});
renderPickerTags(); renderDropdown();
}
function renderPickerTags() {
document.getElementById('picker-tags').innerHTML = selectedUsers.map(u =>
`<span class="user-tag">@${u.username}<button onclick="removeUser(${u.id})">×</button></span>`
).join('');
}
function removeUser(id) { selectedUsers = selectedUsers.filter(u=>u.id!==id); renderPickerTags(); }
document.addEventListener('click', e => {
if (!document.getElementById('user-picker').contains(e.target))
document.getElementById('picker-dropdown').classList.remove('open');
});
window.addEventListener('resize', () => {
requestAnimationFrame(() => trimCalendarCells());
});
</script>
</body>
</html>