mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:01:09 +02:00
feat(calendar): replace calendar dropdown with horizontal pills
- Replace select dropdown with horizontal scrolling pill buttons in QuickEventOverlay - Each pill shows colored dot + calendar name for quick visual selection - Add setAsDefault method to calendars store - Improve settings page calendar editing with SettingsSection/SettingsCard components 🤖 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
5190b1449a
commit
b720f64d2f
3 changed files with 683 additions and 347 deletions
|
|
@ -361,15 +361,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Update draft when calendar changes
|
||||
function handleCalendarChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
calendarId = target.value;
|
||||
if (!isEditMode) {
|
||||
eventsStore.updateDraftEvent({ calendarId: target.value });
|
||||
}
|
||||
}
|
||||
|
||||
// Update draft when all-day changes
|
||||
function handleAllDayToggle() {
|
||||
isAllDay = !isAllDay;
|
||||
|
|
@ -711,26 +702,31 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Calendar select -->
|
||||
<div class="form-row">
|
||||
<div class="row-icon">
|
||||
<div
|
||||
class="calendar-dot"
|
||||
style="background-color: {calendarsStore.getColor(calendarId)}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="row-content">
|
||||
<span class="field-label">Kalender</span>
|
||||
{#if calendarsStore.calendars.length > 0}
|
||||
<select class="field-select" value={calendarId} onchange={handleCalendarChange}>
|
||||
{#each calendarsStore.calendars as cal}
|
||||
<option value={cal.id}>{cal.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<span class="field-placeholder">Standardkalender wird erstellt</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Calendar pills -->
|
||||
<div class="calendar-pills-container">
|
||||
{#if calendarsStore.calendars.length > 0}
|
||||
<div class="calendar-pills-scroll">
|
||||
{#each calendarsStore.calendars as cal}
|
||||
<button
|
||||
type="button"
|
||||
class="calendar-pill"
|
||||
class:active={calendarId === cal.id}
|
||||
onclick={() => {
|
||||
calendarId = cal.id;
|
||||
if (!isEditMode) {
|
||||
eventsStore.updateDraftEvent({ calendarId: cal.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span class="calendar-pill-dot" style="background-color: {cal.color || '#3b82f6'}"
|
||||
></span>
|
||||
<span class="calendar-pill-name">{cal.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="field-placeholder">Standardkalender wird erstellt</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- People (compact) -->
|
||||
|
|
@ -1225,6 +1221,63 @@
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Calendar pills */
|
||||
.calendar-pills-container {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
|
||||
.calendar-pills-scroll {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
padding: 0 1.25rem 2px;
|
||||
}
|
||||
|
||||
.calendar-pills-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.calendar-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 9999px;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: all 150ms;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.calendar-pill:hover {
|
||||
background: hsl(var(--color-muted) / 0.3);
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.calendar-pill.active {
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
border-color: hsl(var(--color-primary) / 0.3);
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.calendar-pill-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.calendar-pill-name {
|
||||
}
|
||||
|
||||
.row-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
|
|||
|
|
@ -115,6 +115,23 @@ export const calendarsStore = {
|
|||
return this.updateCalendar(id, { isVisible: !calendar.isVisible });
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a calendar as the default
|
||||
*/
|
||||
async setAsDefault(id: string) {
|
||||
const result = await api.updateCalendar(id, { isDefault: true });
|
||||
|
||||
if (result.data) {
|
||||
// Update local state: set this one as default, remove default from others
|
||||
calendars = getCalendarsArray().map((c) => ({
|
||||
...c,
|
||||
isDefault: c.id === id,
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get calendar by ID
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,15 +7,32 @@
|
|||
import type { TimeFormat, AllDayDisplayMode } from '$lib/stores/settings.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
import { GlobalSettingsSection } from '@manacore/shared-ui';
|
||||
import { GlobalSettingsSection, SettingsSection, SettingsCard } from '@manacore/shared-ui';
|
||||
import type { CalendarViewType, Calendar } from '@calendar/shared';
|
||||
|
||||
// Calendar management state
|
||||
let editingCalendar = $state<Calendar | null>(null);
|
||||
let editName = $state('');
|
||||
let editColor = $state('');
|
||||
let editIsDefault = $state(false);
|
||||
let showNewCalendarForm = $state(false);
|
||||
let newCalendarName = $state('');
|
||||
let newCalendarColor = $state('#3b82f6');
|
||||
|
||||
function startEditing(calendar: Calendar) {
|
||||
editingCalendar = calendar;
|
||||
editName = calendar.name;
|
||||
editColor = calendar.color || '#3b82f6';
|
||||
editIsDefault = calendar.isDefault || false;
|
||||
}
|
||||
|
||||
function cancelEditing() {
|
||||
editingCalendar = null;
|
||||
editName = '';
|
||||
editColor = '';
|
||||
editIsDefault = false;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!authStore.isAuthenticated) {
|
||||
goto('/login');
|
||||
|
|
@ -59,8 +76,23 @@
|
|||
toast.success('Kalender gelöscht');
|
||||
}
|
||||
|
||||
async function handleUpdateCalendar(calendar: Calendar, name: string, color: string) {
|
||||
const result = await calendarsStore.updateCalendar(calendar.id, { name, color });
|
||||
async function handleUpdateCalendar() {
|
||||
if (!editingCalendar || !editName.trim()) return;
|
||||
|
||||
// If setting as default and it wasn't before, use setAsDefault
|
||||
if (editIsDefault && !editingCalendar.isDefault) {
|
||||
const defaultResult = await calendarsStore.setAsDefault(editingCalendar.id);
|
||||
if (defaultResult?.error) {
|
||||
toast.error(`Fehler: ${defaultResult.error.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update name and color
|
||||
const result = await calendarsStore.updateCalendar(editingCalendar.id, {
|
||||
name: editName.trim(),
|
||||
color: editColor,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
toast.error(`Fehler: ${result.error.message}`);
|
||||
|
|
@ -68,7 +100,7 @@
|
|||
}
|
||||
|
||||
toast.success('Kalender aktualisiert');
|
||||
editingCalendar = null;
|
||||
cancelEditing();
|
||||
}
|
||||
|
||||
function handleViewChange(view: CalendarViewType) {
|
||||
|
|
@ -124,335 +156,422 @@
|
|||
</header>
|
||||
|
||||
<!-- Meine Kalender -->
|
||||
<section class="settings-section card">
|
||||
<div class="calendars-header">
|
||||
<h2>Meine Kalender</h2>
|
||||
<button class="btn btn-primary btn-sm" onclick={() => (showNewCalendarForm = true)}>
|
||||
Neuer Kalender
|
||||
</button>
|
||||
</div>
|
||||
<SettingsSection title="Meine Kalender">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<SettingsCard>
|
||||
<div class="p-5">
|
||||
<div class="calendars-toolbar">
|
||||
<button class="btn btn-primary btn-sm" onclick={() => (showNewCalendarForm = true)}>
|
||||
Neuer Kalender
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showNewCalendarForm}
|
||||
<div class="new-calendar-form">
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleCreateCalendar();
|
||||
}}
|
||||
>
|
||||
<div class="calendar-form-row">
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="Kalender Name"
|
||||
bind:value={newCalendarName}
|
||||
/>
|
||||
<input type="color" class="color-input" bind:value={newCalendarColor} />
|
||||
</div>
|
||||
<div class="calendar-form-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
onclick={() => (showNewCalendarForm = false)}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" disabled={!newCalendarName.trim()}>
|
||||
Erstellen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="calendar-list">
|
||||
{#each calendarsStore.calendars as calendar}
|
||||
<div class="calendar-card">
|
||||
{#if editingCalendar?.id === calendar.id}
|
||||
{#if showNewCalendarForm}
|
||||
<div class="new-calendar-form">
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const name = (form.elements.namedItem('name') as HTMLInputElement).value;
|
||||
const color = (form.elements.namedItem('color') as HTMLInputElement).value;
|
||||
handleUpdateCalendar(calendar, name, color);
|
||||
handleCreateCalendar();
|
||||
}}
|
||||
>
|
||||
<div class="calendar-form-row">
|
||||
<input type="text" name="name" class="input" value={calendar.name} />
|
||||
<input type="color" name="color" class="color-input" value={calendar.color} />
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="Kalender Name"
|
||||
bind:value={newCalendarName}
|
||||
/>
|
||||
<input type="color" class="color-input" bind:value={newCalendarColor} />
|
||||
</div>
|
||||
<div class="calendar-form-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
onclick={() => (editingCalendar = null)}
|
||||
onclick={() => (showNewCalendarForm = false)}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary"> Speichern </button>
|
||||
<button type="submit" class="btn btn-primary" disabled={!newCalendarName.trim()}>
|
||||
Erstellen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="calendar-info">
|
||||
<span class="color-dot" style="background-color: {calendar.color}"></span>
|
||||
<span class="calendar-name">{calendar.name}</span>
|
||||
{#if calendar.isDefault}
|
||||
<span class="badge">Standard</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="calendar-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick={() => (editingCalendar = calendar)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
{#if !calendar.isDefault}
|
||||
<button
|
||||
class="btn btn-ghost btn-sm text-destructive"
|
||||
onclick={() => handleDeleteCalendar(calendar)}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="calendar-list">
|
||||
{#each calendarsStore.calendars as calendar}
|
||||
{#if editingCalendar?.id === calendar.id}
|
||||
<div class="calendar-edit-form">
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleUpdateCalendar();
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
{/if}
|
||||
<div class="edit-form-row">
|
||||
<div class="edit-form-group edit-form-group--name">
|
||||
<label for="edit-name" class="edit-label">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-name"
|
||||
class="edit-input"
|
||||
placeholder="Kalender Name"
|
||||
bind:value={editName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="edit-form-group edit-form-group--color">
|
||||
<label for="edit-color" class="edit-label">Farbe</label>
|
||||
<div class="edit-color-wrapper">
|
||||
<input
|
||||
type="color"
|
||||
id="edit-color"
|
||||
class="edit-color-input"
|
||||
bind:value={editColor}
|
||||
/>
|
||||
<span class="edit-color-value">{editColor}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="edit-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={editIsDefault}
|
||||
disabled={editingCalendar.isDefault}
|
||||
/>
|
||||
<span class="edit-checkbox-text">
|
||||
Als Standardkalender verwenden
|
||||
{#if editingCalendar.isDefault}
|
||||
<span class="edit-checkbox-hint">(aktueller Standard)</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="edit-form-actions">
|
||||
<button type="button" class="btn btn-ghost" onclick={cancelEditing}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" disabled={!editName.trim()}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="calendar-card">
|
||||
<div class="calendar-info">
|
||||
<span class="color-dot" style="background-color: {calendar.color}"></span>
|
||||
<span class="calendar-name">{calendar.name}</span>
|
||||
{#if calendar.isDefault}
|
||||
<span class="badge badge-primary">Standard</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="calendar-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick={() => startEditing(calendar)}>
|
||||
Bearbeiten
|
||||
</button>
|
||||
{#if !calendar.isDefault}
|
||||
<button
|
||||
class="btn btn-ghost btn-sm text-destructive"
|
||||
onclick={() => handleDeleteCalendar(calendar)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if calendarsStore.calendars.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>Keine Kalender vorhanden</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if calendarsStore.calendars.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>Keine Kalender vorhanden</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<!-- Global App Settings (synced across all apps) -->
|
||||
<section class="settings-section">
|
||||
<GlobalSettingsSection
|
||||
{userSettings}
|
||||
appId="calendar"
|
||||
title="App-Einstellungen"
|
||||
description="Diese Einstellungen werden mit allen Mana Apps synchronisiert"
|
||||
/>
|
||||
</section>
|
||||
<GlobalSettingsSection
|
||||
{userSettings}
|
||||
appId="calendar"
|
||||
title="App-Einstellungen"
|
||||
description="Diese Einstellungen werden mit allen Mana Apps synchronisiert"
|
||||
/>
|
||||
|
||||
<!-- Kalender-Ansicht -->
|
||||
<section class="settings-section card">
|
||||
<h2>Kalender-Ansicht</h2>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Standard-Ansicht</span>
|
||||
<span class="setting-description">Ansicht beim Öffnen des Kalenders</span>
|
||||
</div>
|
||||
<select
|
||||
class="select-input"
|
||||
value={settingsStore.defaultView}
|
||||
onchange={(e) => handleViewChange(e.currentTarget.value as CalendarViewType)}
|
||||
>
|
||||
{#each Object.entries(viewLabels) as [value, label]}
|
||||
<option {value}>{label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Zeitformat</span>
|
||||
<span class="setting-description">Anzeige der Uhrzeiten</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="group-button"
|
||||
class:active={settingsStore.timeFormat === '24h'}
|
||||
onclick={() => handleTimeFormatChange('24h')}
|
||||
>
|
||||
24h (14:00)
|
||||
</button>
|
||||
<button
|
||||
class="group-button"
|
||||
class:active={settingsStore.timeFormat === '12h'}
|
||||
onclick={() => handleTimeFormatChange('12h')}
|
||||
>
|
||||
12h (2:00 PM)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="toggle-setting">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingsStore.showOnlyWeekdays}
|
||||
onchange={() => settingsStore.set('showOnlyWeekdays', !settingsStore.showOnlyWeekdays)}
|
||||
<SettingsSection title="Kalender-Ansicht">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<div class="toggle-info">
|
||||
<span class="setting-label">Nur Werktage anzeigen</span>
|
||||
<span class="setting-description">Wochenenden in der Kalenderansicht ausblenden</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="toggle-setting">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingsStore.showWeekNumbers}
|
||||
onchange={() => settingsStore.set('showWeekNumbers', !settingsStore.showWeekNumbers)}
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
<div class="toggle-info">
|
||||
<span class="setting-label">Wochennummern anzeigen</span>
|
||||
<span class="setting-description">Kalenderwoche (KW) in der Ansicht anzeigen</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="toggle-setting">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingsStore.filterHoursEnabled}
|
||||
onchange={() =>
|
||||
settingsStore.set('filterHoursEnabled', !settingsStore.filterHoursEnabled)}
|
||||
/>
|
||||
<div class="toggle-info">
|
||||
<span class="setting-label">Stunden filtern</span>
|
||||
<span class="setting-description"
|
||||
>Nur bestimmte Stunden in der Tages-/Wochenansicht anzeigen</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if settingsStore.filterHoursEnabled}
|
||||
<div class="setting-item hour-range-setting">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Sichtbare Stunden</span>
|
||||
<span class="setting-description"
|
||||
>Zeitbereich der in der Kalenderansicht angezeigt wird</span
|
||||
>
|
||||
</div>
|
||||
<div class="hour-range-inputs">
|
||||
<div class="hour-input-group">
|
||||
<label for="start-hour">Von</label>
|
||||
<select
|
||||
id="start-hour"
|
||||
class="select-input hour-select"
|
||||
value={settingsStore.dayStartHour}
|
||||
onchange={(e) => settingsStore.set('dayStartHour', Number(e.currentTarget.value))}
|
||||
>
|
||||
{#each Array.from({ length: 24 }, (_, i) => i) as hour}
|
||||
{#if hour < settingsStore.dayEndHour}
|
||||
<option value={hour}>{hour.toString().padStart(2, '0')}:00</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<SettingsCard>
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Standard-Ansicht</span>
|
||||
<span class="setting-description">Ansicht beim Öffnen des Kalenders</span>
|
||||
</div>
|
||||
<span class="hour-separator">–</span>
|
||||
<div class="hour-input-group">
|
||||
<label for="end-hour">Bis</label>
|
||||
<select
|
||||
id="end-hour"
|
||||
class="select-input hour-select"
|
||||
value={settingsStore.dayEndHour}
|
||||
onchange={(e) => settingsStore.set('dayEndHour', Number(e.currentTarget.value))}
|
||||
<select
|
||||
class="select-input"
|
||||
value={settingsStore.defaultView}
|
||||
onchange={(e) => handleViewChange(e.currentTarget.value as CalendarViewType)}
|
||||
>
|
||||
{#each Object.entries(viewLabels) as [value, label]}
|
||||
<option {value}>{label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Zeitformat</span>
|
||||
<span class="setting-description">Anzeige der Uhrzeiten</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="group-button"
|
||||
class:active={settingsStore.timeFormat === '24h'}
|
||||
onclick={() => handleTimeFormatChange('24h')}
|
||||
>
|
||||
{#each Array.from({ length: 24 }, (_, i) => i + 1) as hour}
|
||||
{#if hour > settingsStore.dayStartHour}
|
||||
<option value={hour}>{hour.toString().padStart(2, '0')}:00</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
24h (14:00)
|
||||
</button>
|
||||
<button
|
||||
class="group-button"
|
||||
class:active={settingsStore.timeFormat === '12h'}
|
||||
onclick={() => handleTimeFormatChange('12h')}
|
||||
>
|
||||
12h (2:00 PM)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="toggle-setting">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingsStore.showOnlyWeekdays}
|
||||
onchange={() =>
|
||||
settingsStore.set('showOnlyWeekdays', !settingsStore.showOnlyWeekdays)}
|
||||
/>
|
||||
<div class="toggle-info">
|
||||
<span class="setting-label">Nur Werktage anzeigen</span>
|
||||
<span class="setting-description">Wochenenden in der Kalenderansicht ausblenden</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="toggle-setting">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingsStore.showWeekNumbers}
|
||||
onchange={() => settingsStore.set('showWeekNumbers', !settingsStore.showWeekNumbers)}
|
||||
/>
|
||||
<div class="toggle-info">
|
||||
<span class="setting-label">Wochennummern anzeigen</span>
|
||||
<span class="setting-description">Kalenderwoche (KW) in der Ansicht anzeigen</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<label class="toggle-setting">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingsStore.filterHoursEnabled}
|
||||
onchange={() =>
|
||||
settingsStore.set('filterHoursEnabled', !settingsStore.filterHoursEnabled)}
|
||||
/>
|
||||
<div class="toggle-info">
|
||||
<span class="setting-label">Stunden filtern</span>
|
||||
<span class="setting-description"
|
||||
>Nur bestimmte Stunden in der Tages-/Wochenansicht anzeigen</span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if settingsStore.filterHoursEnabled}
|
||||
<div class="setting-item hour-range-setting">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Sichtbare Stunden</span>
|
||||
<span class="setting-description"
|
||||
>Zeitbereich der in der Kalenderansicht angezeigt wird</span
|
||||
>
|
||||
</div>
|
||||
<div class="hour-range-inputs">
|
||||
<div class="hour-input-group">
|
||||
<label for="start-hour">Von</label>
|
||||
<select
|
||||
id="start-hour"
|
||||
class="select-input hour-select"
|
||||
value={settingsStore.dayStartHour}
|
||||
onchange={(e) => settingsStore.set('dayStartHour', Number(e.currentTarget.value))}
|
||||
>
|
||||
{#each Array.from({ length: 24 }, (_, i) => i) as hour}
|
||||
{#if hour < settingsStore.dayEndHour}
|
||||
<option value={hour}>{hour.toString().padStart(2, '0')}:00</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<span class="hour-separator">–</span>
|
||||
<div class="hour-input-group">
|
||||
<label for="end-hour">Bis</label>
|
||||
<select
|
||||
id="end-hour"
|
||||
class="select-input hour-select"
|
||||
value={settingsStore.dayEndHour}
|
||||
onchange={(e) => settingsStore.set('dayEndHour', Number(e.currentTarget.value))}
|
||||
>
|
||||
{#each Array.from({ length: 24 }, (_, i) => i + 1) as hour}
|
||||
{#if hour > settingsStore.dayStartHour}
|
||||
<option value={hour}>{hour.toString().padStart(2, '0')}:00</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Ganztägige Termine</span>
|
||||
<span class="setting-description">Wie sollen ganztägige Termine angezeigt werden?</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="group-button"
|
||||
class:active={settingsStore.allDayDisplayMode === 'header'}
|
||||
onclick={() => settingsStore.set('allDayDisplayMode', 'header' as AllDayDisplayMode)}
|
||||
>
|
||||
In Kopfzeile
|
||||
</button>
|
||||
<button
|
||||
class="group-button"
|
||||
class:active={settingsStore.allDayDisplayMode === 'block'}
|
||||
onclick={() => settingsStore.set('allDayDisplayMode', 'block' as AllDayDisplayMode)}
|
||||
>
|
||||
Als Tagesblock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Ganztägige Termine</span>
|
||||
<span class="setting-description">Wie sollen ganztägige Termine angezeigt werden?</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
class="group-button"
|
||||
class:active={settingsStore.allDayDisplayMode === 'header'}
|
||||
onclick={() => settingsStore.set('allDayDisplayMode', 'header' as AllDayDisplayMode)}
|
||||
>
|
||||
In Kopfzeile
|
||||
</button>
|
||||
<button
|
||||
class="group-button"
|
||||
class:active={settingsStore.allDayDisplayMode === 'block'}
|
||||
onclick={() => settingsStore.set('allDayDisplayMode', 'block' as AllDayDisplayMode)}
|
||||
>
|
||||
Als Tagesblock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<!-- Termin-Einstellungen -->
|
||||
<section class="settings-section card">
|
||||
<h2>Termine</h2>
|
||||
<SettingsSection title="Termine">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<SettingsCard>
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Standard-Dauer</span>
|
||||
<span class="setting-description">Voreingestellte Dauer für neue Termine</span>
|
||||
</div>
|
||||
<select
|
||||
class="select-input"
|
||||
value={settingsStore.defaultEventDuration}
|
||||
onchange={(e) => handleEventDurationChange(Number(e.currentTarget.value))}
|
||||
>
|
||||
{#each durationOptions as duration}
|
||||
<option value={duration}>
|
||||
{duration >= 60
|
||||
? `${duration / 60} Stunde${duration > 60 ? 'n' : ''}`
|
||||
: `${duration} Minuten`}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Standard-Dauer</span>
|
||||
<span class="setting-description">Voreingestellte Dauer für neue Termine</span>
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Standard-Erinnerung</span>
|
||||
<span class="setting-description">Voreingestellte Erinnerung für neue Termine</span>
|
||||
</div>
|
||||
<select
|
||||
class="select-input"
|
||||
value={settingsStore.defaultReminder}
|
||||
onchange={(e) => handleReminderChange(Number(e.currentTarget.value))}
|
||||
>
|
||||
{#each reminderOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
class="select-input"
|
||||
value={settingsStore.defaultEventDuration}
|
||||
onchange={(e) => handleEventDurationChange(Number(e.currentTarget.value))}
|
||||
>
|
||||
{#each durationOptions as duration}
|
||||
<option value={duration}>
|
||||
{duration >= 60
|
||||
? `${duration / 60} Stunde${duration > 60 ? 'n' : ''}`
|
||||
: `${duration} Minuten`}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Standard-Erinnerung</span>
|
||||
<span class="setting-description">Voreingestellte Erinnerung für neue Termine</span>
|
||||
</div>
|
||||
<select
|
||||
class="select-input"
|
||||
value={settingsStore.defaultReminder}
|
||||
onchange={(e) => handleReminderChange(Number(e.currentTarget.value))}
|
||||
>
|
||||
{#each reminderOptions as option}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<!-- Konto -->
|
||||
<section class="settings-section card">
|
||||
<h2>Konto</h2>
|
||||
<SettingsSection title="Konto">
|
||||
{#snippet icon()}
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<SettingsCard>
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">E-Mail</span>
|
||||
<span class="setting-value">{authStore.user?.email || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">E-Mail</span>
|
||||
<span class="setting-value">{authStore.user?.email || '-'}</span>
|
||||
<div class="setting-item">
|
||||
<button
|
||||
class="btn btn-ghost text-destructive"
|
||||
onclick={() => authStore.signOut().then(() => goto('/login'))}
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<button
|
||||
class="btn btn-ghost text-destructive"
|
||||
onclick={() => authStore.signOut().then(() => goto('/login'))}
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -472,16 +591,10 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-section h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1rem 0;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
.calendars-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
|
|
@ -634,21 +747,6 @@
|
|||
}
|
||||
|
||||
/* Calendar management styles */
|
||||
.calendars-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
|
||||
.calendars-header h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.new-calendar-form {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
|
|
@ -696,6 +794,168 @@
|
|||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* Edit form styles */
|
||||
.calendar-edit-form {
|
||||
padding: 1.25rem;
|
||||
background: hsl(var(--color-card));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 8px hsl(var(--color-foreground) / 0.08);
|
||||
}
|
||||
|
||||
.edit-form-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.edit-form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.edit-form-group--name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.edit-form-group--color {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.edit-input {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.875rem;
|
||||
font-size: 0.9375rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
background: hsl(var(--color-background));
|
||||
border: 2px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
outline: none;
|
||||
transition:
|
||||
border-color 150ms ease,
|
||||
box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.edit-input:hover {
|
||||
border-color: hsl(var(--color-muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
.edit-input:focus {
|
||||
border-color: hsl(var(--color-primary));
|
||||
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.15);
|
||||
}
|
||||
|
||||
.edit-input::placeholder {
|
||||
color: hsl(var(--color-muted-foreground) / 0.7);
|
||||
}
|
||||
|
||||
.edit-color-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.375rem 0.625rem 0.375rem 0.375rem;
|
||||
background: hsl(var(--color-background));
|
||||
border: 2px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
|
||||
.edit-color-wrapper:hover {
|
||||
border-color: hsl(var(--color-muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
.edit-color-wrapper:focus-within {
|
||||
border-color: hsl(var(--color-primary));
|
||||
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.15);
|
||||
}
|
||||
|
||||
.edit-color-input {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.edit-color-input::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.edit-color-input::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.edit-color-input::-moz-color-swatch {
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.edit-color-value {
|
||||
font-size: 0.8125rem;
|
||||
font-family: monospace;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.edit-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
background: hsl(var(--color-muted) / 0.3);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.edit-checkbox:hover {
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.edit-checkbox input[type='checkbox'] {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
accent-color: hsl(var(--color-primary));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-checkbox input[type='checkbox']:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.edit-checkbox-text {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.edit-checkbox-hint {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-style: italic;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.edit-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
|
||||
.calendar-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -720,6 +980,12 @@
|
|||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background: hsl(var(--color-primary) / 0.15);
|
||||
color: hsl(var(--color-primary));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue