mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 23:21:08 +02:00
🔥 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:
parent
3ed1453ff4
commit
82da95b855
4 changed files with 10 additions and 329 deletions
|
|
@ -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_');
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue