🎨 style(calendar): improve UI components and integrate shared-auth-ui

- Refactor CSS from @layer to plain CSS for guaranteed inclusion
- Add Svelte 5 runes safety checks in calendars/events stores
- Integrate shared-auth-ui pages with CalendarLogo and translations
- Add AppSlider and LanguageSelector components
- Add feedback service and mana route
- Add auth route detection in layout to skip navigation on auth pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-02 14:39:23 +01:00
parent a00a02a822
commit 3799fe1b54
15 changed files with 466 additions and 329 deletions

View file

@ -2,7 +2,9 @@
@import "@manacore/shared-tailwind/themes.css";
/* Scan shared packages for Tailwind classes */
@source "../../../../packages/shared/src";
@source "../../../packages/shared/src";
@source "../../../../../packages/shared-ui/src";
@source "../../../../../packages/shared-theme-ui/src";
/* Calendar-specific CSS Variables */
@layer base {
@ -34,166 +36,204 @@
}
}
/* Calendar Grid Styles */
@layer components {
/* Hour slot in day/week view */
.hour-slot {
height: var(--hour-height);
border-bottom: 1px solid hsl(var(--border) / 0.5);
position: relative;
}
/* Calendar Grid Styles - Using plain CSS (not @layer) for guaranteed inclusion */
/* Hour slot in day/week view */
.hour-slot {
height: var(--hour-height);
border-bottom: 1px solid hsl(var(--border) / 0.5);
position: relative;
}
.hour-slot:hover {
background-color: hsl(var(--muted) / 0.3);
}
.hour-slot:hover {
background-color: hsl(var(--muted) / 0.3);
}
/* Event card in calendar */
.event-card {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: var(--radius-sm);
padding: 2px 6px;
font-size: 0.75rem;
overflow: hidden;
cursor: pointer;
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
/* Event card in calendar */
.event-card {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: var(--radius-sm);
padding: 2px 6px;
font-size: 0.75rem;
overflow: hidden;
cursor: pointer;
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.event-card:hover {
transform: scale(1.02);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.event-card:hover {
transform: scale(1.02);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
/* Day cell in month view */
.day-cell {
min-height: 100px;
border: 1px solid hsl(var(--border));
padding: var(--spacing-xs);
transition: background-color var(--transition-fast);
}
/* Day cell in month view */
.day-cell {
min-height: 100px;
border: 1px solid hsl(var(--border));
padding: var(--spacing-xs);
transition: background-color var(--transition-fast);
}
.day-cell:hover {
background-color: hsl(var(--muted) / 0.3);
}
.day-cell:hover {
background-color: hsl(var(--muted) / 0.3);
}
.day-cell.today {
background-color: hsl(var(--primary) / 0.1);
}
.day-cell.today {
background-color: hsl(var(--primary) / 0.1);
}
.day-cell.other-month {
opacity: 0.5;
}
.day-cell.other-month {
opacity: 0.5;
}
/* Time indicator (current time line) */
.time-indicator {
position: absolute;
left: 0;
right: 0;
height: 2px;
background-color: hsl(var(--destructive));
z-index: 10;
}
/* Time indicator (current time line) */
.time-indicator {
position: absolute;
left: 0;
right: 0;
height: 2px;
background-color: hsl(var(--destructive));
z-index: 10;
}
.time-indicator::before {
content: '';
position: absolute;
left: -4px;
top: -4px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: hsl(var(--destructive));
}
.time-indicator::before {
content: '';
position: absolute;
left: -4px;
top: -4px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: hsl(var(--destructive));
}
/* Mini calendar */
.mini-calendar {
font-size: 0.875rem;
}
/* Mini calendar */
.mini-calendar {
font-size: 0.875rem;
}
.mini-calendar .day {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-full);
cursor: pointer;
transition: all var(--transition-fast);
}
.mini-calendar .day {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-full);
cursor: pointer;
transition: all var(--transition-fast);
}
.mini-calendar .day:hover {
background-color: hsl(var(--muted));
}
.mini-calendar .day:hover {
background-color: hsl(var(--muted));
}
.mini-calendar .day.today {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.mini-calendar .day.today {
background-color: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.mini-calendar .day.selected {
border: 2px solid hsl(var(--primary));
}
.mini-calendar .day.selected {
border: 2px solid hsl(var(--primary));
}
/* Card styles */
.card {
background-color: hsl(var(--card));
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
border: 1px solid hsl(var(--border));
}
/* Card styles */
.card {
background-color: hsl(var(--card));
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
border: 1px solid hsl(var(--border));
}
/* Button styles */
.btn {
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius-md);
font-weight: 500;
transition: all var(--transition-base);
cursor: pointer;
border: none;
}
/* Button styles */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: var(--radius-md);
font-weight: 500;
font-size: 0.875rem;
transition: all var(--transition-base);
cursor: pointer;
border: none;
background: transparent;
}
.btn-primary {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.btn-primary {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.btn-primary:hover {
background: hsl(var(--primary) / 0.9);
}
.btn-primary:hover {
background: hsl(var(--primary) / 0.9);
}
.btn-secondary {
background: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary:hover {
background: hsl(var(--secondary) / 0.8);
}
.btn-secondary {
background: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
}
.btn-ghost {
background: transparent;
color: hsl(var(--foreground));
}
.btn-secondary:hover {
background: hsl(var(--secondary) / 0.8);
}
.btn-ghost:hover {
background: hsl(var(--muted));
}
.btn-ghost {
background: transparent;
color: hsl(var(--foreground));
}
/* Input styles */
.input {
padding: var(--spacing-sm) var(--spacing-md);
border: 2px solid hsl(var(--border));
border-radius: var(--radius-md);
background-color: hsl(var(--background));
color: hsl(var(--foreground));
transition: border-color var(--transition-fast);
width: 100%;
}
.btn-ghost:hover {
background: hsl(var(--muted));
}
.input:focus {
outline: none;
border-color: hsl(var(--primary));
}
.btn-icon {
padding: 0.5rem;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
/* Input styles */
.input {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
border: 2px solid hsl(var(--border));
border-radius: var(--radius-md);
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-size: 0.875rem;
transition: border-color var(--transition-fast);
}
.input:focus {
outline: none;
border-color: hsl(var(--primary));
}
.input::placeholder {
color: hsl(var(--muted-foreground));
}
/* Select styling */
select.input {
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
/* Text colors */
.text-destructive {
color: hsl(var(--destructive));
}
/* Scrollbar styling */

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { AppSlider, type AppItem } from '@manacore/shared-ui';
import { MANA_APPS, APP_STATUS_LABELS, APP_SLIDER_LABELS } from '@manacore/shared-branding';
// Convert MANA_APPS to AppItem format (German)
const apps: AppItem[] = MANA_APPS.map((app) => ({
name: app.name,
description: app.description.de,
longDescription: app.longDescription.de,
icon: app.icon,
color: app.color,
comingSoon: app.comingSoon,
status: app.status,
}));
const statusLabels = APP_STATUS_LABELS.de;
const labels = APP_SLIDER_LABELS.de;
function handleAppClick(app: AppItem, index: number) {
console.log('Opening app:', app.name);
}
</script>
<AppSlider
{apps}
title={labels.title}
isDark={false}
{statusLabels}
comingSoonLabel={labels.comingSoon}
openAppLabel={labels.openApp}
onAppClick={handleAppClick}
/>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { locale } from 'svelte-i18n';
import { PillDropdown } from '@manacore/shared-ui';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { setLocale, supportedLocales } from '$lib/i18n';
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLabel = $derived(getCurrentLanguageLabel(currentLocale));
</script>
<PillDropdown items={languageItems} label={currentLabel} direction="down" />

View file

@ -77,143 +77,90 @@
}
</script>
<form onsubmit={handleSubmit}>
<div class="form-group">
<label for="title">Titel *</label>
<form onsubmit={handleSubmit} class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<label for="title" class="text-sm font-medium text-foreground">Titel *</label>
<input
type="text"
id="title"
class="input"
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors"
bind:value={title}
placeholder="Terminname eingeben"
required
/>
</div>
<div class="form-group">
<label for="calendar">Kalender</label>
<select id="calendar" class="input" bind:value={calendarId}>
<div class="flex flex-col gap-2">
<label for="calendar" class="text-sm font-medium text-foreground">Kalender</label>
<select id="calendar" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={calendarId}>
{#each calendarsStore.calendars as cal}
<option value={cal.id}>{cal.name}</option>
{/each}
</select>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" bind:checked={isAllDay} />
Ganztägig
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" bind:checked={isAllDay} class="w-4 h-4 accent-primary" />
<span class="text-sm font-medium text-foreground">Ganztägig</span>
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="startDate">Beginn</label>
<input type="date" id="startDate" class="input" bind:value={startDate} required />
<div class="flex gap-4">
<div class="flex-1 flex flex-col gap-2">
<label for="startDate" class="text-sm font-medium text-foreground">Beginn</label>
<input type="date" id="startDate" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={startDate} required />
</div>
{#if !isAllDay}
<div class="form-group">
<label for="startTime">Uhrzeit</label>
<input type="time" id="startTime" class="input" bind:value={startTime} required />
<div class="flex-1 flex flex-col gap-2">
<label for="startTime" class="text-sm font-medium text-foreground">Uhrzeit</label>
<input type="time" id="startTime" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={startTime} required />
</div>
{/if}
</div>
<div class="form-row">
<div class="form-group">
<label for="endDate">Ende</label>
<input type="date" id="endDate" class="input" bind:value={endDate} required />
<div class="flex gap-4">
<div class="flex-1 flex flex-col gap-2">
<label for="endDate" class="text-sm font-medium text-foreground">Ende</label>
<input type="date" id="endDate" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={endDate} required />
</div>
{#if !isAllDay}
<div class="form-group">
<label for="endTime">Uhrzeit</label>
<input type="time" id="endTime" class="input" bind:value={endTime} required />
<div class="flex-1 flex flex-col gap-2">
<label for="endTime" class="text-sm font-medium text-foreground">Uhrzeit</label>
<input type="time" id="endTime" class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors" bind:value={endTime} required />
</div>
{/if}
</div>
<div class="form-group">
<label for="location">Ort</label>
<div class="flex flex-col gap-2">
<label for="location" class="text-sm font-medium text-foreground">Ort</label>
<input
type="text"
id="location"
class="input"
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors"
bind:value={location}
placeholder="Ort hinzufügen"
/>
</div>
<div class="form-group">
<label for="description">Beschreibung</label>
<div class="flex flex-col gap-2">
<label for="description" class="text-sm font-medium text-foreground">Beschreibung</label>
<textarea
id="description"
class="input"
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors resize-y min-h-20"
rows="3"
bind:value={description}
placeholder="Beschreibung hinzufügen"
></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-ghost" onclick={onCancel}>
<div class="flex justify-end gap-3 pt-4 border-t border-border">
<button type="button" class="px-4 py-2 rounded-lg font-medium text-foreground bg-transparent hover:bg-muted transition-colors" onclick={onCancel}>
Abbrechen
</button>
<button type="submit" class="btn btn-primary" disabled={submitting || !title.trim()}>
<button type="submit" class="px-4 py-2 rounded-lg font-medium text-primary-foreground bg-primary hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" disabled={submitting || !title.trim()}>
{mode === 'create' ? 'Erstellen' : 'Speichern'}
</button>
</div>
</form>
<style>
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
}
.form-row {
display: flex;
gap: 1rem;
}
.form-row .form-group {
flex: 1;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input {
width: 18px;
height: 18px;
}
textarea.input {
resize: vertical;
min-height: 80px;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid hsl(var(--border));
}
</style>

View file

@ -0,0 +1,15 @@
/**
* Feedback Service Instance for Calendar Web App
*/
import { createFeedbackService } from '@manacore/shared-feedback-service';
import { authStore } from '$lib/stores/auth.svelte';
import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public';
const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001';
export const feedbackService = createFeedbackService({
apiUrl: MANA_AUTH_URL,
appId: 'calendar',
getAuthToken: async () => authStore.getAccessToken(),
});

View file

@ -10,11 +10,20 @@ let calendars = $state<Calendar[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
// Helper to safely get calendars array (Svelte 5 runes safety)
function getCalendarsArray(): Calendar[] {
const arr = calendars ?? [];
return Array.isArray(arr) ? arr : [];
}
// Derived: visible calendars
const visibleCalendars = $derived(calendars.filter((c) => c.isVisible));
const visibleCalendars = $derived(getCalendarsArray().filter((c) => c.isVisible));
// Derived: default calendar
const defaultCalendar = $derived(calendars.find((c) => c.isDefault) || calendars[0]);
const defaultCalendar = $derived.by(() => {
const arr = getCalendarsArray();
return arr.find((c) => c.isDefault) || arr[0] || null;
});
export const calendarsStore = {
// Getters
@ -74,7 +83,7 @@ export const calendarsStore = {
const result = await api.updateCalendar(id, data);
if (result.data) {
calendars = calendars.map((c) => (c.id === id ? result.data! : c));
calendars = getCalendarsArray().map((c) => (c.id === id ? result.data! : c));
}
return result;
@ -87,7 +96,7 @@ export const calendarsStore = {
const result = await api.deleteCalendar(id);
if (!result.error) {
calendars = calendars.filter((c) => c.id !== id);
calendars = getCalendarsArray().filter((c) => c.id !== id);
}
return result;
@ -97,7 +106,8 @@ export const calendarsStore = {
* Toggle calendar visibility
*/
async toggleVisibility(id: string) {
const calendar = calendars.find((c) => c.id === id);
const arr = getCalendarsArray();
const calendar = arr.find((c) => c.id === id);
if (!calendar) return;
return this.updateCalendar(id, { isVisible: !calendar.isVisible });
@ -107,14 +117,14 @@ export const calendarsStore = {
* Get calendar by ID
*/
getById(id: string) {
return calendars.find((c) => c.id === id);
return getCalendarsArray().find((c) => c.id === id);
},
/**
* Get calendar color by ID (with fallback)
*/
getColor(id: string) {
const calendar = calendars.find((c) => c.id === id);
const calendar = getCalendarsArray().find((c) => c.id === id);
return calendar?.color || '#3b82f6';
},
};

View file

@ -52,7 +52,11 @@ export const eventsStore = {
* Get events for a specific day
*/
getEventsForDay(date: Date) {
return events.filter((event) => {
// Safety check: ensure events is an array (Svelte 5 runes safety)
const currentEvents = events ?? [];
if (!Array.isArray(currentEvents)) return [];
return currentEvents.filter((event) => {
const eventStart = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
const eventEnd = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
@ -70,7 +74,11 @@ export const eventsStore = {
* Get events within a time range
*/
getEventsInRange(start: Date, end: Date) {
return events.filter((event) => {
// Safety check: ensure events is an array (Svelte 5 runes safety)
const currentEvents = events ?? [];
if (!Array.isArray(currentEvents)) return [];
return currentEvents.filter((event) => {
const eventStart = typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
const eventEnd = typeof event.endTime === 'string' ? parseISO(event.endTime) : event.endTime;
@ -122,7 +130,11 @@ export const eventsStore = {
* Get event by ID
*/
getById(id: string) {
return events.find((e) => e.id === id);
// Safety check: ensure events is an array (Svelte 5 runes safety)
const currentEvents = events ?? [];
if (!Array.isArray(currentEvents)) return undefined;
return currentEvents.find((e) => e.id === id);
},
/**

View file

@ -0,0 +1,5 @@
<script lang="ts">
let { children } = $props();
</script>
{@render children()}

View file

@ -1,27 +1,41 @@
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { goto } from '$app/navigation';
import { locale } from 'svelte-i18n';
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { getForgotPasswordTranslations } from '@manacore/shared-i18n';
import { CalendarLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import AppSlider from '$lib/components/AppSlider.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import '$lib/i18n';
async function handleResetPassword(email: string) {
const result = await authStore.resetPassword(email);
// Get translations based on current locale
const translations = $derived(getForgotPasswordTranslations($locale || 'de'));
if (!result.success) {
toast.error(result.error || 'Anfrage fehlgeschlagen');
return { success: false, error: result.error };
}
toast.success('E-Mail gesendet. Bitte überprüfen Sie Ihren Posteingang.');
return { success: true };
async function handleForgotPassword(email: string) {
return authStore.resetPassword(email);
}
</script>
<svelte:head>
<title>Passwort vergessen | Kalender</title>
<title>{translations.titleForm} | Kalender</title>
</svelte:head>
<ForgotPasswordPage
onResetPassword={handleResetPassword}
loginUrl="/login"
appName="Kalender"
/>
logo={CalendarLogo}
primaryColor="#0ea5e9"
onForgotPassword={handleForgotPassword}
{goto}
loginPath="/login"
lightBackground="#e0f2fe"
darkBackground="#0c1929"
{translations}
>
{#snippet headerControls()}
<LanguageSelector />
{/snippet}
{#snippet appSlider()}
<AppSlider />
{/snippet}
</ForgotPasswordPage>

View file

@ -1,30 +1,49 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { LoginPage } from '@manacore/shared-auth-ui';
import { getLoginTranslations } from '@manacore/shared-i18n';
import { CalendarLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import AppSlider from '$lib/components/AppSlider.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import '$lib/i18n';
async function handleLogin(email: string, password: string) {
const result = await authStore.signIn(email, password);
// Get redirect URL from query params
const redirectTo = $derived($page.url.searchParams.get('redirectTo') || '/');
if (!result.success) {
toast.error(result.error || 'Anmeldung fehlgeschlagen');
return { success: false, error: result.error };
}
// Get translations based on current locale
const translations = $derived(getLoginTranslations($locale || 'de'));
toast.success('Erfolgreich angemeldet');
goto('/');
return { success: true };
async function handleSignIn(email: string, password: string) {
return authStore.signIn(email, password);
}
</script>
<svelte:head>
<title>Anmelden | Kalender</title>
<title>{translations.title} | Kalender</title>
</svelte:head>
<LoginPage
onLogin={handleLogin}
registerUrl="/register"
forgotPasswordUrl="/forgot-password"
appName="Kalender"
/>
logo={CalendarLogo}
primaryColor="#0ea5e9"
onSignIn={handleSignIn}
{goto}
enableGoogle={false}
enableApple={false}
successRedirect={redirectTo}
registerPath="/register"
forgotPasswordPath="/forgot-password"
lightBackground="#e0f2fe"
darkBackground="#0c1929"
{translations}
>
{#snippet headerControls()}
<LanguageSelector />
{/snippet}
{#snippet appSlider()}
<AppSlider />
{/snippet}
</LoginPage>

View file

@ -1,35 +1,42 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { locale } from 'svelte-i18n';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { getRegisterTranslations } from '@manacore/shared-i18n';
import { CalendarLogo } from '@manacore/shared-branding';
import { authStore } from '$lib/stores/auth.svelte';
import AppSlider from '$lib/components/AppSlider.svelte';
import LanguageSelector from '$lib/components/LanguageSelector.svelte';
import '$lib/i18n';
async function handleRegister(email: string, password: string) {
const result = await authStore.signUp(email, password);
// Get translations based on current locale
const translations = $derived(getRegisterTranslations($locale || 'de'));
if (!result.success) {
toast.error(result.error || 'Registrierung fehlgeschlagen');
return { success: false, error: result.error };
}
if (result.needsVerification) {
toast.info('Bitte bestätigen Sie Ihre E-Mail-Adresse');
goto('/login');
return { success: true, needsVerification: true };
}
toast.success('Erfolgreich registriert');
goto('/');
return { success: true };
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
}
</script>
<svelte:head>
<title>Registrieren | Kalender</title>
<title>{translations.title} | Kalender</title>
</svelte:head>
<RegisterPage
onRegister={handleRegister}
loginUrl="/login"
appName="Kalender"
/>
logo={CalendarLogo}
primaryColor="#0ea5e9"
onSignUp={handleSignUp}
{goto}
successRedirect="/"
loginPath="/login"
lightBackground="#e0f2fe"
darkBackground="#0c1929"
{translations}
>
{#snippet headerControls()}
<LanguageSelector />
{/snippet}
{#snippet appSlider()}
<AppSlider />
{/snippet}
</RegisterPage>

View file

@ -66,6 +66,13 @@
// User email for user dropdown
let userEmail = $derived(authStore.user?.email || 'Menü');
// Check if current route is an auth route (no navigation needed)
let isAuthRoute = $derived(
$page.url.pathname.startsWith('/login') ||
$page.url.pathname.startsWith('/register') ||
$page.url.pathname.startsWith('/forgot-password')
);
// Navigation items for Calendar
const navItems: PillNavItem[] = [
{ href: '/', label: 'Kalender', icon: 'calendar' },
@ -163,7 +170,10 @@
<ToastContainer />
{#if loading}
{#if isAuthRoute}
<!-- Auth routes: no navigation, just render content -->
{@render children()}
{:else if loading}
<div class="flex min-h-screen items-center justify-center bg-background">
<div class="text-center">
<div

View file

@ -50,8 +50,11 @@
<div class="calendar-layout">
<!-- Left Sidebar -->
<aside class="calendar-sidebar">
<button class="btn btn-primary w-full mb-4" onclick={handleNewEvent}>
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<button
class="w-full mb-4 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium text-primary-foreground bg-primary hover:bg-primary/90 transition-colors"
onclick={handleNewEvent}
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Neuer Termin

View file

@ -1,47 +1,11 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { FeedbackPage } from '@manacore/shared-feedback-ui';
import { createFeedbackService } from '@manacore/shared-feedback-service';
import { env } from '$env/dynamic/public';
const feedbackService = createFeedbackService(
env.PUBLIC_BACKEND_URL || 'http://localhost:3014'
);
onMount(() => {
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
});
async function handleSubmit(type: string, message: string) {
const token = await authStore.getAccessToken();
if (!token) {
toast.error('Bitte melden Sie sich an');
return { success: false, error: 'Not authenticated' };
}
const result = await feedbackService.submit(
{ type: type as any, message },
token
);
if (!result.success) {
toast.error(result.error || 'Fehler beim Senden');
return result;
}
toast.success('Feedback gesendet. Vielen Dank!');
return result;
}
import { feedbackService } from '$lib/services/feedback';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<svelte:head>
<title>Feedback | Kalender</title>
</svelte:head>
<FeedbackPage onSubmit={handleSubmit} appName="Kalender" />
<FeedbackPage {feedbackService} appName="Kalender" currentUserId={authStore.user?.id} />

View file

@ -0,0 +1,40 @@
<script lang="ts">
import { SubscriptionPage } from '@manacore/shared-subscription-ui';
import { toast } from '$lib/stores/toast';
function handleSubscribe(planId: string) {
console.log('Subscribe to plan:', planId);
toast.info(`Abo "${planId}" ausgewählt. Bezahlsystem wird noch integriert.`);
}
function handleBuyPackage(packageId: string) {
console.log('Buy package:', packageId);
toast.info(`Paket "${packageId}" ausgewählt. Bezahlsystem wird noch integriert.`);
}
</script>
<svelte:head>
<title>Mana - Kalender</title>
</svelte:head>
<div class="mana-page">
<SubscriptionPage
appName="Kalender"
onSubscribe={handleSubscribe}
onBuyPackage={handleBuyPackage}
currentPlanId="free"
pageTitle="Wähle dein Abo"
subscriptionsTitle="Abonnements"
packagesTitle="Einmal-Pakete"
yearlyDiscount="2 Monate gratis"
/>
</div>
<style>
.mana-page {
min-height: 100%;
width: 100%;
overflow-x: hidden;
background-color: hsl(var(--background));
}
</style>