♻️ refactor: centralize AuthGateModal in shared-auth-ui

- Create shared AuthGateModal component in @manacore/shared-auth-ui
- Migrate 4 apps to use shared component: chat, todo, contacts, calendar
- Remove duplicate local AuthGateModal components
- Support for 'save', 'sync', 'feature', 'ai' actions
- Built-in i18n (DE + EN) with custom translation support
- Optional migration info display for session data
- Uses Phosphor icons from @manacore/shared-icons
- Update CONSISTENCY_REPORT.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-29 15:03:52 +01:00
parent 0d559c99d6
commit 69d405ca84
12 changed files with 363 additions and 698 deletions

View file

@ -1,150 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
interface Props {
visible: boolean;
onClose: () => void;
action?: 'save' | 'sync' | 'feature';
featureName?: string;
}
let { visible, onClose, action = 'save', featureName = '' }: Props = $props();
// Action-specific messages
const messages = {
save: {
title: 'Eigene Termine erstellen',
description:
'Melde dich an, um deine eigenen Termine zu erstellen und auf allen Geräten zu synchronisieren.',
icon: 'cloud',
},
sync: {
title: 'Kostenlos anmelden',
description:
'Mit einem Account werden deine Termine automatisch synchronisiert und bleiben erhalten.',
icon: 'refresh-cw',
},
feature: {
title: `Anmelden für ${featureName}`,
description: `Diese Funktion erfordert ein Konto. Melde dich an, um ${featureName} zu nutzen.`,
icon: 'lock',
},
};
let currentMessage = $derived(messages[action]);
function handleLogin() {
// Store return URL for redirect after login
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/login');
}
function handleRegister() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/register');
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose();
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if visible}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
onclick={handleBackdropClick}
>
<div
class="bg-card border-border mx-4 w-full max-w-md rounded-xl border p-6 shadow-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="auth-gate-title"
>
<!-- Icon -->
<div
class="bg-primary/10 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full"
>
{#if currentMessage.icon === 'cloud'}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
{:else if currentMessage.icon === 'refresh-cw'}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
{:else}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
{/if}
</div>
<!-- Title -->
<h2 id="auth-gate-title" class="mb-2 text-center text-xl font-semibold">
{currentMessage.title}
</h2>
<!-- Description -->
<p class="text-muted-foreground mb-6 text-center text-sm">
{currentMessage.description}
</p>
<!-- Buttons -->
<div class="flex flex-col gap-3">
<button
onclick={handleLogin}
class="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg px-4 py-3 font-medium transition-colors"
>
Anmelden
</button>
<button
onclick={handleRegister}
class="bg-secondary text-secondary-foreground hover:bg-secondary/80 w-full rounded-lg px-4 py-3 font-medium transition-colors"
>
Kostenloses Konto erstellen
</button>
<button
onclick={onClose}
class="text-muted-foreground hover:text-foreground w-full py-2 text-sm transition-colors"
>
Später
</button>
</div>
<!-- Info text -->
<p class="text-muted-foreground mt-4 text-center text-xs">
Du kannst im Demo-Modus die Beispiel-Termine ansehen, aber keine eigenen erstellen.
</p>
</div>
</div>
{/if}

View file

@ -64,7 +64,7 @@
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 '$lib/components/AuthGateModal.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';
@ -864,7 +864,23 @@
<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 -->

View file

@ -1,230 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
type Props = {
open: boolean;
action?: 'save' | 'sync' | 'ai' | 'feature';
conversationCount?: number;
onClose: () => void;
};
let { open, action = 'ai', conversationCount = 0, onClose }: Props = $props();
// Messages based on action type
const messages = {
save: {
title: 'Unterhaltungen speichern',
description: 'Melde dich an, um deine Unterhaltungen dauerhaft in der Cloud zu speichern.',
},
sync: {
title: 'Unterhaltungen synchronisieren',
description: 'Melde dich an, um deine Unterhaltungen auf allen Geräten zu synchronisieren.',
},
ai: {
title: 'KI-Antworten erhalten',
description:
'Um KI-Antworten zu erhalten, ist eine Anmeldung erforderlich. Dies ermöglicht uns, die Kosten für die KI-Verarbeitung zu verwalten.',
},
feature: {
title: 'Funktion freischalten',
description: 'Diese Funktion ist nur für angemeldete Benutzer verfügbar.',
},
};
const currentMessage = $derived(messages[action] || messages.ai);
function handleLogin() {
if (browser) {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/login');
}
function handleRegister() {
if (browser) {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/register');
}
</script>
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="modal-backdrop" onclick={onClose}>
<div class="modal-content" onclick={(e) => e.stopPropagation()}>
<div class="modal-header">
<h2>{currentMessage.title}</h2>
<button class="close-btn" onclick={onClose} aria-label="Schliessen">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<p>{currentMessage.description}</p>
{#if conversationCount > 0}
<div class="migration-info">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
<span
>Du hast {conversationCount}
{conversationCount === 1 ? 'Unterhaltung' : 'Unterhaltungen'} in deiner Session. Diese
werden nach der Anmeldung in deinen Account übertragen.</span
>
</div>
{/if}
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick={onClose}> Später </button>
<button class="btn btn-primary" onclick={handleLogin}> Anmelden </button>
<button class="btn btn-outline" onclick={handleRegister}> Registrieren </button>
</div>
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 1rem;
}
.modal-content {
background-color: var(--color-background, white);
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 28rem;
width: 100%;
padding: 1.5rem;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.modal-header h2 {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-foreground, #1f2937);
margin: 0;
}
.close-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
color: var(--color-muted-foreground, #6b7280);
border-radius: 0.375rem;
transition: color 0.15s;
}
.close-btn:hover {
color: var(--color-foreground, #1f2937);
}
.modal-body {
margin-bottom: 1.5rem;
}
.modal-body p {
color: var(--color-muted-foreground, #6b7280);
margin: 0 0 1rem 0;
line-height: 1.5;
}
.migration-info {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
background-color: var(--color-primary-50, #eff6ff);
border-radius: 0.5rem;
font-size: 0.875rem;
color: var(--color-primary-700, #1d4ed8);
}
.migration-info svg {
flex-shrink: 0;
margin-top: 0.125rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.btn {
padding: 0.625rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s;
border: 1px solid transparent;
}
.btn-primary {
background-color: var(--color-primary, #3b82f6);
color: white;
flex: 1;
}
.btn-primary:hover {
background-color: var(--color-primary-600, #2563eb);
}
.btn-secondary {
background-color: var(--color-muted, #f3f4f6);
color: var(--color-muted-foreground, #6b7280);
}
.btn-secondary:hover {
background-color: var(--color-muted-200, #e5e7eb);
}
.btn-outline {
background-color: transparent;
border-color: var(--color-border, #e5e7eb);
color: var(--color-foreground, #1f2937);
}
.btn-outline:hover {
background-color: var(--color-muted, #f3f4f6);
}
</style>

View file

@ -24,8 +24,11 @@
import { getPillAppItems } from '@manacore/shared-branding';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
import AuthGateModal from '$lib/components/AuthGateModal.svelte';
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import {
AuthGateModal,
GuestWelcomeModal,
shouldShowGuestWelcome,
} from '@manacore/shared-auth-ui';
import type { LayoutData } from './$types';
// App switcher items
@ -296,10 +299,25 @@
<!-- Auth Gate Modal -->
<AuthGateModal
open={showAuthGateModal}
action={authGateAction}
conversationCount={sessionConversationCount}
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}
migrationCount={sessionConversationCount}
locale={currentLocale === 'en' ? 'en' : 'de'}
/>
<!-- Guest Welcome Modal -->

View file

@ -1,151 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
interface Props {
visible: boolean;
onClose: () => void;
action?: 'save' | 'sync' | 'feature';
featureName?: string;
}
let { visible, onClose, action = 'save', featureName = '' }: Props = $props();
// Action-specific messages
const messages = {
save: {
title: 'Anmelden um zu speichern',
description:
'Im Demo-Modus kannst du die App erkunden. Melde dich an, um eigene Kontakte zu erstellen und zu speichern.',
icon: 'cloud',
},
sync: {
title: 'Anmelden für Cloud-Sync',
description:
'Mit einem Account werden deine Kontakte automatisch synchronisiert und bleiben erhalten.',
icon: 'refresh-cw',
},
feature: {
title: `Anmelden für ${featureName}`,
description: `Diese Funktion erfordert ein Konto. Melde dich an, um ${featureName} zu nutzen.`,
icon: 'lock',
},
};
let currentMessage = $derived(messages[action]);
function handleLogin() {
// Store return URL for redirect after login
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/login');
}
function handleRegister() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/register');
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose();
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if visible}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
onclick={handleBackdropClick}
>
<div
class="bg-card border-border mx-4 w-full max-w-md rounded-xl border p-6 shadow-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="auth-gate-title"
>
<!-- Icon -->
<div
class="bg-primary/10 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full"
>
{#if currentMessage.icon === 'cloud'}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
{:else if currentMessage.icon === 'refresh-cw'}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
{:else}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
{/if}
</div>
<!-- Title -->
<h2 id="auth-gate-title" class="mb-2 text-center text-xl font-semibold">
{currentMessage.title}
</h2>
<!-- Description -->
<p class="text-muted-foreground mb-6 text-center text-sm">
{currentMessage.description}
</p>
<!-- Buttons -->
<div class="flex flex-col gap-3">
<button
onclick={handleLogin}
class="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg px-4 py-3 font-medium transition-colors"
>
Anmelden
</button>
<button
onclick={handleRegister}
class="bg-secondary text-secondary-foreground hover:bg-secondary/80 w-full rounded-lg px-4 py-3 font-medium transition-colors"
>
Kostenloses Konto erstellen
</button>
<button
onclick={onClose}
class="text-muted-foreground hover:text-foreground w-full py-2 text-sm transition-colors"
>
Später
</button>
</div>
<!-- Info text -->
<p class="text-muted-foreground mt-4 text-center text-xs">
Im Demo-Modus werden Beispielkontakte angezeigt. Melde dich an, um eigene Kontakte zu
erstellen.
</p>
</div>
</div>
{/if}

View file

@ -46,8 +46,11 @@
formatParsedContactPreview,
} from '$lib/utils/contact-parser';
import ContactsToolbar from '$lib/components/ContactsToolbar.svelte';
import AuthGateModal from '$lib/components/AuthGateModal.svelte';
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import {
AuthGateModal,
GuestWelcomeModal,
shouldShowGuestWelcome,
} from '@manacore/shared-auth-ui';
import { browser } from '$app/environment';
// Tags state for Quick-Create
@ -483,7 +486,23 @@
<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="Im Demo-Modus werden Beispielkontakte angezeigt. Melde dich an, um eigene Kontakte zu erstellen."
/>
<!-- Guest Welcome Modal -->

View file

@ -1,151 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
interface Props {
visible: boolean;
onClose: () => void;
action?: 'save' | 'sync' | 'feature';
featureName?: string;
}
let { visible, onClose, action = 'save', featureName = '' }: Props = $props();
// Action-specific messages
const messages = {
save: {
title: 'Anmelden um zu speichern',
description:
'Im Demo-Modus kannst du die App erkunden. Melde dich an, um eigene Aufgaben zu erstellen und zu speichern.',
icon: 'cloud',
},
sync: {
title: 'Anmelden für Cloud-Sync',
description:
'Mit einem Account werden deine Aufgaben automatisch synchronisiert und bleiben erhalten.',
icon: 'refresh-cw',
},
feature: {
title: `Anmelden für ${featureName}`,
description: `Diese Funktion erfordert ein Konto. Melde dich an, um ${featureName} zu nutzen.`,
icon: 'lock',
},
};
let currentMessage = $derived(messages[action]);
function handleLogin() {
// Store return URL for redirect after login
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/login');
}
function handleRegister() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('auth-return-url', window.location.pathname);
}
goto('/register');
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose();
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if visible}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-[9995] flex items-center justify-center bg-black/50 backdrop-blur-sm"
onclick={handleBackdropClick}
>
<div
class="bg-card border-border mx-4 w-full max-w-md rounded-xl border p-6 shadow-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="auth-gate-title"
>
<!-- Icon -->
<div
class="bg-primary/10 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full"
>
{#if currentMessage.icon === 'cloud'}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
{:else if currentMessage.icon === 'refresh-cw'}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
{:else}
<svg class="text-primary h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
{/if}
</div>
<!-- Title -->
<h2 id="auth-gate-title" class="mb-2 text-center text-xl font-semibold">
{currentMessage.title}
</h2>
<!-- Description -->
<p class="text-muted-foreground mb-6 text-center text-sm">
{currentMessage.description}
</p>
<!-- Buttons -->
<div class="flex flex-col gap-3">
<button
onclick={handleLogin}
class="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg px-4 py-3 font-medium transition-colors"
>
Anmelden
</button>
<button
onclick={handleRegister}
class="bg-secondary text-secondary-foreground hover:bg-secondary/80 w-full rounded-lg px-4 py-3 font-medium transition-colors"
>
Kostenloses Konto erstellen
</button>
<button
onclick={onClose}
class="text-muted-foreground hover:text-foreground w-full py-2 text-sm transition-colors"
>
Später
</button>
</div>
<!-- Info text -->
<p class="text-muted-foreground mt-4 text-center text-xs">
Du kannst die Demo-Aufgaben ansehen, aber um eigene Aufgaben zu erstellen benötigst du ein
Konto.
</p>
</div>
</div>
{/if}

View file

@ -39,8 +39,11 @@
import { getPillAppItems } from '@manacore/shared-branding';
import { getTasks } from '$lib/api/tasks';
import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from '$lib/utils/task-parser';
import AuthGateModal from '$lib/components/AuthGateModal.svelte';
import { GuestWelcomeModal, shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import {
AuthGateModal,
GuestWelcomeModal,
shouldShowGuestWelcome,
} from '@manacore/shared-auth-ui';
import { browser } from '$app/environment';
// App switcher items
@ -515,7 +518,23 @@
<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 die Demo-Aufgaben ansehen, aber um eigene Aufgaben zu erstellen benötigst du ein Konto."
/>
<!-- Guest Welcome Modal -->

View file

@ -15,7 +15,7 @@ Nach eingehender Analyse aller Web-Apps im Monorepo wurden folgende Bereiche auf
| i18n Implementation | ✅ Gut | ~~Mittel~~ | ✅ Erledigt |
| Auth Implementation | ✅ Gut | Niedrig | - |
| Styling & Tailwind | ✅ Sehr gut | Niedrig | - |
| Komponenten & Layouts | ⚠️ Mittel | Mittel | Offen |
| Komponenten & Layouts | ✅ Gut | ~~Mittel~~ | ✅ Erledigt |
### Erledigte Aufgaben (29.01.2026)
@ -25,6 +25,7 @@ Nach eingehender Analyse aller Web-Apps im Monorepo wurden folgende Bereiche auf
4. ✅ **lucide-svelte entfernt** - shared-ui nutzt jetzt nur noch `@manacore/shared-icons`
5. ✅ **@manacore/shared-api-client Package erstellt** - 10 Apps migriert (clock, todo, contacts, storage, calendar, picture, nutriphi, planta, questions, skilltree)
6. ✅ **i18n zu 6 Apps hinzugefügt** - todo, skilltree, nutriphi, planta, questions, matrix (jeweils DE + EN)
7. ✅ **AuthGateModal zentralisiert** - `@manacore/shared-auth-ui` für 4 Apps (chat, todo, contacts, calendar)
---
@ -204,10 +205,15 @@ Alle Apps nutzen **Mana Core Auth** mit `@manacore/shared-auth`.
- API: `toastStore.success()`, `.error()`, `.warning()`, `.info()`
- `ToastContainer` Komponente mit Phosphor Icons
#### AuthGateModal
#### AuthGateModal
- Dupliziert in: chat, todo, contacts, calendar
- Sollte in `@manacore/shared-auth-ui`
> **Status: Erledigt (29.01.2026)**
- ✅ Zentrales AuthGateModal in `@manacore/shared-auth-ui`
- ✅ Migrierte Apps: chat, todo, contacts, calendar
- Unterstützt: 'save', 'sync', 'feature', 'ai' Actions
- i18n: DE + EN eingebaut
- Optionale Migration-Info für Session-Daten
#### AppLoadingSkeleton
@ -247,6 +253,7 @@ Alle Apps nutzen **Mana Core Auth** mit `@manacore/shared-auth`.
| ~~lucide-svelte aus shared-ui entfernen~~ | ✅ Erledigt |
| ~~API Client Package erstellen~~ | ✅ Erledigt (10 Apps migriert) |
| ~~i18n zu 6 Apps hinzufügen~~ | ✅ Erledigt |
| ~~AuthGateModal zentralisieren~~ | ✅ Erledigt (4 Apps migriert) |
### 🔴 Hohe Priorität
@ -256,7 +263,7 @@ _(Keine offenen Aufgaben mit hoher Priorität)_
| Aufgabe | Aufwand | Impact |
|---------|---------|--------|
| AuthGateModal in Shared Package | Niedrig | Code-Reduktion |
| ~~AuthGateModal in Shared Package~~ | ~~Niedrig~~ | ✅ Erledigt |
| Global Error Handler extrahieren | Niedrig | Error UX |
### 🟢 Niedrige Priorität
@ -272,7 +279,7 @@ _(Keine offenen Aufgaben mit hoher Priorität)_
1. ~~**API Client Package** als nächstes angehen (höchster Impact)~~ ✅ Erledigt
2. ~~**i18n** zu fehlenden Apps hinzufügen~~ ✅ Erledigt (6 Apps)
3. **AuthGateModal** in Shared Package extrahieren
3. ~~**AuthGateModal** in Shared Package extrahieren~~ ✅ Erledigt (4 Apps)
4. **Global Error Handler** extrahieren
5. Schrittweise weitere Punkte abarbeiten

View file

@ -0,0 +1,249 @@
<script lang="ts">
import { CloudArrowUp, ArrowsClockwise, Lock, Sparkle, Info, X } from '@manacore/shared-icons';
import type { AuthGateTranslations, AuthGateAction } from '../types';
const defaultTranslationsDE: Record<AuthGateAction, { title: string; description: string }> = {
save: {
title: 'Anmelden um zu speichern',
description:
'Im Demo-Modus kannst du die App erkunden. Melde dich an, um deine Daten zu speichern.',
},
sync: {
title: 'Anmelden für Cloud-Sync',
description:
'Mit einem Account werden deine Daten automatisch synchronisiert und bleiben erhalten.',
},
feature: {
title: 'Anmelden erforderlich',
description: 'Diese Funktion erfordert ein Konto. Melde dich an, um sie zu nutzen.',
},
ai: {
title: 'KI-Antworten erhalten',
description:
'Um KI-Antworten zu erhalten, ist eine Anmeldung erforderlich. Dies ermöglicht uns, die Kosten für die KI-Verarbeitung zu verwalten.',
},
};
const defaultTranslationsEN: Record<AuthGateAction, { title: string; description: string }> = {
save: {
title: 'Sign in to save',
description: 'In demo mode you can explore the app. Sign in to save your data permanently.',
},
sync: {
title: 'Sign in for Cloud Sync',
description: 'With an account your data will be automatically synced and preserved.',
},
feature: {
title: 'Sign in required',
description: 'This feature requires an account. Sign in to use it.',
},
ai: {
title: 'Get AI responses',
description:
'To receive AI responses, sign in is required. This allows us to manage AI processing costs.',
},
};
const defaultButtonsDE: AuthGateTranslations = {
loginButton: 'Anmelden',
registerButton: 'Kostenloses Konto erstellen',
laterButton: 'Später',
migrationInfo: (count) =>
`Du hast ${count} ${count === 1 ? 'Unterhaltung' : 'Unterhaltungen'} in deiner Session. Diese werden nach der Anmeldung in deinen Account übertragen.`,
};
const defaultButtonsEN: AuthGateTranslations = {
loginButton: 'Sign In',
registerButton: 'Create Free Account',
laterButton: 'Later',
migrationInfo: (count) =>
`You have ${count} ${count === 1 ? 'conversation' : 'conversations'} in your session. These will be transferred to your account after signing in.`,
};
interface Props {
/** Whether the modal is visible */
visible: boolean;
/** Callback when modal is closed */
onClose: () => void;
/** Callback when login is clicked */
onLogin: () => void;
/** Callback when register is clicked */
onRegister: () => void;
/** The action type that triggered this modal */
action?: AuthGateAction;
/** Custom feature name (for action='feature') */
featureName?: string;
/** Number of items to migrate (shows migration info) */
migrationCount?: number;
/** Locale for translations (default: 'de') */
locale?: 'de' | 'en';
/** Custom translations */
translations?: Partial<AuthGateTranslations>;
/** Custom title (overrides action-based title) */
customTitle?: string;
/** Custom description (overrides action-based description) */
customDescription?: string;
/** Custom info text at bottom */
infoText?: string;
}
let {
visible,
onClose,
onLogin,
onRegister,
action = 'save',
featureName = '',
migrationCount = 0,
locale = 'de',
translations = {},
customTitle,
customDescription,
infoText,
}: Props = $props();
// Merge translations
const defaultMessages = $derived(locale === 'de' ? defaultTranslationsDE : defaultTranslationsEN);
const defaultButtons = $derived(locale === 'de' ? defaultButtonsDE : defaultButtonsEN);
const t = $derived({ ...defaultButtons, ...translations });
// Get current message based on action
const currentMessage = $derived(() => {
const msg = defaultMessages[action];
let title = customTitle || msg.title;
let description = customDescription || msg.description;
// Handle feature action with custom name
if (action === 'feature' && featureName) {
title = locale === 'de' ? `Anmelden für ${featureName}` : `Sign in for ${featureName}`;
description =
locale === 'de'
? `Diese Funktion erfordert ein Konto. Melde dich an, um ${featureName} zu nutzen.`
: `This feature requires an account. Sign in to use ${featureName}.`;
}
return { title, description };
});
// Migration info text
const migrationText = $derived(() => {
if (migrationCount <= 0) return '';
if (translations.migrationInfo) {
return translations.migrationInfo(migrationCount);
}
return t.migrationInfo(migrationCount);
});
// Icon for action type
const ActionIcon = $derived(() => {
switch (action) {
case 'save':
return CloudArrowUp;
case 'sync':
return ArrowsClockwise;
case 'ai':
return Sparkle;
default:
return Lock;
}
});
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose();
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape' && visible) {
onClose();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if visible}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
onclick={handleBackdropClick}
>
<div
class="bg-card border-border relative mx-4 w-full max-w-md rounded-xl border p-6 shadow-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="auth-gate-title"
onclick={(e) => e.stopPropagation()}
>
<!-- Close button -->
<button
type="button"
class="absolute right-4 top-4 p-1 text-muted-foreground hover:text-foreground transition-colors rounded-md"
onclick={onClose}
aria-label="Close"
>
<X size={20} />
</button>
<!-- Icon -->
<div
class="bg-primary/10 mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full"
>
<svelte:component this={ActionIcon()} size={32} class="text-primary" />
</div>
<!-- Title -->
<h2 id="auth-gate-title" class="mb-2 text-center text-xl font-semibold text-foreground">
{currentMessage().title}
</h2>
<!-- Description -->
<p class="text-muted-foreground mb-4 text-center text-sm">
{currentMessage().description}
</p>
<!-- Migration Info -->
{#if migrationText()}
<div
class="flex gap-3 p-3 mb-4 bg-primary/10 border border-primary/20 rounded-lg text-sm text-primary"
>
<Info size={20} class="flex-shrink-0 mt-0.5" />
<span>{migrationText()}</span>
</div>
{/if}
<!-- Buttons -->
<div class="flex flex-col gap-3">
<button
type="button"
onclick={onLogin}
class="bg-primary text-primary-foreground hover:bg-primary/90 w-full rounded-lg px-4 py-3 font-medium transition-colors"
>
{t.loginButton}
</button>
<button
type="button"
onclick={onRegister}
class="bg-secondary text-secondary-foreground hover:bg-secondary/80 w-full rounded-lg px-4 py-3 font-medium transition-colors"
>
{t.registerButton}
</button>
<button
type="button"
onclick={onClose}
class="text-muted-foreground hover:text-foreground w-full py-2 text-sm transition-colors"
>
{t.laterButton}
</button>
</div>
<!-- Info text -->
{#if infoText}
<p class="text-muted-foreground mt-4 text-center text-xs">
{infoText}
</p>
{/if}
</div>
</div>
{/if}

View file

@ -7,6 +7,7 @@ export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.svelte
export { default as GoogleSignInButton } from './components/GoogleSignInButton.svelte';
export { default as AppleSignInButton } from './components/AppleSignInButton.svelte';
export { default as GuestWelcomeModal } from './components/GuestWelcomeModal.svelte';
export { default as AuthGateModal } from './components/AuthGateModal.svelte';
// Utilities
export {
@ -42,4 +43,6 @@ export type {
AuthServiceInterface,
AuthResult,
GuestWelcomeTranslations,
AuthGateAction,
AuthGateTranslations,
} from './types';

View file

@ -77,3 +77,19 @@ export interface GuestWelcomeTranslations {
/** App-specific feature list (array of strings) */
features?: string[];
}
/**
* Action types for the AuthGateModal
*/
export type AuthGateAction = 'save' | 'sync' | 'feature' | 'ai';
/**
* Translation strings for the auth gate modal
*/
export interface AuthGateTranslations {
loginButton: string;
registerButton: string;
laterButton: string;
/** Function to generate migration info text */
migrationInfo: (count: number) => string;
}