mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 01:01:09 +02:00
feat(calendar): add tag filtering to calendar views
- Add selectedTagIds to settings store with toggle/clear methods - Update TagStrip to select tags for filtering instead of navigation - Add filterByTags function to eventFiltering utils - Apply tag filtering across all calendar views: - MultiDayView (timed & all-day events) - AgendaView (with empty group removal) - MonthView - YearView (event counts) - Add "Filter löschen" button (hidden when no tags selected) - Rename buttons: "Mehr" → "Alle Tags", "Neu" → "Neuer Tag" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
781bc529ba
commit
c7c451e439
7 changed files with 200 additions and 28 deletions
|
|
@ -2,7 +2,9 @@
|
|||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
|
||||
import { filterByTags } from '$lib/utils/eventFiltering';
|
||||
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
|
||||
import { format, parseISO, isToday, isTomorrow, startOfDay } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
|
@ -33,6 +35,9 @@
|
|||
|
||||
const groups: Map<string, CalendarEvent[]> = new Map();
|
||||
|
||||
// Get selected tag IDs for filtering
|
||||
const selectedTagIds = settingsStore.selectedTagIds;
|
||||
|
||||
for (const event of currentEvents) {
|
||||
// Skip events from hidden calendars
|
||||
if (!visibleCalendarIds.has(event.calendarId)) continue;
|
||||
|
|
@ -50,17 +55,21 @@
|
|||
groups.get(dateKey)!.push(event);
|
||||
}
|
||||
|
||||
// Sort groups by date
|
||||
// Sort groups by date and apply tag filtering
|
||||
return Array.from(groups.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([dateKey, events]) => ({
|
||||
date: parseISO(dateKey),
|
||||
events: events.sort((a, b) => {
|
||||
const aStart = toDate(a.startTime);
|
||||
const bStart = toDate(b.startTime);
|
||||
return aStart.getTime() - bStart.getTime();
|
||||
}),
|
||||
}));
|
||||
events: filterByTags(
|
||||
events.sort((a, b) => {
|
||||
const aStart = toDate(a.startTime);
|
||||
const bStart = toDate(b.startTime);
|
||||
return aStart.getTime() - bStart.getTime();
|
||||
}),
|
||||
selectedTagIds
|
||||
),
|
||||
}))
|
||||
.filter((group) => group.events.length > 0); // Remove empty groups after tag filtering
|
||||
});
|
||||
|
||||
function formatDateHeader(date: Date) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
} from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { filterByVisibleCalendars } from '$lib/utils/eventFiltering';
|
||||
import { filterByVisibleCalendars, filterByTags } from '$lib/utils/eventFiltering';
|
||||
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
|
||||
|
|
@ -201,17 +201,20 @@
|
|||
// Event Handlers
|
||||
// ============================================================================
|
||||
function getEventsForDay(day: Date): CalendarEvent[] {
|
||||
return filterByVisibleCalendars(
|
||||
eventsStore.getEventsForDay(day),
|
||||
calendarsStore.visibleCalendars
|
||||
).slice(0, 3); // Max 3 events shown
|
||||
}
|
||||
|
||||
function getAllEventsForDay(day: Date): CalendarEvent[] {
|
||||
return filterByVisibleCalendars(
|
||||
let events = filterByVisibleCalendars(
|
||||
eventsStore.getEventsForDay(day),
|
||||
calendarsStore.visibleCalendars
|
||||
);
|
||||
events = filterByTags(events, settingsStore.selectedTagIds);
|
||||
return events.slice(0, 3); // Max 3 events shown
|
||||
}
|
||||
|
||||
function getAllEventsForDay(day: Date): CalendarEvent[] {
|
||||
let events = filterByVisibleCalendars(
|
||||
eventsStore.getEventsForDay(day),
|
||||
calendarsStore.visibleCalendars
|
||||
);
|
||||
return filterByTags(events, settingsStore.selectedTagIds);
|
||||
}
|
||||
|
||||
function handleDayClick(day: Date, e: MouseEvent) {
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
filterHoursEnabled: settingsStore.filterHoursEnabled,
|
||||
dayStartHour: settingsStore.dayStartHour,
|
||||
dayEndHour: settingsStore.dayEndHour,
|
||||
selectedTagIds: settingsStore.selectedTagIds,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -174,7 +175,10 @@
|
|||
function getAllDayEventsForDay(day: Date): CalendarEvent[] {
|
||||
return getVisibleAllDayEvents(
|
||||
eventsStore.getEventsForDay(day),
|
||||
calendarsStore.visibleCalendars
|
||||
calendarsStore.visibleCalendars,
|
||||
{
|
||||
selectedTagIds: settingsStore.selectedTagIds,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { eventTagGroupsStore } from '$lib/stores/event-tag-groups.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { DotsThree, Plus } from '@manacore/shared-icons';
|
||||
import { DotsThree, Plus, X } from '@manacore/shared-icons';
|
||||
import TagStripModal from './TagStripModal.svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -16,10 +16,16 @@
|
|||
let showModal = $state(false);
|
||||
|
||||
function handleTagClick(tagId: string) {
|
||||
// Navigate to tags page with the tag selected for editing
|
||||
goto(`/tags?edit=${tagId}`);
|
||||
// Toggle tag selection for filtering calendar view
|
||||
settingsStore.toggleTagSelection(tagId);
|
||||
}
|
||||
|
||||
function isTagSelected(tagId: string): boolean {
|
||||
return settingsStore.isTagSelected(tagId);
|
||||
}
|
||||
|
||||
const hasSelectedTags = $derived(settingsStore.hasSelectedTags);
|
||||
|
||||
function handleOpenModal() {
|
||||
showModal = true;
|
||||
}
|
||||
|
|
@ -58,10 +64,22 @@
|
|||
|
||||
<div class="tag-strip-wrapper" class:sidebar-mode={isSidebarMode}>
|
||||
<div class="tag-strip-container">
|
||||
<!-- Clear Filter Button (always rendered to prevent layout shift) -->
|
||||
<button
|
||||
class="clear-filter-pill glass-tag"
|
||||
class:hidden={!hasSelectedTags}
|
||||
onclick={() => settingsStore.clearTagSelection()}
|
||||
title="Filter löschen"
|
||||
disabled={!hasSelectedTags}
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
<span class="tag-name">Filter</span>
|
||||
</button>
|
||||
|
||||
<!-- More Pill (opens modal) -->
|
||||
<button class="more-pill glass-tag" onclick={handleOpenModal} title="Alle Tags anzeigen">
|
||||
<DotsThree size={18} weight="bold" />
|
||||
<span class="tag-name">Mehr</span>
|
||||
<span class="tag-name">Alle Tags</span>
|
||||
</button>
|
||||
|
||||
{#if eventTagsStore.loading}
|
||||
|
|
@ -75,6 +93,7 @@
|
|||
{#each sortedTags as tag (tag.id)}
|
||||
<button
|
||||
class="tag-pill glass-tag"
|
||||
class:selected={isTagSelected(tag.id)}
|
||||
onclick={() => handleTagClick(tag.id)}
|
||||
title={tag.name}
|
||||
style="--tag-color: {tag.color || '#3b82f6'}"
|
||||
|
|
@ -91,6 +110,7 @@
|
|||
title="Neuer Tag"
|
||||
>
|
||||
<Plus size={16} weight="bold" />
|
||||
<span class="tag-name">Neuer Tag</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -141,7 +161,8 @@
|
|||
|
||||
.tag-pill,
|
||||
.more-pill,
|
||||
.create-pill {
|
||||
.create-pill,
|
||||
.clear-filter-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
|
@ -150,6 +171,53 @@
|
|||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
/* Selected tag state */
|
||||
.tag-pill.selected {
|
||||
background: var(--tag-color) !important;
|
||||
border-color: var(--tag-color) !important;
|
||||
}
|
||||
|
||||
.tag-pill.selected .tag-dot {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.tag-pill.selected .tag-name {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Clear filter pill */
|
||||
.clear-filter-pill {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1) !important;
|
||||
border-color: rgba(239, 68, 68, 0.3) !important;
|
||||
}
|
||||
|
||||
.clear-filter-pill .tag-name {
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.dark) .clear-filter-pill {
|
||||
color: #f87171;
|
||||
background: rgba(239, 68, 68, 0.15) !important;
|
||||
border-color: rgba(239, 68, 68, 0.3) !important;
|
||||
}
|
||||
|
||||
:global(.dark) .clear-filter-pill .tag-name {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.clear-filter-pill:hover:not(.hidden) {
|
||||
background: rgba(239, 68, 68, 0.2) !important;
|
||||
border-color: rgba(239, 68, 68, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Hidden state for clear filter pill (prevents layout shift) */
|
||||
.clear-filter-pill.hidden {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* More pill with muted style */
|
||||
.more-pill {
|
||||
color: #6b7280;
|
||||
|
|
@ -171,13 +239,21 @@
|
|||
/* Create pill with primary accent */
|
||||
.create-pill {
|
||||
color: #3b82f6;
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
|
||||
.create-pill .tag-name {
|
||||
color: #3b82f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.dark) .create-pill {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
:global(.dark) .create-pill .tag-name {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
/* Glass tag styling - same as PillNavigation pills */
|
||||
.glass-tag {
|
||||
padding: 0.5rem 1rem;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
} from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { toDate } from '$lib/utils/eventDateHelpers';
|
||||
import { filterByTags } from '$lib/utils/eventFiltering';
|
||||
import type { CalendarViewType, CalendarEvent } from '@calendar/shared';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -60,7 +61,10 @@
|
|||
// Precompute event counts for performance
|
||||
let eventCountsByDay = $derived.by(() => {
|
||||
const counts = new Map<string, number>();
|
||||
const events = eventsStore.events ?? [];
|
||||
let events = eventsStore.events ?? [];
|
||||
|
||||
// Apply tag filter if tags are selected
|
||||
events = filterByTags(events, settingsStore.selectedTagIds);
|
||||
|
||||
for (const event of events) {
|
||||
const start = toDate(event.startTime);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export interface CalendarAppSettings {
|
|||
|
||||
// TagStrip settings
|
||||
tagStripCollapsed: boolean; // Whether TagStrip is hidden
|
||||
selectedTagIds: string[]; // Tags selected for filtering calendar view
|
||||
|
||||
// Immersive Mode settings
|
||||
immersiveModeEnabled: boolean; // Fullscreen mode - hides all UI elements
|
||||
|
|
@ -91,6 +92,7 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
|
|||
dateStripCollapsed: false,
|
||||
// TagStrip defaults
|
||||
tagStripCollapsed: true, // Hidden by default
|
||||
selectedTagIds: [], // No tags selected by default
|
||||
// Immersive Mode defaults
|
||||
immersiveModeEnabled: false,
|
||||
// Birthday defaults
|
||||
|
|
@ -241,6 +243,12 @@ export const settingsStore = {
|
|||
get tagStripCollapsed() {
|
||||
return settings.tagStripCollapsed;
|
||||
},
|
||||
get selectedTagIds() {
|
||||
return settings.selectedTagIds;
|
||||
},
|
||||
get hasSelectedTags() {
|
||||
return settings.selectedTagIds.length > 0;
|
||||
},
|
||||
// Immersive Mode settings
|
||||
get immersiveModeEnabled() {
|
||||
return settings.immersiveModeEnabled;
|
||||
|
|
@ -316,6 +324,34 @@ export const settingsStore = {
|
|||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle a tag selection for filtering
|
||||
*/
|
||||
toggleTagSelection(tagId: string) {
|
||||
const currentIds = settings.selectedTagIds;
|
||||
const isSelected = currentIds.includes(tagId);
|
||||
const newIds = isSelected ? currentIds.filter((id) => id !== tagId) : [...currentIds, tagId];
|
||||
settings = { ...settings, selectedTagIds: newIds };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a tag is selected
|
||||
*/
|
||||
isTagSelected(tagId: string): boolean {
|
||||
return settings.selectedTagIds.includes(tagId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all tag selections
|
||||
*/
|
||||
clearTagSelection() {
|
||||
settings = { ...settings, selectedTagIds: [] };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle Immersive Mode (fullscreen, hide all UI)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export function getOverflowEvents(
|
|||
}
|
||||
|
||||
/**
|
||||
* Combined filter: Get visible timed events for a day with optional hour filtering
|
||||
* Combined filter: Get visible timed events for a day with optional hour and tag filtering
|
||||
*/
|
||||
export function getVisibleTimedEvents(
|
||||
events: CalendarEvent[],
|
||||
|
|
@ -123,6 +123,7 @@ export function getVisibleTimedEvents(
|
|||
filterHoursEnabled?: boolean;
|
||||
dayStartHour?: number;
|
||||
dayEndHour?: number;
|
||||
selectedTagIds?: string[];
|
||||
}
|
||||
): CalendarEvent[] {
|
||||
let filtered = filterByVisibleCalendars(events, visibleCalendars);
|
||||
|
|
@ -136,18 +137,57 @@ export function getVisibleTimedEvents(
|
|||
filtered = filterByHourRange(filtered, options.dayStartHour, options.dayEndHour);
|
||||
}
|
||||
|
||||
// Apply tag filter if tags are selected
|
||||
if (options?.selectedTagIds) {
|
||||
filtered = filterByTags(filtered, options.selectedTagIds);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined filter: Get visible all-day events for a day
|
||||
* Combined filter: Get visible all-day events for a day with optional tag filtering
|
||||
*/
|
||||
export function getVisibleAllDayEvents(
|
||||
events: CalendarEvent[],
|
||||
visibleCalendars: Calendar[]
|
||||
visibleCalendars: Calendar[],
|
||||
options?: {
|
||||
selectedTagIds?: string[];
|
||||
}
|
||||
): CalendarEvent[] {
|
||||
let filtered = filterByVisibleCalendars(events, visibleCalendars);
|
||||
return filterAllDayEvents(filtered);
|
||||
filtered = filterAllDayEvents(filtered);
|
||||
|
||||
// Apply tag filter if tags are selected
|
||||
if (options?.selectedTagIds) {
|
||||
filtered = filterByTags(filtered, options.selectedTagIds);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter events by selected tag IDs
|
||||
* If no tags are selected (empty array), returns all events
|
||||
* If tags are selected, returns only events that have at least one of the selected tags
|
||||
*/
|
||||
export function filterByTags(events: CalendarEvent[], selectedTagIds: string[]): CalendarEvent[] {
|
||||
// If no tags are selected, show all events
|
||||
if (selectedTagIds.length === 0) {
|
||||
return events;
|
||||
}
|
||||
|
||||
const selectedTagSet = new Set(selectedTagIds);
|
||||
|
||||
return events.filter((event) => {
|
||||
// If event has no tags, don't show it when filtering by tags
|
||||
if (!event.tags || event.tags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if event has at least one of the selected tags
|
||||
return event.tags.some((tag) => selectedTagSet.has(tag.id));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue