🔥 remove(calendar-web): remove demo mode, enforce login

- Remove demo events and related data files
- Redirect unauthenticated users to /login
- Remove AuthGateModal, GuestWelcomeModal
- Remove demo banner and related CSS
- Simplify events store (no more demo checks)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-30 17:07:17 +01:00
parent 3ed1453ff4
commit 82da95b855
4 changed files with 10 additions and 329 deletions

View file

@ -1,117 +0,0 @@
/**
* Demo Events - Static sample events for unauthenticated users
*
* Shows a realistic calendar with various event types to demonstrate
* the app's capabilities without requiring login.
*/
import type { CalendarEvent } from '@calendar/shared';
import { addDays, setHours, setMinutes, startOfWeek } from 'date-fns';
// Get browser timezone or default to Europe/Berlin
const timezone =
typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'Europe/Berlin';
/**
* Create a demo event with common defaults
*/
function createDemoEvent(
id: string,
title: string,
dayOffset: number,
startHour: number,
startMinute: number,
endHour: number,
endMinute: number,
color: string,
options: {
description?: string;
location?: string;
isAllDay?: boolean;
} = {}
): CalendarEvent {
const now = new Date();
const weekStart = startOfWeek(now, { weekStartsOn: 1 });
const eventDay = addDays(weekStart, dayOffset);
return {
id: `demo_${id}`,
calendarId: 'demo-calendar',
userId: 'demo',
title,
description: options.description,
location: options.location,
startTime: setMinutes(setHours(eventDay, startHour), startMinute).toISOString(),
endTime: setMinutes(setHours(eventDay, endHour), endMinute).toISOString(),
isAllDay: options.isAllDay ?? false,
timezone,
color,
status: 'confirmed',
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
};
}
/**
* Generate demo events relative to the current date
* Events are spread across the current week to always look relevant
*/
export function generateDemoEvents(): CalendarEvent[] {
return [
// Monday - Work meeting
createDemoEvent('1', 'Team Standup', 0, 9, 0, 9, 30, '#3b82f6', {
description: 'Tägliches Standup Meeting mit dem Entwicklungsteam',
location: 'Meeting Room A',
}),
// Monday - Lunch
createDemoEvent('2', 'Mittagessen mit Lisa', 0, 12, 30, 13, 30, '#10b981', {
location: 'Café Central',
}),
// Tuesday - All day event
createDemoEvent('3', 'Projektabgabe', 1, 0, 0, 23, 59, '#f59e0b', {
description: 'Deadline für das Q1 Projekt',
isAllDay: true,
}),
// Tuesday - Client call
createDemoEvent('4', 'Kundengespräch', 1, 14, 0, 15, 0, '#8b5cf6', {
description: 'Quartals-Review mit Kunde XYZ',
location: 'Zoom',
}),
// Wednesday - Workout
createDemoEvent('5', 'Fitness', 2, 7, 0, 8, 30, '#ef4444', {
description: 'Krafttraining',
location: 'FitGym',
}),
// Wednesday - Long meeting
createDemoEvent('6', 'Sprint Planning', 2, 10, 0, 12, 0, '#3b82f6', {
description: 'Planung für den nächsten Sprint',
location: 'Großer Konferenzraum',
}),
// Thursday - Doctor appointment
createDemoEvent('7', 'Arzttermin', 3, 11, 0, 12, 0, '#ec4899', {
description: 'Jährliche Vorsorgeuntersuchung',
location: 'Dr. Müller, Hauptstraße 15',
}),
// Friday - Team event
createDemoEvent('8', 'Team-Event: Escape Room', 4, 16, 0, 19, 0, '#10b981', {
description: 'Teambuilding Aktivität',
location: 'Escape Berlin',
}),
// Saturday - Weekend activity
createDemoEvent('9', 'Brunch mit Familie', 5, 11, 0, 14, 0, '#f59e0b', {
location: 'Bei Oma',
}),
// Sunday - Relaxation
createDemoEvent('10', 'Yoga-Kurs', 6, 10, 0, 11, 30, '#8b5cf6', {
description: 'Sonntags-Entspannung',
location: 'Yoga Studio Harmony',
}),
];
}
/**
* Check if an event ID is a demo event
*/
export function isDemoEvent(id: string): boolean {
return id.startsWith('demo_');
}

View file

@ -1,7 +1,5 @@
/**
* Events Store - Manages calendar events using Svelte 5 runes
* Authenticated users: events from API
* Demo mode: static sample events to showcase the app
*/
import type { CalendarEvent, CreateEventInput, UpdateEventInput } from '@calendar/shared';
@ -9,8 +7,6 @@ import * as api from '$lib/api/events';
import { format, isWithinInterval, isSameDay } from 'date-fns';
import { toDate } from '$lib/utils/eventDateHelpers';
import { toastStore } from '@manacore/shared-ui';
import { authStore } from './auth.svelte';
import { generateDemoEvents, isDemoEvent } from '$lib/data/demo-events';
// State
let events = $state<CalendarEvent[]>([]);
@ -38,31 +34,11 @@ export const eventsStore = {
/**
* Fetch events for a date range
* Demo mode: shows static sample events
* Authenticated: fetches from API
*/
async fetchEvents(startDate: Date, endDate: Date, calendarIds?: string[]) {
loading = true;
error = null;
// Demo mode: load demo events
if (!authStore.isAuthenticated) {
const demoEvents = generateDemoEvents();
// Filter demo events by date range
const filteredEvents = demoEvents.filter((event) => {
const eventStart = toDate(event.startTime);
const eventEnd = toDate(event.endTime);
return eventStart <= endDate && eventEnd >= startDate;
});
events = filteredEvents;
loadedRange = { start: startDate, end: endDate };
loading = false;
return { data: filteredEvents, error: null };
}
// Authenticated: fetch from API
const result = await api.getEvents({
startDate: format(startDate, "yyyy-MM-dd'T'HH:mm:ss"),
endDate: format(endDate, "yyyy-MM-dd'T'HH:mm:ss"),
@ -137,16 +113,8 @@ export const eventsStore = {
/**
* Create a new event
* Demo mode: returns auth_required error
* Authenticated: creates via API
*/
async createEvent(data: CreateEventInput) {
// Demo mode: require authentication
if (!authStore.isAuthenticated) {
return { data: null, error: { code: 'auth_required', message: 'Anmeldung erforderlich' } };
}
// Authenticated: create via API
const result = await api.createEvent(data);
if (result.data) {
@ -158,16 +126,8 @@ export const eventsStore = {
/**
* Update an event
* Demo mode: returns auth_required error
* Authenticated: updates via API
*/
async updateEvent(id: string, data: UpdateEventInput) {
// Demo event: require authentication
if (isDemoEvent(id) || !authStore.isAuthenticated) {
return { data: null, error: { code: 'auth_required', message: 'Anmeldung erforderlich' } };
}
// Cloud event: update via API
const result = await api.updateEvent(id, data);
if (result.error) {
@ -181,16 +141,8 @@ export const eventsStore = {
/**
* Delete an event (optimistic update)
* Demo mode: returns auth_required error
* Authenticated: deletes via API
*/
async deleteEvent(id: string) {
// Demo event: require authentication
if (isDemoEvent(id) || !authStore.isAuthenticated) {
return { data: null, error: { code: 'auth_required', message: 'Anmeldung erforderlich' } };
}
// Cloud event: delete via API
// Optimistic: remove event immediately
const eventToDelete = events.find((e) => e.id === id);
events = events.filter((e) => e.id !== id);
@ -282,11 +234,4 @@ export const eventsStore = {
isDraftEvent(eventId: string) {
return eventId === '__draft__';
},
/**
* Check if an event is a demo event
*/
isDemoEvent(eventId: string) {
return isDemoEvent(eventId);
},
};

View file

@ -64,12 +64,10 @@
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
import ViewModePillContextMenu from '$lib/components/calendar/ViewModePillContextMenu.svelte';
import SettingsModal from '$lib/components/settings/SettingsModal.svelte';
import { AuthGateModal } from '@manacore/shared-auth-ui';
import VoiceRecordButton from '$lib/components/voice/VoiceRecordButton.svelte';
import VoiceRecordingModal from '$lib/components/voice/VoiceRecordingModal.svelte';
import { voiceRecordingStore } from '$lib/stores/voice-recording.svelte';
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import type { CalendarViewType } from '@calendar/shared';
// App switcher items
@ -539,31 +537,6 @@
}
});
// Auth gate modal state
let showAuthGateModal = $state(false);
let authGateAction = $state<'save' | 'sync' | 'feature'>('save');
// Guest welcome modal state
let showGuestWelcome = $state(false);
// Show auth gate modal (can be called from child components)
function showAuthGate(action: 'save' | 'sync' | 'feature' = 'save') {
authGateAction = action;
showAuthGateModal = true;
}
// Listen for show-auth-gate events from child components
$effect(() => {
if (browser) {
const handler = (e: Event) => {
const customEvent = e as CustomEvent<{ action?: 'save' | 'sync' | 'feature' }>;
showAuthGate(customEvent.detail?.action || 'save');
};
window.addEventListener('show-auth-gate', handler);
return () => window.removeEventListener('show-auth-gate', handler);
}
});
// Voice recording result handler
function handleVoiceResult(transcription: string) {
if (!browser) return;
@ -593,25 +566,24 @@
// Initialize auth state from stored tokens
await authStore.initialize();
// Redirect to login if not authenticated
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
// Initialize split-panel from URL/localStorage
splitPanel.initialize();
// Initialize view state
viewStore.initialize();
// Show guest welcome modal for unauthenticated users
if (!authStore.isAuthenticated && shouldShowGuestWelcome('calendar')) {
showGuestWelcome = true;
}
// Load calendars and tags (works in both guest and authenticated mode)
// Load calendars and tags
await calendarsStore.fetchCalendars();
// Only fetch tags and user settings if authenticated
if (authStore.isAuthenticated) {
await eventTagsStore.fetchTags();
await userSettings.load();
}
// Fetch tags and user settings
await eventTagsStore.fetchTags();
await userSettings.load();
// Note: Birthdays are loaded via reactive $effect when showBirthdays is enabled
@ -653,41 +625,6 @@
<SplitPaneContainer>
<div class="layout-container">
<!-- Demo Mode Banner -->
{#if !authStore.isAuthenticated}
<div
class="guest-banner bg-primary/10 border-primary/20 fixed top-0 right-0 left-0 z-50 flex items-center justify-between border-b px-4 py-2"
>
<div class="flex items-center gap-2 text-sm">
<svg class="text-primary h-4 w-4" 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"
/>
<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"
/>
</svg>
<span class="text-foreground">
<strong>Demo-Modus</strong>
<span class="text-muted-foreground hidden sm:inline">
- Beispiel-Termine zum Ausprobieren
</span>
</span>
</div>
<button
onclick={() => showAuthGate('save')}
class="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-3 py-1 text-sm font-medium transition-colors"
>
Anmelden
</button>
</div>
{/if}
<!-- UI Elements (hidden in immersive mode) -->
{#if !settingsStore.immersiveModeEnabled}
<PillNavigation
@ -863,45 +800,6 @@
{isSidebarMode}
/>
<!-- Auth Gate Modal -->
<AuthGateModal
visible={showAuthGateModal}
onClose={() => (showAuthGateModal = false)}
onLogin={() => {
showAuthGateModal = false;
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/login');
}}
onRegister={() => {
showAuthGateModal = false;
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/register');
}}
action={authGateAction}
locale={currentLocale === 'en' ? 'en' : 'de'}
infoText="Du kannst im Demo-Modus die Beispiel-Termine ansehen, aber keine eigenen erstellen."
/>
<!-- Guest Welcome Modal -->
<GuestWelcomeModal
appId="calendar"
visible={showGuestWelcome}
onClose={() => (showGuestWelcome = false)}
onLogin={() => {
showGuestWelcome = false;
goto('/login');
}}
onRegister={() => {
showGuestWelcome = false;
goto('/register');
}}
helpHref="/help"
locale={currentLocale === 'en' ? 'en' : 'de'}
/>
<style>
.layout-container {
@ -911,19 +809,6 @@
overflow: hidden;
}
/* Guest banner styling */
.guest-banner {
height: 40px;
min-height: 40px;
}
/* Offset content when guest banner is visible */
.layout-container:has(.guest-banner) {
padding-top: 40px;
}
/* Floating mode already has padding-top, no extra adjustment needed since container handles banner offset */
/* Mobile: Fixed viewport, no scroll */
@media (max-width: 768px) {
.layout-container {
@ -931,12 +816,6 @@
max-height: 100vh;
overflow: hidden;
}
.layout-container:has(.guest-banner) {
height: calc(100vh - 40px);
margin-top: 40px;
padding-top: 0;
}
}
.main-content {

View file

@ -5,7 +5,6 @@
import { eventsStore } from '$lib/stores/events.svelte';
import { calendarsStore } from '$lib/stores/calendars.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import ViewCarousel from '$lib/components/calendar/ViewCarousel.svelte';
import TodoSidebarSection from '$lib/components/calendar/TodoSidebarSection.svelte';
import QuickEventOverlay from '$lib/components/event/QuickEventOverlay.svelte';
@ -24,20 +23,7 @@
// Generate a unique key for the overlay to force remount
let overlayKey = $state(0);
// Helper to show auth gate modal (dispatch event to layout)
function showAuthGate() {
if (browser) {
window.dispatchEvent(new CustomEvent('show-auth-gate', { detail: { action: 'save' } }));
}
}
function handleQuickCreate(date: Date, position: { x: number; y: number }) {
// Demo mode: show auth gate instead of creating event
if (!authStore.isAuthenticated) {
showAuthGate();
return;
}
// Close any existing overlay first
editingEvent = null;
@ -60,12 +46,6 @@
}
function handleEventClick(event: CalendarEvent) {
// Demo mode: show auth gate for demo events
if (eventsStore.isDemoEvent(event.id)) {
showAuthGate();
return;
}
// Close any existing overlay/draft first
eventsStore.clearDraftEvent();
@ -106,12 +86,6 @@
}
function handleVoiceEventCreate(event: CustomEvent<VoiceEventData>) {
// Demo mode: show auth gate instead of creating event
if (!authStore.isAuthenticated) {
showAuthGate();
return;
}
const data = event.detail;
// Close any existing overlay first