calender stuff
This commit is contained in:
1 parent
8e78468743
commit
e0fca064ae
15 files changed
+1716
-67
No files matched your search
+180
-36
@@ -4,6 +4,7 @@ import { $, esc, api } from './util.js';
|
||||
import { state, sortedCards, parseTags, tagChipHtml, getChildren, getRecent, getFavorites, isFavorite, SORT_MODES, SORT_ICONS, CARD_COLORS } from './state.js';
|
||||
import { stripMd, getTodos, firstImage, leadingImage } from './markdown.js';
|
||||
import { pushReorder, pushCardEdit, pushBatch, snapshotCard } from './undo.js';
|
||||
import { getCalConfig } from './caldav.js';
|
||||
|
||||
const board = $('board');
|
||||
let activeDragEl = null;
|
||||
@@ -90,6 +91,39 @@ export function updateSortUI() {
|
||||
if (icon) icon.className = `ph ${SORT_ICONS[state.sortMode]} text-xs`;
|
||||
}
|
||||
|
||||
$('superview-toggle')?.addEventListener('click', () => {
|
||||
state.superView = !state.superView;
|
||||
localStorage.setItem('hyprcrd-superview', state.superView ? '1' : '0');
|
||||
const btn = $('superview-toggle');
|
||||
const icon = btn?.querySelector('i');
|
||||
if (icon) icon.className = `ph ${state.superView ? 'ph-squares-four' : 'ph-list'} text-xs`;
|
||||
if (btn) btn.classList.toggle('text-t-accent', state.superView);
|
||||
renderBoard();
|
||||
});
|
||||
if (state.superView) {
|
||||
const btn = $('superview-toggle');
|
||||
const icon = btn?.querySelector('i');
|
||||
if (icon) icon.className = 'ph ph-squares-four text-xs';
|
||||
if (btn) btn.classList.add('text-t-accent');
|
||||
}
|
||||
|
||||
$('hide-termine')?.addEventListener('click', () => {
|
||||
state.hideTermine = !state.hideTermine;
|
||||
localStorage.setItem('hyprcrd-hide-termine', state.hideTermine ? '1' : '0');
|
||||
const btn = $('hide-termine');
|
||||
const icon = btn?.querySelector('i');
|
||||
if (icon) icon.className = `ph ${state.hideTermine ? 'ph-calendar-check' : 'ph-calendar-x'} text-xs`;
|
||||
if (btn) btn.classList.toggle('text-t-accent', state.hideTermine);
|
||||
renderBoard();
|
||||
});
|
||||
// Init toggle state
|
||||
if (state.hideTermine) {
|
||||
const btn = $('hide-termine');
|
||||
const icon = btn?.querySelector('i');
|
||||
if (icon) icon.className = 'ph ph-calendar-check text-xs';
|
||||
if (btn) btn.classList.add('text-t-accent');
|
||||
}
|
||||
|
||||
$('sort-toggle')?.addEventListener('click', () => {
|
||||
state.sortMode = SORT_MODES[(SORT_MODES.indexOf(state.sortMode) + 1) % SORT_MODES.length];
|
||||
localStorage.setItem('hyprcrd-sort', state.sortMode);
|
||||
@@ -97,6 +131,49 @@ $('sort-toggle')?.addEventListener('click', () => {
|
||||
renderBoard();
|
||||
});
|
||||
|
||||
function renderCardHtml(c, inGroup = false, recent = []) {
|
||||
const coverImg = leadingImage(c.body);
|
||||
const thumbImg = coverImg ? null : firstImage(c.body);
|
||||
const children = getChildren(c.id);
|
||||
const isGroup = !inGroup && children.length > 0;
|
||||
const todos = getTodos(c.body);
|
||||
const todoPct = todos ? Math.round(todos.done / todos.total * 100) : 0;
|
||||
const hasCover = !!coverImg;
|
||||
const tags = parseTags(c);
|
||||
const isTermin = tags.includes('termin') || tags.includes('geburtstag');
|
||||
let calCfg = null;
|
||||
if (isTermin) { for (const t of tags) { if (t && t !== 'termin' && t !== 'geburtstag') { const cfg = getCalConfig(t); if (cfg) { calCfg = cfg; break; } } } }
|
||||
const cardColor = calCfg?.color || c.color;
|
||||
const calIcon = isTermin ? (calCfg?.icon || 'calendar-dots') : null;
|
||||
|
||||
return `
|
||||
<div class="board-card ${isGroup ? 'card-group ' : ''}card-glow ${hasCover ? 'card-has-cover' : 'bg-t-surface'} border border-t-border cursor-pointer hover:border-t-muted transition-colors relative group"
|
||||
style="--glow-color:${cardColor}22;${hasCover ? `background-image:url('${coverImg}')` : ''};${recent.includes(String(c.id)) ? 'border-bottom:3px solid ' + cardColor : ''}" draggable="true" data-id="${c.id}">
|
||||
${hasCover ? '<div class="card-cover-overlay"></div>' : ''}
|
||||
<div class="card-bar w-full card-layer" style="background:${cardColor}"></div>
|
||||
${isGroup ? `<div class="card-group-badge cursor-pointer hover:scale-110 transition-transform" style="background:${cardColor};color:#000" data-group="${c.id}">${children.length + 1}</div>` : ''}
|
||||
${thumbImg ? `<div class="card-thumb" style="background-image:url('${thumbImg}')"></div>` : ''}
|
||||
<div class="p-4 card-layer flex flex-col h-full ${thumbImg ? 'pr-16' : ''}">
|
||||
<div class="flex items-start gap-2 mb-2">
|
||||
${calIcon ? `<i class="ph ph-${calIcon} text-xs mt-0.5 shrink-0" style="color:${cardColor}"></i>` : ''}
|
||||
${isFavorite(c.id) ? `<i class="ph-fill ph-star text-amber-400 text-xs mt-0.5 shrink-0"></i>` : ''}
|
||||
${todos ? `<i class="ph ${todos.done === todos.total ? 'ph-check-square text-t-accent' : 'ph-squares-four text-t-muted'} text-sm mt-0.5 shrink-0"></i>` : ''}
|
||||
<h3 class="font-pixel text-xs text-t-bright leading-relaxed line-clamp-2">${esc(c.title)}</h3>
|
||||
</div>
|
||||
${todos ? `<div class="mb-2"><div class="flex items-center gap-2 mb-1">
|
||||
<div class="flex-1 h-1.5 bg-t-border rounded-full overflow-hidden"><div class="h-full rounded-full transition-all" style="width:${todoPct}%;background:${cardColor}"></div></div>
|
||||
<span class="font-pixel text-[9px] ${todos.done === todos.total ? 'text-t-accent' : 'text-t-dim'}">${todos.done}/${todos.total}</span>
|
||||
</div></div>` : ''}
|
||||
<p class="text-xs ${hasCover ? 'text-t-text' : 'text-t-muted'} leading-relaxed line-clamp-5 flex-1">${esc(stripMd(c.body))}</p>
|
||||
${tags.length ? `<div class="flex flex-wrap gap-1 mt-1.5 card-layer">${tags.slice(0, 5).map(t => tagChipHtml(t)).join('')}${tags.length > 5 ? `<span class="tag-chip tag-chip-more">+${tags.length - 5}</span>` : ''}</div>` : ''}
|
||||
<div class="mt-auto pt-2 flex items-center gap-2 text-[10px] ${hasCover ? 'text-t-text' : 'text-t-dim'} font-pixel">
|
||||
<span class="flex items-center gap-1"><i class="ph ph-clock text-xs"></i>${(c.updated_at || c.created_at || '').substring(0, 10)}</span>
|
||||
${c.bucket_name ? `<span class="flex items-center gap-1"><i class="ph ph-folder text-xs"></i>${esc(c.bucket_name)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Render ──
|
||||
|
||||
export function renderBoard() {
|
||||
@@ -122,7 +199,8 @@ export function renderBoard() {
|
||||
const favs = getFavorites();
|
||||
cards = cards.filter(c => favs.includes(String(c.id)));
|
||||
} else if (state.activeBucket === 'journal') {
|
||||
cards = cards.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title));
|
||||
const today = new Date().toISOString().substring(0, 10);
|
||||
cards = cards.filter(c => /^\d{4}-\d{2}-\d{2}/.test(c.title) && c.title.substring(0, 10) <= today);
|
||||
cards.sort((a, b) => b.title.localeCompare(a.title));
|
||||
} else if (state.activeBucket === 'none') {
|
||||
cards = cards.filter(c => !c.bucket_id && !/^\d{4}-\d{2}-\d{2}/.test(c.title));
|
||||
@@ -131,6 +209,10 @@ export function renderBoard() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hide appointments
|
||||
if (state.hideTermine) {
|
||||
cards = cards.filter(c => !parseTags(c).includes('termin'));
|
||||
}
|
||||
// Tag filter (intersects with bucket filter) — hierarchical: #nase matches #nase/popel
|
||||
if (state.activeTags.length) {
|
||||
cards = cards.filter(c => {
|
||||
@@ -149,6 +231,7 @@ export function renderBoard() {
|
||||
renderGroupBar(cards);
|
||||
|
||||
if (!cards.length) {
|
||||
board.style.display = '';
|
||||
board.innerHTML = `<div class="col-span-full text-center py-20">
|
||||
<i class="ph ph-cards text-4xl text-t-dim mb-4 block"></i>
|
||||
<p class="font-pixel text-sm text-t-dim cursor-blink">no cards found</p></div>`;
|
||||
@@ -160,42 +243,103 @@ export function renderBoard() {
|
||||
const recent = getRecent();
|
||||
const inGroup = !!state.activeGroup;
|
||||
|
||||
board.innerHTML = cards.map(c => {
|
||||
const coverImg = leadingImage(c.body);
|
||||
const thumbImg = coverImg ? null : firstImage(c.body);
|
||||
const children = getChildren(c.id);
|
||||
const isGroup = !inGroup && children.length > 0;
|
||||
const todos = getTodos(c.body);
|
||||
const todoPct = todos ? Math.round(todos.done / todos.total * 100) : 0;
|
||||
const hasCover = !!coverImg;
|
||||
const tags = parseTags(c);
|
||||
if (state.superView) {
|
||||
// Compact multi-column list view — override board grid
|
||||
board.style.display = 'block';
|
||||
const cols = cards.length <= 20 ? 2 : cards.length <= 50 ? 3 : cards.length <= 100 ? 4 : 5;
|
||||
board.innerHTML = `<div class="superview-grid" style="column-count:${cols};">` + cards.map(c => {
|
||||
const tags = parseTags(c);
|
||||
const isTermin = tags.includes('termin') || tags.includes('geburtstag');
|
||||
let calCfg = null;
|
||||
if (isTermin) { for (const t of tags) { if (t !== 'termin' && t !== 'geburtstag') { const cfg = getCalConfig(t); if (cfg) { calCfg = cfg; break; } } } }
|
||||
const dotColor = calCfg?.color || c.color;
|
||||
const icon = isTermin ? (calCfg?.icon || 'calendar-dots') : (isFavorite(c.id) ? 'star' : null);
|
||||
const iconColor = isTermin ? dotColor : (isFavorite(c.id) ? '#fbbf24' : 'var(--t-dim)');
|
||||
return `<div class="superview-item" data-id="${c.id}">
|
||||
<span class="superview-dot" style="background:${dotColor}"></span>
|
||||
${icon ? `<i class="ph ph-${icon} superview-icon" style="color:${iconColor}"></i>` : ''}
|
||||
<span class="superview-title">${esc(c.title)}</span>
|
||||
</div>`;
|
||||
}).join('') + '</div>';
|
||||
|
||||
return `
|
||||
<div class="board-card ${isGroup ? 'card-group ' : ''}card-glow ${hasCover ? 'card-has-cover' : 'bg-t-surface'} border border-t-border cursor-pointer hover:border-t-muted transition-colors relative group"
|
||||
style="--glow-color:${c.color}22;${hasCover ? `background-image:url('${coverImg}')` : ''};${recent.includes(String(c.id)) ? 'border-bottom:3px solid ' + c.color : ''}" draggable="true" data-id="${c.id}">
|
||||
${hasCover ? '<div class="card-cover-overlay"></div>' : ''}
|
||||
<div class="card-bar w-full card-layer" style="background:${c.color}"></div>
|
||||
${isGroup ? `<div class="card-group-badge cursor-pointer hover:scale-110 transition-transform" style="background:${c.color};color:#000" data-group="${c.id}">${children.length + 1}</div>` : ''}
|
||||
${thumbImg ? `<div class="card-thumb" style="background-image:url('${thumbImg}')"></div>` : ''}
|
||||
<div class="p-4 card-layer flex flex-col h-full ${thumbImg ? 'pr-16' : ''}">
|
||||
<div class="flex items-start gap-2 mb-2">
|
||||
${isFavorite(c.id) ? `<i class="ph-fill ph-star text-amber-400 text-xs mt-0.5 shrink-0"></i>` : ''}
|
||||
${todos ? `<i class="ph ${todos.done === todos.total ? 'ph-check-square text-t-accent' : 'ph-squares-four text-t-muted'} text-sm mt-0.5 shrink-0"></i>` : ''}
|
||||
<h3 class="font-pixel text-xs text-t-bright leading-relaxed line-clamp-2">${esc(c.title)}</h3>
|
||||
</div>
|
||||
${todos ? `<div class="mb-2"><div class="flex items-center gap-2 mb-1">
|
||||
<div class="flex-1 h-1.5 bg-t-border rounded-full overflow-hidden"><div class="h-full rounded-full transition-all" style="width:${todoPct}%;background:${c.color}"></div></div>
|
||||
<span class="font-pixel text-[9px] ${todos.done === todos.total ? 'text-t-accent' : 'text-t-dim'}">${todos.done}/${todos.total}</span>
|
||||
</div></div>` : ''}
|
||||
<p class="text-xs ${hasCover ? 'text-t-text' : 'text-t-muted'} leading-relaxed line-clamp-5 flex-1">${esc(stripMd(c.body))}</p>
|
||||
${tags.length ? `<div class="flex flex-wrap gap-1 mt-1.5 card-layer">${tags.slice(0, 5).map(t => tagChipHtml(t)).join('')}${tags.length > 5 ? `<span class="tag-chip tag-chip-more">+${tags.length - 5}</span>` : ''}</div>` : ''}
|
||||
<div class="mt-auto pt-2 flex items-center gap-2 text-[10px] ${hasCover ? 'text-t-text' : 'text-t-dim'} font-pixel">
|
||||
<span class="flex items-center gap-1"><i class="ph ph-clock text-xs"></i>${(c.updated_at || c.created_at || '').substring(0, 10)}</span>
|
||||
${c.bucket_name ? `<span class="flex items-center gap-1"><i class="ph ph-folder text-xs"></i>${esc(c.bucket_name)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
let hoverPopup = null, hoverTimer = null, hoverCloseTimer = null;
|
||||
function clearHover() {
|
||||
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; }
|
||||
if (hoverCloseTimer) { clearTimeout(hoverCloseTimer); hoverCloseTimer = null; }
|
||||
if (hoverPopup) { hoverPopup.remove(); hoverPopup = null; }
|
||||
}
|
||||
function delayedClose() {
|
||||
hoverCloseTimer = setTimeout(clearHover, 150);
|
||||
}
|
||||
function cancelClose() {
|
||||
if (hoverCloseTimer) { clearTimeout(hoverCloseTimer); hoverCloseTimer = null; }
|
||||
}
|
||||
|
||||
board.querySelectorAll('.superview-item[data-id]').forEach(el => {
|
||||
el.addEventListener('click', () => { clearHover(); window._openDetail(el.dataset.id); });
|
||||
el.addEventListener('dblclick', () => { clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._openModal(card); });
|
||||
el.addEventListener('contextmenu', e => { e.preventDefault(); clearHover(); const card = state.cards.find(c => c.id == el.dataset.id); if (card) window._showContextMenu(e.clientX, e.clientY, card); });
|
||||
|
||||
el.addEventListener('mouseenter', () => {
|
||||
cancelClose();
|
||||
if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; }
|
||||
hoverTimer = setTimeout(() => {
|
||||
if (hoverPopup) { hoverPopup.remove(); hoverPopup = null; }
|
||||
const card = state.cards.find(c => c.id == el.dataset.id);
|
||||
if (!card) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
hoverPopup = document.createElement('div');
|
||||
Object.assign(hoverPopup.style, {
|
||||
position: 'fixed', zIndex: '100',
|
||||
width: '280px',
|
||||
filter: 'drop-shadow(0 8px 24px rgba(0,0,0,0.4))',
|
||||
});
|
||||
const textEnd = el.querySelector('.superview-title');
|
||||
const textRect = textEnd ? textEnd.getBoundingClientRect() : rect;
|
||||
const left = textRect.right + 8;
|
||||
const useLeft = left + 290 < window.innerWidth;
|
||||
hoverPopup.style.left = (useLeft ? left : textRect.left - 288) + 'px';
|
||||
hoverPopup.style.top = Math.min(rect.top - 20, window.innerHeight - 320) + 'px';
|
||||
hoverPopup.innerHTML = renderCardHtml(card);
|
||||
hoverPopup.addEventListener('mouseenter', e => { e.stopPropagation(); cancelClose(); });
|
||||
hoverPopup.addEventListener('mouseleave', delayedClose);
|
||||
hoverPopup.addEventListener('mouseover', e => e.stopPropagation());
|
||||
// Tag clicks inside hover card
|
||||
hoverPopup.querySelectorAll('.tag-seg[data-tag]').forEach(seg => {
|
||||
seg.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const tag = seg.dataset.tag;
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
const idx = state.activeTags.indexOf(tag);
|
||||
if (idx >= 0) state.activeTags.splice(idx, 1);
|
||||
else state.activeTags.push(tag);
|
||||
} else {
|
||||
state.activeTags = state.activeTags.length === 1 && state.activeTags[0] === tag ? [] : [tag];
|
||||
}
|
||||
clearHover();
|
||||
window._renderTagBar?.();
|
||||
renderBoard();
|
||||
});
|
||||
});
|
||||
// Click on hover card opens detail
|
||||
hoverPopup.querySelector('.board-card')?.addEventListener('click', () => {
|
||||
clearHover();
|
||||
window._openDetail(card.id);
|
||||
});
|
||||
document.body.appendChild(hoverPopup);
|
||||
}, 400);
|
||||
});
|
||||
el.addEventListener('mouseleave', delayedClose);
|
||||
});
|
||||
|
||||
const fc = $('footer-count');
|
||||
if (fc) fc.textContent = `${cards.length} cards · ${state.cards.length} total`;
|
||||
return;
|
||||
}
|
||||
|
||||
board.style.display = '';
|
||||
board.innerHTML = cards.map(c => renderCardHtml(c, inGroup, recent)).join('');
|
||||
|
||||
bindCardEvents();
|
||||
|
||||
|
||||
Reference in new issue
Block a user