mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:21:10 +02:00
🎨 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:
parent
a00a02a822
commit
3799fe1b54
15 changed files with 466 additions and 329 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
32
apps/calendar/apps/web/src/lib/components/AppSlider.svelte
Normal file
32
apps/calendar/apps/web/src/lib/components/AppSlider.svelte
Normal 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}
|
||||
/>
|
||||
|
|
@ -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" />
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
15
apps/calendar/apps/web/src/lib/services/feedback.ts
Normal file
15
apps/calendar/apps/web/src/lib/services/feedback.ts
Normal 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(),
|
||||
});
|
||||
|
|
@ -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';
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
5
apps/calendar/apps/web/src/routes/(auth)/+layout.svelte
Normal file
5
apps/calendar/apps/web/src/routes/(auth)/+layout.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
40
apps/calendar/apps/web/src/routes/mana/+page.svelte
Normal file
40
apps/calendar/apps/web/src/routes/mana/+page.svelte
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue