diff --git a/frontend/css/app.css b/frontend/css/app.css index d3a38f0..74654d8 100644 --- a/frontend/css/app.css +++ b/frontend/css/app.css @@ -975,11 +975,15 @@ a { color: var(--primary); text-decoration: none; } } .ctx-item:hover { background: var(--bg-hover); } +<<<<<<< HEAD /* ── Event Popup ────────────────────────────────────────── Layout: Color-Dot + Title links, kleine Icon-Toolbar rechts oben. Icons sind im Ruhezustand transparent (nur das SVG selbst sichtbar), bekommen erst beim Hover einen runden farbigen Hintergrund. Wirkt modern und lässt dem Titel die meiste Breite. */ +======= +/* ── Event Popup ────────────────────────────────────────── */ +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 .event-popup { position: fixed; z-index: 600; background: var(--bg-surface); @@ -1697,12 +1701,24 @@ a { color: var(--primary); text-decoration: none; } .topbar-left { gap: 0; } .topbar-right { gap: 0; } +<<<<<<< HEAD /* Event-Popup auf Mobile: an Viewport-Breite anpassen */ .event-popup { width: min(94vw, 380px); max-width: 94vw; } .popup-header { padding: 10px 8px 10px 14px; } .popup-header h4 { font-size: 13.5px; } .popup-icon-btn { width: 32px; height: 32px; } .popup-icon-btn svg { width: 16px; height: 16px; } +======= + /* Event-Popup: Buttons kompakt halten, kein 44px-Override ───── */ + .event-popup .icon-btn { + min-width: 32px !important; + min-height: 32px !important; + width: 32px; + height: 32px; + } + .event-popup .popup-header { gap: 2px; padding: 10px 12px; } + .event-popup { width: min(92vw, 340px); max-width: 92vw; } +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 /* Monatsansicht: Startzeit ausblenden — nur Titel anzeigen ──── */ .month-event-time { display: none; } diff --git a/frontend/index.html b/frontend/index.html index 5ee5a49..8526ce5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,11 @@ +<<<<<<< HEAD Calendarr v17 +======= + Calendarr v11 +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 @@ -80,7 +84,11 @@ +<<<<<<< HEAD +======= + +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 @@ -185,7 +193,7 @@ Meine Kalender
+<<<<<<< HEAD +======= + +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 @@ -235,7 +247,11 @@
+<<<<<<< HEAD +======= + +>>>>>>> e744b1829e99db6b80922f75542ced329138e474
@@ -243,7 +259,11 @@
+<<<<<<< HEAD +======= + +>>>>>>> e744b1829e99db6b80922f75542ced329138e474
@@ -253,7 +273,11 @@
+<<<<<<< HEAD +======= + +>>>>>>> e744b1829e99db6b80922f75542ced329138e474
@@ -261,7 +285,11 @@
+<<<<<<< HEAD +======= + +>>>>>>> e744b1829e99db6b80922f75542ced329138e474
@@ -311,7 +339,11 @@
+<<<<<<< HEAD +======= + +>>>>>>> e744b1829e99db6b80922f75542ced329138e474
@@ -375,6 +407,7 @@ diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index e5fac64..abe53c4 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -115,6 +115,7 @@ export async function initCalendar() { bindProfileModal(); bindSwipeNavigation(); handleHAOAuthReturn(); +<<<<<<< HEAD // Browser-Back/Forward: URL-Hash → State synchronisieren window.addEventListener('hashchange', () => { @@ -131,6 +132,8 @@ export async function initCalendar() { } if (changed) fetchAndRender(); }); +======= +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 } function handleHAOAuthReturn() { @@ -144,7 +147,11 @@ function handleHAOAuthReturn() { }; if (params.has('ha_connected')) { showToast('Home Assistant verbunden'); +<<<<<<< HEAD window.history.replaceState({}, '', window.location.pathname + window.location.hash); +======= + window.history.replaceState({}, '', window.location.pathname); +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 fetchAndRender(true); api.get('/homeassistant/accounts').then(accs => { state.haAccounts = accs || []; @@ -154,7 +161,11 @@ function handleHAOAuthReturn() { } else if (params.has('ha_error')) { const code = params.get('ha_error'); showToast(errMap[code] || `HA-Anmeldung fehlgeschlagen: ${code}`, true); +<<<<<<< HEAD window.history.replaceState({}, '', window.location.pathname + window.location.hash); +======= + window.history.replaceState({}, '', window.location.pathname); +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 } } @@ -249,7 +260,10 @@ async function fetchAndRender(force = false, silent = false) { renderView(); updateTitle(); renderMiniCal(); +<<<<<<< HEAD writeUrlState(); +======= +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 prefetchIfNeeded(start, end); // extend cache in background if approaching an edge return; } diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js index 957b180..250b214 100644 --- a/frontend/js/i18n.js +++ b/frontend/js/i18n.js @@ -154,7 +154,11 @@ const translations = { rec_every: 'Alle', rec_days: 'Tage', rec_weeks: 'Wochen', rec_months: 'Monate', rec_ends: 'Endet', rec_never: 'Nie', rec_after_count: 'Nach Anzahl', rec_on_date: 'Am Datum', rec_occurrences: 'Termine', +<<<<<<< HEAD copy_to_calendar: 'Kopieren nach…', event_copied: 'Termin kopiert', copy: 'Kopieren', +======= + copy_to_calendar: 'Kopieren nach…', event_copied: 'Termin kopiert', +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 edit_before_copy: 'Vor dem Kopieren bearbeiten', event_updated: 'Termin aktualisiert', event_created: 'Termin erstellt', confirm_delete_event: '"{title}" wirklich löschen?', @@ -365,7 +369,11 @@ const translations = { rec_every: 'Every', rec_days: 'days', rec_weeks: 'weeks', rec_months: 'months', rec_ends: 'Ends', rec_never: 'Never', rec_after_count: 'After count', rec_on_date: 'On date', rec_occurrences: 'occurrences', +<<<<<<< HEAD copy_to_calendar: 'Copy to…', event_copied: 'Event copied', copy: 'Copy', +======= + copy_to_calendar: 'Copy to…', event_copied: 'Event copied', +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 edit_before_copy: 'Edit before copying', event_updated: 'Event updated', event_created: 'Event created', confirm_delete_event: 'Really delete "{title}"?', diff --git a/frontend/js/version.js b/frontend/js/version.js index 9b7797e..9d0c41b 100644 --- a/frontend/js/version.js +++ b/frontend/js/version.js @@ -1,2 +1,6 @@ // Increment APP_VERSION with every code change +<<<<<<< HEAD export const APP_VERSION = 'v17'; +======= +export const APP_VERSION = 'v11'; +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 diff --git a/frontend/js/views/week.js b/frontend/js/views/week.js index e84c0cb..4aa14d0 100644 --- a/frontend/js/views/week.js +++ b/frontend/js/views/week.js @@ -63,6 +63,7 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC const color = ev.color || ev.calendarColor || '#4285f4'; const pastCls = isPast(ev) ? 'past' : ''; const multiCls = isMultiTimed ? 'multiday-timed' : ''; +<<<<<<< HEAD // continues-left/right: compute on date-only basis for all-day events let evStart = new Date(ev.start); let evEnd = new Date(ev.end); @@ -76,6 +77,10 @@ export function renderWeek(container, currentDate, events, onSlotClick, onEventC const lastDay = new Date(days[n-1]); lastDay.setHours(0, 0, 0, 0); const cL = evStart < firstDay ? 'continues-left' : ''; const cR = (ev.allDay ? evEnd > lastDay : evEnd > lastDayMidnight) ? 'continues-right' : ''; +======= + const cL = new Date(ev.start) < new Date(days[0]) ? 'continues-left' : ''; + const cR = new Date(ev.end) > (() => { const d = new Date(days[n-1]); d.setHours(24,0,0,0); return d; })() ? 'continues-right' : ''; +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 const label = isMultiTimed && isSameDay(new Date(ev.start), days[colStart]) ? `${fmtTime(new Date(ev.start))} ${ev.title}` : ev.title; @@ -247,6 +252,7 @@ function renderNowLine(container, days, hourH = 60) { function layoutWeekAllDay(evs, days) { const items = []; evs.forEach(ev => { +<<<<<<< HEAD // For all-day events, normalize to date-only with inclusive end-day // (iCal stores exclusive end → subtract 1). For timed events, keep // the original strict-overlap logic so events ending exactly at @@ -269,6 +275,13 @@ function layoutWeekAllDay(evs, days) { matches = new Date(ev.start) < de && new Date(ev.end) > ds; } if (matches) { +======= + let colStart = -1, colEnd = -1; + days.forEach((day, i) => { + const ds = new Date(day); ds.setHours(0, 0, 0, 0); + const de = new Date(day); de.setHours(24, 0, 0, 0); + if (new Date(ev.start) < de && new Date(ev.end) > ds) { +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 if (colStart === -1) colStart = i; colEnd = i; } diff --git a/frontend/sw.js b/frontend/sw.js index 7d26e5f..1d172c7 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,3 +1,4 @@ +<<<<<<< HEAD // Calendarr Service Worker — minimal-cache strategy // // Strategy: network-first for everything. The cache is only used as a @@ -9,13 +10,50 @@ const CACHE_VERSION = 'calendarr-v17'; const OFFLINE_SHELL = ['/', '/index.html']; +======= +// Calendarr Service Worker +// Cache-first for static assets, network-first for /api/* (graceful offline) + +const CACHE_VERSION = 'calendarr-v11'; +const STATIC_ASSETS = [ + '/', + '/index.html', + '/manifest.json', + '/static/css/app.css', + '/static/favicon.svg', + '/static/js/app.js', + '/static/js/api.js', + '/static/js/calendar.js', + '/static/js/color-picker.js', + '/static/js/date-picker.js', + '/static/js/i18n.js', + '/static/js/utils.js', + '/static/js/version.js', + '/static/js/views/agenda.js', + '/static/js/views/month.js', + '/static/js/views/quarter.js', + '/static/js/views/week.js', + '/icons/icon-192.png', + '/icons/icon-512.png', + '/icons/icon.svg', +]; +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_VERSION).then(cache => +<<<<<<< HEAD Promise.all(OFFLINE_SHELL.map(url => cache.add(url).catch(err => console.warn('[SW] skip', url, err)) )) +======= + // Use addAll with a fallback so a single missing file doesn't abort install + Promise.all( + STATIC_ASSETS.map(url => + cache.add(url).catch(err => console.warn('[SW] skip', url, err)) + ) + ) +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 ).then(() => self.skipWaiting()) ); }); @@ -34,8 +72,12 @@ self.addEventListener('fetch', event => { const url = new URL(req.url); +<<<<<<< HEAD // API routes: always go to the network, no offline fallback (we'd just // be returning stale account/event data otherwise). +======= + // Network-first for API routes — fail silently if offline +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(req).catch(() => @@ -48,6 +90,7 @@ self.addEventListener('fetch', event => { return; } +<<<<<<< HEAD // Everything else: network-first. The browser's HTTP cache (driven by // the server's Cache-Control headers) already throttles re-fetches — // the SW just makes sure offline still works for the entry HTML. @@ -71,6 +114,47 @@ self.addEventListener('fetch', event => { return caches.match(req).then(c => c || caches.match('/index.html')); } return new Response('', { status: 503 }); +======= + // Network-first for navigation (HTML) and the version-defining files — + // ensures users always get the freshest entry point so new releases + // take effect on the next reload without a manual SW unregister. + const isHtml = req.mode === 'navigate' + || url.pathname === '/' + || url.pathname === '/index.html'; + const isVersionFile = url.pathname === '/static/js/version.js'; + + if (isHtml || isVersionFile) { + event.respondWith( + fetch(req).then(resp => { + if (resp && resp.status === 200) { + const clone = resp.clone(); + caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {}); + } + return resp; + }).catch(() => + caches.match(req).then(c => c || caches.match('/index.html')) + ) + ); + return; + } + + // Cache-first for everything else (static) + event.respondWith( + caches.match(req).then(cached => { + if (cached) return cached; + return fetch(req).then(resp => { + // Only cache successful, basic-origin responses + if (resp && resp.status === 200 && resp.type === 'basic') { + const clone = resp.clone(); + caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {}); + } + return resp; + }).catch(() => { + // Offline fallback for navigation requests + if (req.mode === 'navigate') return caches.match('/index.html'); + return new Response('', { status: 503 }); + }); +>>>>>>> e744b1829e99db6b80922f75542ced329138e474 }) ); });