feat: smoother loading when checking auth status

This commit is contained in:
ae 2025-04-19 22:58:12 +03:00
parent cae360fc0e
commit 7a0c0a9007
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E
6 changed files with 161 additions and 196 deletions

View File

@ -501,14 +501,48 @@
}
/* Loading spinner */
.loading-container {
@apply pointer-events-none flex h-screen w-full items-center justify-center;
}
.spinner {
@apply relative h-10 w-10;
}
.spinner-dot {
@apply bg-[var(--light-accent)];
@apply absolute top-[37.5%] inline-block h-1/4 w-1/4 rounded-full bg-[var(--light-accent)];
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
.dark .spinner-dot {
@apply bg-[var(--dark-accent)];
}
.spinner .bounce1 {
@apply left-0;
animation-delay: -0.32s;
}
.spinner .bounce2 {
@apply left-[37.5%];
animation-delay: -0.16s;
}
.spinner .bounce3 {
@apply left-3/4;
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/* Main layout */
.main-layout-container {
@apply flex h-screen bg-[var(--light-background)];

View File

@ -5,6 +5,15 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
<script>
const darkMode = localStorage.getItem("darkMode") === "true" ||
(localStorage.getItem("darkMode") === null &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
if (darkMode) {
document.documentElement.classList.add("dark")
}
</script>
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>

View File

@ -1,51 +1,7 @@
<script>
export let size = "40px"
// export let useDarkAccent = false
</script>
<div class="spinner" style="width: {size}; height: {size};">
<div class="bounce1 spinner-dot"></div>
<div class="bounce2 spinner-dot"></div>
<div class="bounce3 spinner-dot"></div>
<div class="loading-container">
<div class="spinner">
<div class="bounce1 spinner-dot"></div>
<div class="bounce2 spinner-dot"></div>
<div class="bounce3 spinner-dot"></div>
</div>
</div>
<style>
.spinner {
position: relative;
}
.spinner > div {
width: 25%;
height: 25%;
border-radius: 100%;
display: inline-block;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
position: absolute;
top: 37.5%;
}
.spinner .bounce1 {
left: 0;
animation-delay: -0.32s;
}
.spinner .bounce2 {
left: 37.5%;
animation-delay: -0.16s;
}
.spinner .bounce3 {
left: 75%;
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
</style>

View File

@ -17,18 +17,14 @@
import ToggleSidebar from "$lib/icons/ToggleSidebar.svelte"
import VersionArrow from "$lib/icons/VersionArrow.svelte"
// Props
export let isLoading = false
// State
let isComponentReady = false
let sidebarOpen = window.innerWidth > 768
let showSettings = false
let showVersionsDropdown = false
let isEditing = false
onMount(async (): Promise<any> => {
isLoading = true
try {
// The following fetch attempts to refresh any expired tokens automatically
await apiClient.getCurrentUser()
@ -44,8 +40,6 @@
} catch (error) {
console.error(`error during auth: ${error}`)
goto("/login")
} finally {
isLoading = false
}
// Default to sidebar closed on mobile
@ -60,6 +54,8 @@
// Keep listening to browser's resize events
window.addEventListener("resize", handleResize)
isComponentReady = true
return () => {
window.removeEventListener("resize", handleResize)
}
@ -173,113 +169,115 @@
}
</script>
<div class="main-layout-container">
<!-- Error notification -->
{#if $cError}
<div class="main-error-popup">
<div class="error">
{$cError}
<button class="ml-2 text-red-700" on:click={() => cError.set(null)}> × </button>
{#if isComponentReady}
<div class="main-layout-container">
<!-- Error notification -->
{#if $cError}
<div class="main-error-popup">
<div class="error">
{$cError}
<button class="ml-2 text-red-700" on:click={() => cError.set(null)}> × </button>
</div>
</div>
</div>
{/if}
{/if}
<!-- Sidebar (2-way binding prop allows changing the same var. in either component) -->
<Sidebar
bind:sidebarOpen
notes={$availableNotes || []}
currentNote={$currentFullNote}
{toggleSettings}
{logout}
{createNewNote}
{selectNote}
on:close={closeSidebar}
/>
<!-- Sidebar (2-way binding prop allows changing the same var. in either component) -->
<Sidebar
bind:sidebarOpen
notes={$availableNotes || []}
currentNote={$currentFullNote}
{toggleSettings}
{logout}
{createNewNote}
{selectNote}
on:close={closeSidebar}
/>
<!-- Main content -->
<div
class="flex flex-1 flex-col overflow-hidden transition-all duration-300"
class:md:-ml-64={!sidebarOpen}
class:md:ml-0={sidebarOpen}
>
<!-- Top navbar -->
<header class="main-header">
<button
on:click={toggleSidebar}
class="btn-secondary rounded-full p-2"
aria-label="Toggle sidebar"
>
<ToggleSidebar />
</button>
<!-- Main content -->
<div
class="flex flex-1 flex-col overflow-hidden transition-all duration-300"
class:md:-ml-64={!sidebarOpen}
class:md:ml-0={sidebarOpen}
>
<!-- Top navbar -->
<header class="main-header">
<button
on:click={toggleSidebar}
class="btn-secondary rounded-full p-2"
aria-label="Toggle sidebar"
>
<ToggleSidebar />
</button>
<div class="flex items-center space-x-4 pl-2">
{#if $currentFullNote}
<!-- Note edit/preview button (only shown for latest version) -->
{#if isLatestVersion($currentFullNote)}
<button on:click={toggleEditMode} class="btn-primary rounded-full">
{isEditing ? "Preview" : "Edit"}
</button>
{:else}
<button disabled class="btn-primary cursor-not-allowed rounded-full opacity-40">
Edit
</button>
{/if}
<!-- Version history dropdown menu -->
<div class="relative">
<button
on:click={toggleVersionDropdown}
class="btn-secondary flex items-center rounded-full"
>
<span>Versions</span>
<VersionArrow />
</button>
{#if showVersionsDropdown && $versionHistory && $versionHistory.length > 0}
<div class="versions-dropdown">
{#each $versionHistory as version}
<button
on:click={() => selectVersion(version.versionID)}
class="versions-dropdown-item {$currentFullNote.versionNumber ===
version.versionNumber
? 'versions-dropdown-item-active'
: ''}"
>
<p class="versions-dropdown-item-text truncate font-bold">{version.title}</p>
<span class="versions-dropdown-item-text"
>{version.createdAt.toLocaleString()}</span
>
{#if version.isActive}<span class="versions-dropdown-item-text ml-2 opacity-70"
>(active)</span
>{/if}
</button>
{/each}
</div>
<div class="flex items-center space-x-4 pl-2">
{#if $currentFullNote}
<!-- Note edit/preview button (only shown for latest version) -->
{#if isLatestVersion($currentFullNote)}
<button on:click={toggleEditMode} class="btn-primary rounded-full">
{isEditing ? "Preview" : "Edit"}
</button>
{:else}
<button disabled class="btn-primary cursor-not-allowed rounded-full opacity-40">
Edit
</button>
{/if}
<!-- Version history dropdown menu -->
<div class="relative">
<button
on:click={toggleVersionDropdown}
class="btn-secondary flex items-center rounded-full"
>
<span>Versions</span>
<VersionArrow />
</button>
{#if showVersionsDropdown && $versionHistory && $versionHistory.length > 0}
<div class="versions-dropdown">
{#each $versionHistory as version}
<button
on:click={() => selectVersion(version.versionID)}
class="versions-dropdown-item {$currentFullNote.versionNumber ===
version.versionNumber
? 'versions-dropdown-item-active'
: ''}"
>
<p class="versions-dropdown-item-text truncate font-bold">{version.title}</p>
<span class="versions-dropdown-item-text"
>{version.createdAt.toLocaleString()}</span
>
{#if version.isActive}<span
class="versions-dropdown-item-text ml-2 opacity-70">(active)</span
>{/if}
</button>
{/each}
</div>
{/if}
</div>
{/if}
</div>
<div class="flex items-center space-x-4 pl-2">
<ThemeToggle />
</div>
</header>
<!-- Note content area -->
<main class="main-content">
{#if $currentFullNote}
<NoteEditor note={$currentFullNote} bind:isEditing {saveNote} />
{:else}
<div class="flex h-full flex-col items-center justify-center">
<p class="mb-4 text-lg">None selected</p>
<button on:click={createNewNote} class="btn-primary">Create note</button>
</div>
{/if}
</div>
</main>
</div>
<div class="flex items-center space-x-4 pl-2">
<ThemeToggle />
</div>
</header>
<!-- Note content area -->
<main class="main-content">
{#if $currentFullNote}
<NoteEditor note={$currentFullNote} bind:isEditing {saveNote} />
{:else}
<div class="flex h-full flex-col items-center justify-center">
<p class="mb-4 text-lg">None selected</p>
<button on:click={createNewNote} class="btn-primary">Create note</button>
</div>
{/if}
</main>
<!-- Settings Modal -->
{#if showSettings}
<SettingsModal onClose={toggleSettings} />
{/if}
</div>
<!-- Settings Modal -->
{#if showSettings}
<SettingsModal onClose={toggleSettings} />
{/if}
</div>
{/if}

View File

@ -10,25 +10,6 @@
})
</script>
<svelte:head>
<script lang="ts">
if (document) {
let darkMode = false
const savedTheme = localStorage.getItem("darkMode")
if (savedTheme !== null) {
darkMode = savedTheme === "true"
} else {
// Fallback to system preference
darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches
localStorage.setItem("darkMode", darkMode ? "true" : "false")
}
document.documentElement.classList.toggle("dark", darkMode)
}
</script>
</svelte:head>
<button
on:click={() => {
themeState.isDarkMode = !themeState.isDarkMode

View File

@ -1,34 +1,21 @@
<script>
import NoteView from "$lib/components/NoteView.svelte"
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"
import NoteView from "$lib/components/NoteView.svelte"
import { onMount } from "svelte"
import { fade } from "svelte/transition"
let isLoading = true
onMount(() => {
// Set a small timeout to ensure the loading state shows
// -> Prevents component flickering during auth checks
// `NoteView` requires a small timeout as it utilizes `window` during init, additionally
// especially in prod. we need to give some time for the auth check request
setTimeout(() => {
isLoading = false
}, 500)
}, 1000)
})
</script>
{#if isLoading}
<div class="loading-container" transition:fade={{ duration: 200 }}>
<LoadingSpinner />
</div>
<LoadingSpinner />
{:else}
<NoteView />
{/if}
<style>
.loading-container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100%;
}
</style>