🐛 fix(calendar): fix app hanging and layout issues

- Fix $effect infinite loop causing app to hang after guest mode
- Track viewType and currentDate primitives instead of derived viewRange
- Fix root layout to use h-screen flex container for full height
- Fix guest banner offset using padding instead of margin
- Calendar now fills entire screen with UI floating on top
- Simplify i18n initialization
This commit is contained in:
Till-JS 2026-01-26 21:20:29 +01:00
parent be365a0c1e
commit 3df7157389
4 changed files with 47 additions and 61 deletions

View file

@ -35,17 +35,12 @@ function getInitialLocale(): SupportedLocale {
}
// Initialize i18n at module scope (required for SSR)
// Always set initialLocale to ensure it's never undefined
// getInitialLocale() internally checks for browser and falls back to defaultLocale
init({
fallbackLocale: defaultLocale,
initialLocale: browser ? getInitialLocale() : defaultLocale,
initialLocale: getInitialLocale(),
});
// On browser, also explicitly set locale to ensure it's loaded
if (browser) {
locale.set(getInitialLocale());
}
// Set locale and persist to localStorage
export function setLocale(newLocale: SupportedLocale) {
locale.set(newLocale);

View file

@ -854,13 +854,11 @@
}
/* Offset content when guest banner is visible */
.layout-container:has(.guest-banner) .main-content {
margin-top: 40px;
.layout-container:has(.guest-banner) {
padding-top: 40px;
}
.layout-container:has(.guest-banner) .main-content.floating-mode {
padding-top: calc(70px + 40px);
}
/* Floating mode already has padding-top, no extra adjustment needed since container handles banner offset */
/* Mobile: Fixed viewport, no scroll */
@media (max-width: 768px) {
@ -869,6 +867,12 @@
max-height: 100vh;
overflow: hidden;
}
.layout-container:has(.guest-banner) {
height: calc(100vh - 40px);
margin-top: 40px;
padding-top: 0;
}
}
.main-content {
@ -979,6 +983,11 @@
min-height: 0;
}
/* Calendar fills entire screen - UI elements float on top */
.main-content:has(.content-wrapper.calendar-expanded) {
padding-bottom: 0 !important;
}
/* Immersive Mode - fullscreen, no UI elements visible */
.main-content.immersive {
padding: 0 !important;

View file

@ -76,15 +76,25 @@
// Event is automatically removed from store
}
// Track view changes to refetch events
let lastViewType = $state(viewStore.viewType);
let lastDateKey = $state(viewStore.currentDate.toDateString());
onMount(async () => {
// Fetch events for current view range (works in both guest and authenticated mode)
await eventsStore.fetchEvents(viewStore.viewRange.start, viewStore.viewRange.end);
initialized = true;
});
// Refetch events when view changes
// Refetch events when view type or date changes
$effect(() => {
if (initialized) {
const currentViewType = viewStore.viewType;
const currentDateKey = viewStore.currentDate.toDateString();
// Only refetch if view actually changed
if (initialized && (currentViewType !== lastViewType || currentDateKey !== lastDateKey)) {
lastViewType = currentViewType;
lastDateKey = currentDateKey;
eventsStore.fetchEvents(viewStore.viewRange.start, viewStore.viewRange.end);
}
});
@ -224,20 +234,20 @@
width: 24px;
height: 24px;
border-radius: var(--radius-full);
background: hsl(var(--color-surface));
border: 1px solid hsl(var(--color-border));
background: var(--color-surface);
border: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
transition: all 150ms ease;
color: hsl(var(--color-muted-foreground));
color: var(--color-muted-foreground);
}
.sidebar-collapse-btn:hover {
background: hsl(var(--color-muted));
color: hsl(var(--color-foreground));
background: var(--color-muted);
color: var(--color-foreground);
}
.calendar-main {
@ -247,9 +257,9 @@
min-width: 0;
min-height: 0;
overflow: hidden;
background: hsl(var(--color-surface));
background: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid hsl(var(--color-border));
border: 1px solid var(--color-border);
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
@ -268,8 +278,8 @@
.calendar-sidebar-mobile {
width: 100%;
flex-direction: column;
background: hsl(var(--color-surface));
border-top: 1px solid hsl(var(--color-border));
background: var(--color-surface);
border-top: 1px solid var(--color-border);
padding: 0.75rem;
overflow-y: auto;
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
@ -290,22 +300,19 @@
flex-direction: column;
gap: 0;
flex: 1;
height: 100%; /* Fill parent container */
height: 100%;
min-height: 0;
overflow: hidden;
}
/* Hide desktop elements on mobile */
.desktop-only {
display: none !important;
}
/* Show mobile elements */
.mobile-only {
display: flex;
}
/* Calendar container */
.calendar-main {
border-radius: 0;
border: none;
@ -313,25 +320,21 @@
overflow: hidden;
}
/* When todos are visible: 50/50 split */
.calendar-layout:has(.calendar-sidebar-mobile:not(.collapsed)) .calendar-main {
flex: 0 0 50%;
height: 50%;
}
/* When todos are collapsed: calendar takes full space */
.calendar-layout:has(.calendar-sidebar-mobile.collapsed) .calendar-main {
flex: 1;
height: 100%;
}
/* Calendar content must scroll internally */
.calendar-content {
height: 100%;
overflow-y: auto;
}
/* Todos section takes other half */
.calendar-sidebar-mobile {
display: flex;
flex-direction: column;
@ -345,7 +348,6 @@
overflow: hidden;
}
/* Make TodoSidebarSection fill the container */
.calendar-sidebar-mobile > :global(*) {
flex: 1;
min-height: 0;
@ -359,7 +361,6 @@
}
}
/* Tablet: Keep desktop layout but smaller sidebar */
@media (min-width: 769px) and (max-width: 1024px) {
.calendar-sidebar {
width: 220px;

View file

@ -1,51 +1,32 @@
<script lang="ts">
import '../app.css';
// Initialize i18n early - must be imported before any component that uses $_
import { waitLocale } from '$lib/i18n';
import { onMount } from 'svelte';
import '$lib/i18n';
import { theme } from '$lib/stores/theme';
import { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { toastStore } from '$lib/stores/toast.svelte';
import ToastContainer from '$lib/components/ToastContainer.svelte';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
import { isLoading as i18nLoading } from 'svelte-i18n';
import { onMount } from 'svelte';
let { children } = $props();
let loading = $state(true);
let appReady = $derived(!loading && !$i18nLoading);
onMount(async () => {
// Wait for i18n locale to be loaded
await waitLocale();
// Initialize theme
theme.initialize();
// Initialize auth
await authStore.initialize();
loading = false;
});
// Load user settings when authenticated
$effect(() => {
if (authStore.isAuthenticated) {
userSettings.load().then(() => {
// Enable cloud sync for calendar settings after user settings are loaded
settingsStore.enableCloudSync();
});
} else {
settingsStore.disableCloudSync();
}
});
</script>
<ToastContainer />
{#if loading}
{#if !appReady}
<AppLoadingSkeleton />
{:else}
<div class="min-h-screen bg-background text-foreground">
<div class="h-screen flex flex-col bg-background text-foreground overflow-hidden">
{@render children()}
</div>
{/if}
<ToastContainer />