mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:41:08 +02:00
feat(calendar): add agenda view components
Add AgendaFilters and AgendaItem components for enhanced agenda view. 🤖 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
3fc89f565d
commit
c6b5c2e89a
2 changed files with 368 additions and 0 deletions
|
|
@ -0,0 +1,151 @@
|
|||
<script lang="ts">
|
||||
import { Calendar, CheckSquare, Filter } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
showEvents: boolean;
|
||||
showTodos: boolean;
|
||||
timeRange: '7' | '30' | 'all';
|
||||
onToggleEvents?: () => void;
|
||||
onToggleTodos?: () => void;
|
||||
onRangeChange?: (range: '7' | '30' | 'all') => void;
|
||||
}
|
||||
|
||||
let {
|
||||
showEvents = true,
|
||||
showTodos = true,
|
||||
timeRange = '30',
|
||||
onToggleEvents,
|
||||
onToggleTodos,
|
||||
onRangeChange,
|
||||
}: Props = $props();
|
||||
|
||||
const rangeOptions = [
|
||||
{ value: '7' as const, label: '7 Tage' },
|
||||
{ value: '30' as const, label: '30 Tage' },
|
||||
{ value: 'all' as const, label: 'Alle' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="agenda-filters">
|
||||
<div class="filter-group type-toggles">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-toggle"
|
||||
class:active={showEvents}
|
||||
onclick={onToggleEvents}
|
||||
aria-pressed={showEvents}
|
||||
>
|
||||
<Calendar size={14} />
|
||||
<span>Events</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="filter-toggle"
|
||||
class:active={showTodos}
|
||||
onclick={onToggleTodos}
|
||||
aria-pressed={showTodos}
|
||||
>
|
||||
<CheckSquare size={14} />
|
||||
<span>Aufgaben</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<div class="range-selector">
|
||||
<Filter size={14} />
|
||||
<select
|
||||
value={timeRange}
|
||||
onchange={(e) =>
|
||||
onRangeChange?.((e.target as HTMLSelectElement).value as '7' | '30' | 'all')}
|
||||
>
|
||||
{#each rangeOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.agenda-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: hsl(var(--color-surface));
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.type-toggles {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.filter-toggle:hover {
|
||||
border-color: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.filter-toggle.active {
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
border-color: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.range-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.range-selector select {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-surface));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.range-selector select:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.agenda-filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<script lang="ts">
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
import type { Task } from '$lib/api/todos';
|
||||
import { PRIORITY_COLORS, PRIORITY_LABELS } from '$lib/api/todos';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import TodoCheckbox from '$lib/components/todo/TodoCheckbox.svelte';
|
||||
import PriorityBadge from '$lib/components/todo/PriorityBadge.svelte';
|
||||
import { Calendar, MapPin, Clock } from 'lucide-svelte';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
||||
type ItemType = 'event' | 'todo';
|
||||
|
||||
interface Props {
|
||||
type: ItemType;
|
||||
event?: CalendarEvent;
|
||||
todo?: Task;
|
||||
onclick?: () => void;
|
||||
}
|
||||
|
||||
let { type, event, todo, onclick }: Props = $props();
|
||||
|
||||
let isToggling = $state(false);
|
||||
|
||||
// Event helpers
|
||||
const eventColor = $derived(event ? calendarsStore.getColor(event.calendarId) : undefined);
|
||||
const eventTimeLabel = $derived.by(() => {
|
||||
if (!event) return '';
|
||||
if (event.isAllDay) return 'Ganztägig';
|
||||
|
||||
const start = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
|
||||
const end = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
|
||||
|
||||
return `${format(start, 'HH:mm')} - ${format(end, 'HH:mm')}`;
|
||||
});
|
||||
|
||||
// Todo helpers
|
||||
const todoTimeLabel = $derived.by(() => {
|
||||
if (!todo) return '';
|
||||
if (todo.dueTime) return `Fällig: ${todo.dueTime}`;
|
||||
return 'Heute fällig';
|
||||
});
|
||||
|
||||
async function handleToggleTodo() {
|
||||
if (!todo) return;
|
||||
isToggling = true;
|
||||
await todosStore.toggleComplete(todo.id);
|
||||
isToggling = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if type === 'event' && event}
|
||||
<button type="button" class="agenda-item event" style="--item-color: {eventColor};" {onclick}>
|
||||
<div class="item-indicator">
|
||||
<Calendar size={14} />
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<div class="item-header">
|
||||
<span class="item-time">{eventTimeLabel}</span>
|
||||
</div>
|
||||
<span class="item-title">{event.title}</span>
|
||||
{#if event.location}
|
||||
<div class="item-meta">
|
||||
<MapPin size={12} />
|
||||
<span>{event.location}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{:else if type === 'todo' && todo}
|
||||
<div
|
||||
class="agenda-item todo"
|
||||
class:completed={todo.isCompleted}
|
||||
style="--item-color: {PRIORITY_COLORS[todo.priority]};"
|
||||
>
|
||||
<div class="item-checkbox">
|
||||
<TodoCheckbox
|
||||
checked={todo.isCompleted}
|
||||
loading={isToggling}
|
||||
size="md"
|
||||
onchange={handleToggleTodo}
|
||||
/>
|
||||
</div>
|
||||
<button type="button" class="item-content" {onclick}>
|
||||
<div class="item-header">
|
||||
<PriorityBadge priority={todo.priority} variant="dot" size="sm" />
|
||||
<span class="item-time">{todoTimeLabel}</span>
|
||||
</div>
|
||||
<span class="item-title">{todo.title}</span>
|
||||
{#if todo.project}
|
||||
<div class="item-meta">
|
||||
<span class="project-tag" style="color: {todo.project.color};">
|
||||
{todo.project.name}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.agenda-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: var(--radius-md);
|
||||
background: hsl(var(--color-surface));
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.agenda-item.event {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
border-left: 4px solid var(--item-color);
|
||||
}
|
||||
|
||||
.agenda-item.event:hover {
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.agenda-item.todo {
|
||||
border-left: 3px solid var(--item-color);
|
||||
}
|
||||
|
||||
.agenda-item.todo.completed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.agenda-item.todo.completed .item-title {
|
||||
text-decoration: line-through;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.item-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--item-color);
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-checkbox {
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.todo .item-content {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.todo .item-content:hover .item-title {
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.item-meta :global(svg) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-tag {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
background: color-mix(in srgb, currentColor 15%, transparent);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue