Compare commits

..

No commits in common. "8f9f5c76cb81f8d5bf19c5dad76611d5f9f7c17c" and "2b1ce51d319857239727977244c002d1dc9cb68a" have entirely different histories.

11 changed files with 155 additions and 269 deletions

View File

@ -15,7 +15,3 @@ APP_ENV="production"
DOMAIN="" DOMAIN=""
FRONTEND_URL="" FRONTEND_URL=""
# Frontend
VITE_VIEW_COOKIE_PATH="/"
VITE_VIEW_COOKIE_DOMAIN=$DOMAIN
VITE_COKOIE_SAME_SITE="strict"

View File

@ -5,19 +5,11 @@
--light-foreground: #e0e0e0; --light-foreground: #e0e0e0;
--light-accent: #303052; --light-accent: #303052;
--light-text: rgb(34, 40, 49); --light-text: rgb(34, 40, 49);
--light-error-text: #4b0000;
--light-error-background: #fcaaaae1;
--light-success-text: #004b0a;
--light-success-background: #adfcaae1;
--dark-background: #181818; --dark-background: #181818;
--dark-foreground: #222222; --dark-foreground: #222222;
--dark-accent: #bebff7; --dark-accent: #bebff7;
--dark-text: rgb(238, 238, 238); --dark-text: rgb(238, 238, 238);
--dark-error-text: #fcadaa;
--dark-error-background: #4b0000e0;
--dark-success-text: #adfcaa;
--dark-success-background: #004b0ae0;
} }
/* Scrollbar width */ /* Scrollbar width */
@ -87,7 +79,7 @@
} }
button { button {
@apply cursor-pointer rounded-md bg-[var(--light-accent)] px-4 py-2 text-[var(--light-background)] transition-all duration-200; @apply rounded-md bg-[var(--light-accent)] px-4 py-2 text-[var(--light-background)] transition-all duration-200;
} }
button:hover { button:hover {
@ -139,20 +131,20 @@
/* Error messages */ /* Error messages */
.error { .error {
@apply flex items-center justify-between rounded-lg bg-[var(--light-error-background)] p-3 text-sm text-[var(--light-error-text)]; @apply rounded-lg bg-red-100 p-3 text-sm text-red-500;
} }
.dark .error { .dark .error {
@apply bg-[var(--dark-error-background)] text-[var(--dark-error-text)]; @apply bg-red-900/30 text-red-300;
} }
/* Success messages */ /* Success messages */
.success { .success {
@apply flex items-center justify-between rounded-lg bg-[var(--light-success-background)] p-3 text-sm text-[var(--light-success-text)]; @apply rounded-lg bg-green-100 p-3 text-sm text-green-500;
} }
.dark .success { .dark .success {
@apply bg-[var(--dark-success-background)] text-[var(--dark-success-text)]; @apply bg-green-900/30 text-green-300;
} }
.btn-primary { .btn-primary {
@ -193,18 +185,13 @@
} }
.sidebar { .sidebar {
@apply fixed z-10 flex h-full flex-col overflow-hidden border-r border-[var(--light-foreground)] bg-[var(--light-foreground)] transition-all duration-300; @apply fixed flex h-full flex-col overflow-hidden border-r border-[var(--light-foreground)] bg-[var(--light-foreground)] transition-all duration-300;
} }
.dark .sidebar { .dark .sidebar {
@apply border-[var(--dark-foreground)] bg-[var(--dark-foreground)]; @apply border-[var(--dark-foreground)] bg-[var(--dark-foreground)];
} }
.sidebar-header {
/* Should align with the navbar height for visual consistency */
@apply h-20;
}
.sidebar-header, .sidebar-header,
.sidebar-footer { .sidebar-footer {
@apply border-[var(--light-text)]/20 p-4; @apply border-[var(--light-text)]/20 p-4;
@ -256,11 +243,27 @@
} }
.sidebar-item-delete { .sidebar-item-delete {
@apply flex h-6.5 w-6.5 items-center justify-center rounded-lg border-1 border-[var(--light-accent)]/20 bg-transparent p-1 text-[var(--light-accent)]/50 hover:text-[var(--light-accent)]; @apply flex h-6.5 w-6.5 items-center justify-center rounded-lg border-1 border-[var(--light-accent)]/20 bg-transparent p-1;
} }
.dark .sidebar-item-delete { .dark .sidebar-item-delete {
@apply border-[var(--dark-accent)]/20 text-[var(--dark-accent)]/50 hover:text-[var(--dark-accent)]; @apply border-[var(--dark-accent)]/20;
}
.sidebar-item-delete > svg {
@apply text-[var(--light-accent)]/50;
}
.dark .sidebar-item-delete > svg {
@apply text-[var(--dark-accent)]/50;
}
.sidebar-item-delete:hover > svg {
@apply text-[var(--light-accent)] transition-colors delay-100;
}
.dark .sidebar-item-delete:hover > svg {
@apply text-[var(--dark-accent)];
} }
.sidebar-divider { .sidebar-divider {
@ -301,7 +304,7 @@
} }
.versions-dropdown-item { .versions-dropdown-item {
@apply w-full bg-[var(--light-foreground)] p-3 transition-colors hover:bg-[var(--light-background)]; @apply w-full cursor-pointer bg-[var(--light-foreground)] p-3 transition-colors hover:bg-[var(--light-background)];
} }
.dark .versions-dropdown-item { .dark .versions-dropdown-item {
@ -333,10 +336,6 @@
} }
/* Note editor */ /* Note editor */
.note-title-container {
@apply mb-4;
}
.note-title-input { .note-title-input {
@apply w-full rounded-2xl border-b border-[var(--light-text)]/20 bg-transparent pb-2 text-2xl font-bold focus:border-[var(--light-accent)]; @apply w-full rounded-2xl border-b border-[var(--light-text)]/20 bg-transparent pb-2 text-2xl font-bold focus:border-[var(--light-accent)];
} }
@ -353,28 +352,16 @@
@apply text-[var(--dark-text)]/60; @apply text-[var(--dark-text)]/60;
} }
.note-editor-container {
@apply flex min-h-0 flex-1 flex-col overflow-auto;
}
.note-editor-wrapper {
@apply flex h-full flex-col;
}
.note-editor-content {
@apply flex h-full flex-col;
}
.note-textarea { .note-textarea {
@apply h-full max-h-full min-h-1/2 w-full resize-none rounded-2xl bg-transparent p-3.5 font-mono outline-none focus:border-4 focus:border-[var(--light-accent)]/60; @apply h-full max-h-full min-h-1/2 w-full resize-none rounded-2xl bg-transparent p-3.5 font-mono outline-none focus:border-4 focus:border-[var(--light-accent)]/60;
} }
.dark .note-textarea { .note-save-button {
@apply focus:border-[var(--dark-accent)]/60; @apply absolute right-10 bottom-10;
} }
.note-save-button { .dark .note-textarea {
@apply fixed right-10 bottom-10 z-10; @apply focus:border-[var(--dark-accent)]/60;
} }
/* Markdown preview */ /* Markdown preview */
@ -518,15 +505,15 @@
/* Settings modal */ /* Settings modal */
.modal-backdrop { .modal-backdrop {
@apply fixed inset-0 z-40 flex items-center justify-center backdrop-blur-xs; @apply fixed inset-0 z-50 flex items-center justify-center backdrop-blur-xs;
} }
.modal-content { .modal-content {
@apply mx-4 max-h-[90vh] w-full max-w-md overflow-y-auto rounded-lg border-2 border-[var(--light-accent)]/10 bg-[var(--light-background)] shadow-lg; @apply mx-4 max-h-[90vh] w-full max-w-md overflow-y-auto rounded-lg bg-[var(--light-background)] shadow-lg;
} }
.dark .modal-content { .dark .modal-content {
@apply border-[var(--dark-accent)]/10 bg-[var(--dark-background)]; @apply bg-[var(--dark-background)];
} }
.modal-section { .modal-section {
@ -538,11 +525,11 @@
} }
.modal-close-button { .modal-close-button {
@apply h-8 w-8 items-center justify-center rounded-md border-1 border-[var(--light-accent)]/20 bg-transparent p-1 text-[var(--light-accent)]/50 hover:text-[var(--light-accent)]; @apply text-[var(--light-background)]/60 hover:text-[var(--light-background)];
} }
.dark .modal-close-button { .dark .modal-close-button {
@apply border-[var(--dark-accent)]/20 text-[var(--dark-accent)]/50 hover:text-[var(--dark-accent)]; @apply text-[var(--dark-background)]/60 hover:text-[var(--dark-background)];
} }
/* Loading spinner */ /* Loading spinner */
@ -590,43 +577,19 @@
/* Main layout */ /* Main layout */
.main-layout-container { .main-layout-container {
@apply flex h-screen w-full bg-[var(--light-background)]; @apply flex h-screen bg-[var(--light-background)];
} }
.dark .main-layout-container { .dark .main-layout-container {
@apply bg-[var(--dark-background)]; @apply bg-[var(--dark-background)];
} }
.content-wrapper { .main-error-popup {
@apply flex h-screen flex-1 flex-col overflow-hidden;
}
.note-content-fixed-width {
@apply mx-auto flex w-full max-w-[800px] flex-col px-8 py-0;
height: calc(100% - 5rem);
}
@media (max-width: 768px) {
.note-content-fixed-width {
@apply max-w-full px-4 py-0;
}
}
.main-info-popup {
/* Z-value should be set so that this is on top of the modal's background blur */
@apply fixed top-4 right-4 z-50 max-w-md; @apply fixed top-4 right-4 z-50 max-w-md;
} }
.main-info-popup-button {
@apply ml-2 h-4 w-4 items-center justify-center rounded-md bg-transparent p-0 text-inherit;
}
.main-header { .main-header {
/* @apply flex items-center justify-between bg-[var(--light-foreground)] p-4 shadow-sm;
Should have higher z-value than the contents, but still less than the sidebar
(otherwise the navbar and the sidebar shadows will seem uneven).
*/
@apply z-5 flex h-20 items-center justify-between bg-[var(--light-foreground)] p-4 shadow-sm;
} }
.dark .main-header { .dark .main-header {
@ -634,7 +597,7 @@
} }
.main-content { .main-content {
@apply flex min-h-0 flex-1 flex-col overflow-auto bg-[var(--light-background)] p-6; @apply flex-1 overflow-auto bg-[var(--light-background)] p-6;
} }
.dark .main-content { .dark .main-content {

View File

@ -103,7 +103,6 @@ export const accessToken: Writable<string | null> = writable(null)
export const csrfToken: Writable<string | null> = writable(null) export const csrfToken: Writable<string | null> = writable(null)
export const isPending: Writable<boolean> = writable(false) export const isPending: Writable<boolean> = writable(false)
export const cError: Writable<string | null> = writable(null) export const cError: Writable<string | null> = writable(null)
export const cSuccess: Writable<string | null> = writable(null)
class ApiClient { class ApiClient {
private viewCookieName: string private viewCookieName: string
@ -123,11 +122,10 @@ class ApiClient {
private async handleRequest<T>( private async handleRequest<T>(
fn: () => Promise<T>, fn: () => Promise<T>,
options: { useBearerAuth: boolean; suspendGlobalErr: boolean } options: { useBearerAuth: boolean }
): Promise<T | null> { ): Promise<T | null> {
isPending.set(true) isPending.set(true)
cError.set(null) cError.set(null)
cSuccess.set(null)
// NOTE: If `handleResponse` is used, errors thrown from it will be caught here // NOTE: If `handleResponse` is used, errors thrown from it will be caught here
@ -147,17 +145,8 @@ class ApiClient {
return await fn() return await fn()
} catch (err) { } catch (err) {
const errMsg = err instanceof Error ? err.message : "Unknown error" cError.set(err instanceof Error ? err.message : "Unknown error")
console.log(`[ERR] ${get(cError)}`)
// The suspension option is handy when we want to display the error inside a modal instead of in a global notification
if (options.suspendGlobalErr) {
// Throw the same error to the next handler (should be handled inside the caller component)
throw new Error(errMsg)
} else {
cError.set(errMsg)
}
console.log(`[ERR] ${errMsg}`)
} finally { } finally {
isPending.set(false) isPending.set(false)
} }
@ -235,7 +224,7 @@ class ApiClient {
accessToken.set(newToken) accessToken.set(newToken)
this.lastAtUpdate = new Date() this.lastAtUpdate = new Date()
}, },
{ useBearerAuth: false, suspendGlobalErr: false } { useBearerAuth: false }
) )
} }
@ -269,7 +258,7 @@ class ApiClient {
csrfToken.set(newToken) csrfToken.set(newToken)
this.lastCsrfUpdate = new Date() this.lastCsrfUpdate = new Date()
}, },
{ useBearerAuth: false, suspendGlobalErr: false } { useBearerAuth: false }
) )
} }
@ -425,7 +414,7 @@ class ApiClient {
await goto("/login") await goto("/login")
}, },
{ useBearerAuth: false, suspendGlobalErr: false } { useBearerAuth: false }
) )
} }
@ -448,7 +437,7 @@ class ApiClient {
goto("/") goto("/")
}, },
{ useBearerAuth: false, suspendGlobalErr: false } { useBearerAuth: false }
) )
} }
@ -469,10 +458,10 @@ class ApiClient {
return return
} }
await this.handleResponse<void>(response, { useBearerAuth: true }) await this.handleResponse<void>(response, { useBearerAuth: false })
await this.handleLocalLogout() await this.handleLocalLogout()
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
@ -484,12 +473,12 @@ class ApiClient {
...this.getAuthHeader() ...this.getAuthHeader()
} }
}) })
const data = await this.handleResponse<ApiUserResponse>(response, { useBearerAuth: true }) const data = await this.handleResponse<ApiUserResponse>(response, { useBearerAuth: false })
const user = this.deserializeUser(data) const user = this.deserializeUser(data)
currentUser.set(user) currentUser.set(user)
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
@ -509,20 +498,19 @@ class ApiClient {
...this.getAuthHeader(), ...this.getAuthHeader(),
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
credentials: "include",
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
const { access_token: token, user } = await this.handleResponse<{ const { accessToken: token, user } = await this.handleResponse<{
access_token: string accessToken: string
user: User user: User
}>(response, { useBearerAuth: true }) }>(response, { useBearerAuth: false })
accessToken.set(token) accessToken.set(token)
currentUser.set(user || null) currentUser.set(user || null)
this.lastAtUpdate = new Date() this.lastAtUpdate = new Date()
}, },
{ useBearerAuth: true, suspendGlobalErr: true } // Error displayed inside the settings modal { useBearerAuth: true }
) )
} }
@ -544,10 +532,10 @@ class ApiClient {
return return
} }
await this.handleResponse<void>(response, { useBearerAuth: true }) await this.handleResponse<void>(response, { useBearerAuth: false })
await this.handleLocalLogout() await this.handleLocalLogout()
}, },
{ useBearerAuth: true, suspendGlobalErr: true } // Error displayed inside the settings modal { useBearerAuth: true }
) )
} }
@ -568,12 +556,12 @@ class ApiClient {
} }
}) })
const users = await this.handleResponse<User[]>(response, { useBearerAuth: true }) const users = await this.handleResponse<User[]>(response, { useBearerAuth: false })
console.log(`[ADMIN] Got ${users.length} user results`) console.log(`[ADMIN] Got ${users.length} user results`)
return users return users
}, },
{ useBearerAuth: true, suspendGlobalErr: true } // Error displayed inside the settings modal { useBearerAuth: true }
) )
} }
@ -601,9 +589,9 @@ class ApiClient {
return return
} }
await this.handleResponse<void>(response, { useBearerAuth: true }) await this.handleResponse<void>(response, { useBearerAuth: false })
}, },
{ useBearerAuth: true, suspendGlobalErr: true } // Error displayed inside the settings modal { useBearerAuth: true }
) )
} }
@ -621,7 +609,7 @@ class ApiClient {
let notes: NoteMetadata[] = [] let notes: NoteMetadata[] = []
let data = await this.handleResponse<ApiNoteMetadataResponse[]>(response, { let data = await this.handleResponse<ApiNoteMetadataResponse[]>(response, {
useBearerAuth: true useBearerAuth: false
}) })
if (data) { if (data) {
@ -632,7 +620,7 @@ class ApiClient {
return notes return notes
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
@ -648,9 +636,9 @@ class ApiClient {
...this.getAuthHeader() ...this.getAuthHeader()
} }
}) })
return await this.handleResponse<NewNoteResponse>(response, { useBearerAuth: true }) return await this.handleResponse<NewNoteResponse>(response, { useBearerAuth: false })
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
@ -677,7 +665,7 @@ class ApiClient {
}) })
const data = await this.handleResponse<ApiFullNoteResponse>(response, { const data = await this.handleResponse<ApiFullNoteResponse>(response, {
useBearerAuth: true useBearerAuth: false
}) })
const note = this.deserializeFullNote(data) const note = this.deserializeFullNote(data)
@ -687,7 +675,7 @@ class ApiClient {
return note return note
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
@ -710,9 +698,9 @@ class ApiClient {
return return
} }
await this.handleResponse<void>(response, { useBearerAuth: true }) await this.handleResponse<void>(response, { useBearerAuth: false })
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
@ -740,7 +728,7 @@ class ApiClient {
}) })
const data = await this.handleResponse<ApiVersionMetadataResponse[]>(response, { const data = await this.handleResponse<ApiVersionMetadataResponse[]>(response, {
useBearerAuth: true useBearerAuth: false
}) })
const versions = this.deserializeVersionMetadatas(data) const versions = this.deserializeVersionMetadatas(data)
@ -749,7 +737,7 @@ class ApiClient {
return versions return versions
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
@ -776,9 +764,9 @@ class ApiClient {
return return
} }
await this.handleResponse<void>(response, { useBearerAuth: true }) await this.handleResponse<void>(response, { useBearerAuth: false })
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
@ -793,9 +781,6 @@ class ApiClient {
// NOTE: No need to explicitly prevent attempting a cache hit as versions aren't editable // NOTE: No need to explicitly prevent attempting a cache hit as versions aren't editable
// TODO: If accessing the active version, don't attempt to hit the cache,
// but instead load the contents from the currently active full note
const cachedVersion = this.loadedVersionsCache.get(noteID + versionID) const cachedVersion = this.loadedVersionsCache.get(noteID + versionID)
if (cachedVersion != null) { if (cachedVersion != null) {
return cachedVersion return cachedVersion
@ -810,7 +795,7 @@ class ApiClient {
}) })
const data = await this.handleResponse<ApiFullVersionResponse>(response, { const data = await this.handleResponse<ApiFullVersionResponse>(response, {
useBearerAuth: true useBearerAuth: false
}) })
const version = this.joinDeserializedVersion(noteID, data) const version = this.joinDeserializedVersion(noteID, data)
@ -823,7 +808,7 @@ class ApiClient {
return version return version
}, },
{ useBearerAuth: true, suspendGlobalErr: false } { useBearerAuth: true }
) )
} }
} }

View File

@ -41,7 +41,11 @@
return return
} }
try {
await handler(username, password) await handler(username, password)
} catch (err) {
cError.set(err instanceof Error ? err.message : "Authentication failed")
}
} }
</script> </script>

View File

@ -122,9 +122,9 @@
} }
</script> </script>
<div class="note-editor-content"> <div class="flex h-full flex-col">
<!-- Note title --> <!-- Note title -->
<div class="note-title-container"> <div class="mb-4">
{#if isEditing} {#if isEditing}
<input <input
type="text" type="text"
@ -158,10 +158,10 @@
{/if} {/if}
</div> </div>
<!-- Note content (takes up remaining vertical space) --> <!-- Note content -->
<div class="note-editor-container"> <div class="flex-1 overflow-auto">
{#if isEditing} {#if isEditing}
<div class="note-editor-wrapper"> <div class="relative h-full">
<textarea <textarea
bind:this={textarea} bind:this={textarea}
bind:value={editableContent} bind:value={editableContent}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from "svelte" import { onMount } from "svelte"
import { goto } from "$app/navigation" import { goto } from "$app/navigation"
import ThemeToggle from "./ThemeToggle.svelte" import ThemeToggle from "./ThemeToggle.svelte"
import Sidebar from "./Sidebar.svelte" import Sidebar from "./Sidebar.svelte"
@ -12,13 +12,10 @@
availableNotes, availableNotes,
versionHistory, versionHistory,
cError, cError,
type FullNote, type FullNote
cSuccess
} from "$lib/client" } from "$lib/client"
import ToggleSidebar from "$lib/icons/ToggleSidebar.svelte" import ToggleSidebar from "$lib/icons/ToggleSidebar.svelte"
import VersionArrow from "$lib/icons/VersionArrow.svelte" import VersionArrow from "$lib/icons/VersionArrow.svelte"
import Close from "$lib/icons/Close.svelte"
import { ERR_NOTIFICATION_DUR, SUC_NOTIFICATION_DUR } from "$lib/const"
// State // State
let isComponentReady = false let isComponentReady = false
@ -26,21 +23,24 @@
let showSettings = false let showSettings = false
let showVersionsDropdown = false let showVersionsDropdown = false
let isEditing = false let isEditing = false
let errorTimeout: ReturnType<typeof setTimeout> | null = null
let successTimeout: ReturnType<typeof setTimeout> | null = null
onMount(async (): Promise<any> => { onMount(async (): Promise<any> => {
try {
// The following fetch attempts to refresh any expired tokens automatically // The following fetch attempts to refresh any expired tokens automatically
await apiClient.getCurrentUser() await apiClient.getCurrentUser()
// If still no current user after the fetch attempt, redirect to login // If still no current user after the fetch attempt, redirect to login
if (!$currentUser) { if (!$currentUser) {
console.log("[VIEW] No user data found, routing to login page") console.log("no user data found, routing to auth page")
goto("/login") goto("/login")
return return
} }
await loadNotes() await loadNotes()
} catch (error) {
console.error(`error during auth: ${error}`)
goto("/login")
}
// Default to sidebar closed on mobile // Default to sidebar closed on mobile
const handleResize = () => { const handleResize = () => {
@ -165,6 +165,7 @@
} }
const deleteNote = async (noteID: string) => { const deleteNote = async (noteID: string) => {
try {
await apiClient.deleteNote(noteID) await apiClient.deleteNote(noteID)
// If we're deleting the currently active note, clear the current note // If we're deleting the currently active note, clear the current note
@ -174,79 +175,24 @@
// Refresh the notes list due to updates being pushed to server // Refresh the notes list due to updates being pushed to server
await loadNotes() await loadNotes()
} catch (err) {
cSuccess.set("Note deleted successfully.") cError.set(`Failed to delete note: ${err instanceof Error ? err.message : "Unknown error"}`)
}
} }
const logout = async () => { const logout = async () => {
await apiClient.logout() await apiClient.logout()
} }
const errorUnsubscribe = cError.subscribe((value) => {
// Clear any existing timeout to avoid multiple timeouts
if (errorTimeout) {
clearTimeout(errorTimeout)
errorTimeout = null
}
if (value) {
errorTimeout = setTimeout(() => {
cError.set(null)
}, ERR_NOTIFICATION_DUR)
}
})
const successUnsubscribe = cSuccess.subscribe((value) => {
// Clear any existing timeout to avoid multiple timeouts
if (successTimeout) {
clearTimeout(successTimeout)
successTimeout = null
}
if (value) {
successTimeout = setTimeout(() => {
cSuccess.set(null)
}, SUC_NOTIFICATION_DUR)
}
})
onDestroy(() => {
errorUnsubscribe()
successUnsubscribe()
// Clear any pending timeouts
if (errorTimeout) {
clearTimeout(errorTimeout)
}
if (successTimeout) {
clearTimeout(successTimeout)
}
})
</script> </script>
{#if isComponentReady} {#if isComponentReady}
<div class="main-layout-container"> <div class="main-layout-container">
<!-- Error notification --> <!-- Error notification -->
{#if $cError} {#if $cError}
<div class="main-info-popup"> <div class="main-error-popup">
<div class="error"> <div class="error">
{$cError} {$cError}
<button class="main-info-popup-button" on:click={() => cError.set(null)}> <button class="ml-2 text-red-700" on:click={() => cError.set(null)}> × </button>
<Close />
</button>
</div>
</div>
{/if}
<!-- Success notification -->
{#if $cSuccess}
<div class="main-info-popup">
<div class="success">
{$cSuccess}
<button class="main-info-popup-button" on:click={() => cSuccess.set(null)}>
<Close />
</button>
</div> </div>
</div> </div>
{/if} {/if}
@ -264,9 +210,9 @@
on:close={closeSidebar} on:close={closeSidebar}
/> />
<!-- Main content area --> <!-- Main content -->
<div <div
class="content-wrapper transition-all duration-300" class="flex flex-1 flex-col overflow-hidden transition-all duration-300"
class:md:-ml-64={!sidebarOpen} class:md:-ml-64={!sidebarOpen}
class:md:ml-0={sidebarOpen} class:md:ml-0={sidebarOpen}
> >
@ -333,8 +279,7 @@
</div> </div>
</header> </header>
<!-- Note content area with fixed width --> <!-- Note content area -->
<div class="note-content-fixed-width">
<main class="main-content"> <main class="main-content">
{#if $currentFullNote} {#if $currentFullNote}
<NoteEditor note={$currentFullNote} bind:isEditing {saveNote} /> <NoteEditor note={$currentFullNote} bind:isEditing {saveNote} />
@ -346,7 +291,6 @@
{/if} {/if}
</main> </main>
</div> </div>
</div>
<!-- Settings Modal --> <!-- Settings Modal -->
{#if showSettings} {#if showSettings}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { apiClient, cSuccess } from "$lib/client" import { apiClient, cError } from "$lib/client"
import Close from "$lib/icons/Close.svelte" import Close from "$lib/icons/Close.svelte"
import { isPasswordValid } from "$lib/utils" import { isPasswordValid } from "$lib/utils"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -10,12 +10,11 @@
let currentPassword = "" let currentPassword = ""
let newPassword = "" let newPassword = ""
let confirmPassword = "" let confirmPassword = ""
let changePasswordModalError = "" let passwordError = ""
let isNewPasswordValid = true let isNewPasswordValid = true
let deleteConfirmPassword = "" let deleteConfirmPassword = ""
let deleteConfirmText = "" let deleteConfirmText = ""
let deleteUserModalError = ""
let modalContent: HTMLDivElement | null = null let modalContent: HTMLDivElement | null = null
let previouslyFocusedElement: HTMLElement | null = null let previouslyFocusedElement: HTMLElement | null = null
@ -46,24 +45,24 @@
const changePassword = async () => { const changePassword = async () => {
if (!currentPassword) { if (!currentPassword) {
changePasswordModalError = "Current password is required." passwordError = "Current password is required"
return return
} }
if (!newPassword) { if (!newPassword) {
changePasswordModalError = "New password is required." passwordError = "New password is required"
return return
} }
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
changePasswordModalError = "New passwords do not match." passwordError = "New passwords do not match"
return return
} }
;[isNewPasswordValid, changePasswordModalError] = isPasswordValid(newPassword) ;[isNewPasswordValid, passwordError] = isPasswordValid(newPassword)
if (!isNewPasswordValid) { if (!isNewPasswordValid) {
// Error will be automatically displayed inside corresponding modal section // Error will be automatically displayed
return return
} }
@ -73,32 +72,33 @@
newPassword = "" newPassword = ""
confirmPassword = "" confirmPassword = ""
cSuccess.set("Password updated successfully.") cError.set("Password updated successfully")
setTimeout(() => { setTimeout(() => {
cError.set(null)
onClose() onClose()
}, 100) }, 2000)
} catch (err) { } catch (error) {
changePasswordModalError = err instanceof Error ? err.message : "Unknown error." // Error handling is done by the API client
} }
} }
const deleteAccount = async () => { const deleteAccount = async () => {
if (!deleteConfirmPassword) { if (!deleteConfirmPassword) {
deleteUserModalError = "Password is required to delete your account." passwordError = "Password is required to delete your account"
return return
} }
if (deleteConfirmText !== "DELETE") { if (deleteConfirmText !== "DELETE") {
deleteUserModalError = "Please type DELETE to confirm account deletion." passwordError = "Please type DELETE to confirm account deletion"
return return
} }
try { try {
await apiClient.deleteCurrentUser(deleteConfirmPassword) await apiClient.deleteCurrentUser(deleteConfirmPassword)
// The API client will handle the redirect to login page after successful deletion // The API client will handle the redirect to login page after successful deletion
} catch (err) { } catch (error) {
deleteUserModalError = err instanceof Error ? err.message : "Unknown error." // Error handling is done by the API client
} }
} }
@ -142,7 +142,7 @@
role="document" role="document"
tabindex="-1" tabindex="-1"
> >
<!-- Header (title + close button) --> <!-- Header -->
<div class="modal-section flex items-center justify-between" role="heading" aria-level="1"> <div class="modal-section flex items-center justify-between" role="heading" aria-level="1">
<h2 class="text-xl font-bold">Settings</h2> <h2 class="text-xl font-bold">Settings</h2>
<button on:click={onClose} class="modal-close-button" aria-label="Close settings"> <button on:click={onClose} class="modal-close-button" aria-label="Close settings">
@ -154,9 +154,9 @@
<div class="modal-section"> <div class="modal-section">
<h3 class="mb-4 text-lg font-bold">Account Settings</h3> <h3 class="mb-4 text-lg font-bold">Account Settings</h3>
{#if changePasswordModalError} {#if passwordError}
<div class="error mb-4" role="alert"> <div class="error mb-4" role="alert">
{changePasswordModalError} {passwordError}
</div> </div>
{/if} {/if}
@ -181,22 +181,14 @@
<input type="password" id="confirmPassword" bind:value={confirmPassword} class="w-full" /> <input type="password" id="confirmPassword" bind:value={confirmPassword} class="w-full" />
</div> </div>
<button on:click={changePassword} class="btn-primary w-full rounded-full"> <button on:click={changePassword} class="btn-primary w-full"> Change Password </button>
Change Password
</button>
</div> </div>
</div> </div>
<!-- Danger zone --> <!-- Danger zone -->
<div class="modal-section"> <div class="p-4">
<h3 class="mb-4 text-lg font-bold text-red-500">Danger Zone</h3> <h3 class="mb-4 text-lg font-bold text-red-500">Danger Zone</h3>
{#if deleteUserModalError}
<div class="error mb-4" role="alert">
{deleteUserModalError}
</div>
{/if}
<div class="space-y-4"> <div class="space-y-4">
<p class="text-sm"> <p class="text-sm">
Deleting your account will permanently remove all your notes and account information. This Deleting your account will permanently remove all your notes and account information. This
@ -228,7 +220,7 @@
<button <button
on:click={deleteAccount} on:click={deleteAccount}
class="w-full rounded-full bg-red-500 font-bold text-white hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50" class="w-full rounded bg-red-500 px-4 py-2 font-bold text-white hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50"
disabled={deleteConfirmText !== "DELETE" || !deleteConfirmPassword} disabled={deleteConfirmText !== "DELETE" || !deleteConfirmPassword}
> >
Delete My Account Delete My Account

View File

@ -90,7 +90,7 @@
<div class="relative pl-4"> <div class="relative pl-4">
<button <button
on:click={closeSidebar} on:click={closeSidebar}
class="btn-secondary h-9 w-9 rounded-full p-2 md:hidden" class="btn-secondary rounded-full p-2 md:hidden"
aria-label="Close sidebar" aria-label="Close sidebar"
> >
<Close /> <Close />

View File

@ -30,7 +30,3 @@ export const VIEW_COOKIE_PATH = import.meta.env.VITE_VIEW_COOKIE_PATH || "/"
export const VIEW_COOKIE_DOMAIN = import.meta.env.VITE_VIEW_COOKIE_DOMAIN || "localhost" export const VIEW_COOKIE_DOMAIN = import.meta.env.VITE_VIEW_COOKIE_DOMAIN || "localhost"
export const COOKIE_SAME_SITE = import.meta.env.VITE_COOKIE_SAME_SITE || "strict" export const COOKIE_SAME_SITE = import.meta.env.VITE_COOKIE_SAME_SITE || "strict"
export const COOKIE_SECURE = import.meta.env.PROD ? true : false export const COOKIE_SECURE = import.meta.env.PROD ? true : false
// Error/success notification display durations
export const ERR_NOTIFICATION_DUR = 8 * 1000 // 8 s.
export const SUC_NOTIFICATION_DUR = 8 * 1000 // 8 s.

View File

@ -1,3 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 201 B

After

Width:  |  Height:  |  Size: 223 B

View File

@ -3,7 +3,7 @@
import { apiClient } from "$lib/client" import { apiClient } from "$lib/client"
const loginHandler = (username: string, password: string) => { const loginHandler = (username: string, password: string) => {
return apiClient.login(username, password) as Promise<void> return apiClient.login(username, password)
} }
</script> </script>