feat(web): hide individual member calendars in the group view

In the group overlay a per-member filter bar appears under the banner: tap a
member (or the group calendar) to hide/show their events, Outlook-style.
Filtering is client-side by event owner; members + colours load from the group
detail. Bumped version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-01 18:16:25 +02:00
parent 447c3ddab1
commit 12c14e3c02
4 changed files with 87 additions and 2 deletions

View File

@@ -1840,6 +1840,32 @@ a { color: var(--primary); text-decoration: none; }
font-size: 14px;
color: var(--text-1);
}
/* Per-member filter bar under the group banner (hide individual people). */
.group-members-filter {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 16px;
background: color-mix(in srgb, var(--accent) 7%, var(--bg-app));
border-bottom: 1px solid var(--border);
}
.gmf-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--bg-surface);
color: var(--text-1);
font-size: 12.5px;
cursor: pointer;
transition: opacity var(--transition), background var(--transition);
}
.gmf-chip:hover { background: var(--bg-hover); }
.gmf-chip.gmf-off { opacity: 0.4; text-decoration: line-through; }
.gmf-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.group-item-active {
background: var(--bg-surface);
background: color-mix(in srgb, var(--accent) 18%, transparent);

View File

@@ -50,6 +50,8 @@ let state = {
selectedEventColor: '', // '' = use calendar color
groups: [],
activeGroupId: null, // when set, the calendar shows the combined group view
activeGroupDetail: null, // full detail (members + colours) of the active group
hiddenGroupMembers: new Set(), // member ids / 'gc' hidden in the group overlay
};
// ── URL state ────────────────────────────────────────────
@@ -456,6 +458,16 @@ function renderView() {
}
function filterEvents(events) {
// Group overlay: hide individual members' calendars / the group calendar
// (Outlook-style), filtered client-side by event owner.
const hidden = state.hiddenGroupMembers;
if (state.activeGroupId && hidden && hidden.size) {
return events.filter(ev => {
if (ev.is_group_event) return !hidden.has('gc');
const oid = ev.owner && ev.owner.id;
return oid == null || !hidden.has(oid);
});
}
// If dimPast is enabled, events are still shown but CSS handles opacity via .past class
return events;
}
@@ -2316,23 +2328,68 @@ function openGroupEventModal() {
updatePrivateRow(false);
}
function enterGroupView(groupId) {
async function enterGroupView(groupId) {
state.activeGroupId = groupId;
state.hiddenGroupMembers = new Set();
state.activeGroupDetail = null;
const g = state.groups.find(x => x.id === groupId);
document.getElementById('group-view-banner').classList.remove('hidden');
document.getElementById('group-view-label').textContent =
t('group_view_label', { name: g ? g.name : '' });
renderGroupList();
// Load the member list (with server colours) for the per-member filter.
try { state.activeGroupDetail = await api.get(`/groups/${groupId}`); } catch (e) { /* ignore */ }
renderGroupMemberFilter();
fetchAndRender(true);
}
function exitGroupView() {
state.activeGroupId = null;
state.activeGroupDetail = null;
state.hiddenGroupMembers = new Set();
document.getElementById('group-view-banner').classList.add('hidden');
renderGroupMemberFilter();
renderGroupList();
fetchAndRender(true);
}
// Per-member filter bar shown under the group banner: tap a member (or the
// group calendar) to hide/show their events in the combined overlay.
function renderGroupMemberFilter() {
const banner = document.getElementById('group-view-banner');
if (!banner) return;
let bar = document.getElementById('group-members-filter');
if (!state.activeGroupId || !state.activeGroupDetail) {
if (bar) bar.remove();
return;
}
if (!bar) {
bar = document.createElement('div');
bar.id = 'group-members-filter';
bar.className = 'group-members-filter';
banner.insertAdjacentElement('afterend', bar);
}
const g = state.activeGroupDetail;
const chip = (key, name, color) => {
const off = state.hiddenGroupMembers.has(key);
return `<button class="gmf-chip${off ? ' gmf-off' : ''}" data-gmf="${key}">
<span class="gmf-dot" style="background:${color || '#4285f4'}"></span>${escHtml(name)}</button>`;
};
const parts = (g.members || []).map(m => chip(String(m.id), m.display_name || '—', m.color));
parts.push(chip('gc', t('group_calendar'), g.group_calendar_color));
bar.innerHTML = parts.join('');
bar.querySelectorAll('[data-gmf]').forEach(b => {
b.addEventListener('click', () => {
const raw = b.dataset.gmf;
const key = raw === 'gc' ? 'gc' : parseInt(raw);
if (state.hiddenGroupMembers.has(key)) state.hiddenGroupMembers.delete(key);
else state.hiddenGroupMembers.add(key);
renderGroupMemberFilter();
renderView();
});
});
}
// Open the group modal in create mode (no id) or manage mode (existing group).
async function openGroupModal(groupId) {
const modal = document.getElementById('modal-group');

View File

@@ -124,6 +124,7 @@ const translations = {
group_created: 'Gruppe erstellt',
group_view_label: 'Gruppenansicht: {name}',
group_exit: 'Gruppenansicht verlassen',
group_calendar: 'Gruppenkalender',
group_icon: 'Icon',
group_visible_flag: 'Für deine Gruppen sichtbar',
group_new_event: '+ Gruppentermin',
@@ -396,6 +397,7 @@ const translations = {
group_created: 'Group created',
group_view_label: 'Group view: {name}',
group_exit: 'Exit group view',
group_calendar: 'Group calendar',
group_icon: 'Icon',
group_visible_flag: 'Visible to your groups',
group_new_event: '+ Group event',

View File

@@ -1,2 +1,2 @@
// Increment APP_VERSION with every code change
export const APP_VERSION = 'v41';
export const APP_VERSION = 'v42';