feat/fix: head theme setter & greet messages (broken sidebar)

This commit is contained in:
ae 2025-04-27 19:34:14 +03:00
parent 9e9a77f53a
commit b2f7533d88
Signed by: ae
GPG Key ID: 995EFD5C1B532B3E
10 changed files with 295 additions and 65 deletions

View File

@ -1,5 +1,15 @@
@import "tailwindcss"; @import "tailwindcss";
@theme {
--font-copernicus: "Copernicus", "sans-serif";
}
@font-face {
font-family: "Copernicus";
font-style: normal;
src: url("/fonts/Copernicus-Regular-Latin.woff2") format("woff2");
}
:root { :root {
--light-background: #f5f5f5; --light-background: #f5f5f5;
--light-foreground: #e0e0e0; --light-foreground: #e0e0e0;
@ -54,11 +64,7 @@
} }
a { a {
@apply text-[var(--light-accent)] transition-colors duration-200; @apply text-[var(--light-accent)] transition-colors duration-200 hover:underline;
}
a:hover {
@apply underline;
} }
.dark a { .dark a {
@ -88,31 +94,11 @@
} }
button { button {
@apply cursor-pointer rounded-md bg-[var(--light-accent)] px-4 py-2 text-[var(--light-background)] transition-all duration-200; @apply cursor-pointer rounded-md bg-[var(--light-accent)] px-4 py-2 text-[var(--light-background)] transition-all duration-200 hover:bg-[var(--light-accent)]/80 focus:ring-2 focus:ring-[var(--light-accent)]/50 focus:outline-none disabled:opacity-50;
}
button:hover {
@apply bg-[var(--light-accent)]/80;
}
button:focus {
@apply ring-2 ring-[var(--light-accent)]/50 outline-none;
}
button:disabled {
@apply opacity-50;
} }
.dark button { .dark button {
@apply bg-[var(--dark-accent)] text-[var(--dark-background)]; @apply bg-[var(--dark-accent)] text-[var(--dark-background)] hover:bg-[var(--dark-accent)]/80 focus:ring-[var(--dark-accent)]/50;
}
.dark button:hover {
@apply bg-[var(--dark-accent)]/80;
}
.dark button:focus {
@apply ring-[var(--dark-accent)]/50;
} }
} }
@ -157,7 +143,7 @@
} }
.btn-primary { .btn-primary {
@apply bg-[var(--light-accent)] text-[var(--light-background)]; @apply rounded-lg bg-[var(--light-accent)] text-[var(--light-background)];
} }
.dark .btn-primary { .dark .btn-primary {
@ -165,7 +151,7 @@
} }
.btn-secondary { .btn-secondary {
@apply border border-[var(--light-text)]/20 bg-[var(--light-foreground)] text-[var(--light-text)]; @apply rounded-lg border border-[var(--light-text)]/20 bg-[var(--light-foreground)] text-[var(--light-text)];
} }
.dark .btn-secondary { .dark .btn-secondary {
@ -201,14 +187,10 @@
@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; /* Height should align with the navbar height for visual consistency */
@apply flex h-20 items-center justify-center border-b border-[var(--light-text)]/20 p-2;
} }
.dark .sidebar-header, .dark .sidebar-header,
@ -216,10 +198,6 @@
@apply border-[var(--dark-text)]/20; @apply border-[var(--dark-text)]/20;
} }
.sidebar-header {
@apply border-b;
}
.sidebar-footer { .sidebar-footer {
@apply border-t; @apply border-t;
} }
@ -273,11 +251,11 @@
} }
.search-bar { .search-bar {
@apply w-full rounded-md py-2 pr-3 pl-9; @apply w-full rounded-lg py-2.5 pr-4 pl-11;
} }
.search-bar-icon { .search-bar-icon {
@apply absolute top-3 left-7 h-4 w-4 text-[var(--light-text)]/60; @apply absolute top-3 left-8 h-5 w-5 text-[var(--light-text)]/60;
} }
.dark .search-bar-icon { .dark .search-bar-icon {
@ -641,4 +619,20 @@
.dark .main-content { .dark .main-content {
@apply bg-[var(--dark-background)]; @apply bg-[var(--dark-background)];
} }
.greeting-container {
@apply flex h-full flex-col items-center justify-center;
}
.greeting-message {
@apply font-copernicus mb-4 text-center text-4xl;
}
.greeting-bottom-link {
@apply cursor-pointer text-[var(--light-accent)] transition-colors duration-200 hover:underline;
}
.greeting-bottom-link {
@apply text-[var(--dark-accent)];
}
} }

View File

@ -17,6 +17,8 @@
if (darkMode) { if (darkMode) {
document.documentElement.classList.add("dark") document.documentElement.classList.add("dark")
} }
localStorage.setItem("darkMode", darkMode)
</script> </script>
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View File

@ -45,40 +45,40 @@
} }
</script> </script>
<div class="flex min-h-screen items-center justify-center px-4"> <div class="flex min-h-screen items-center justify-center">
<div class="card grid w-auto max-w-md justify-items-center space-y-6 rounded-3xl"> <div class="card rounded-4x1 grid w-[16rem] justify-items-center space-y-6 p-6">
{#if $cError} {#if $cError}
<div class="error"> <div class="error max-w-full overflow-hidden text-ellipsis text-center">
{$cError} {$cError}
</div> </div>
{/if} {/if}
<form class="space-y-5" on:submit|preventDefault={handleSubmit}> <form class="space-y-5" on:submit|preventDefault={handleSubmit}>
<div class="form-group"> <div class="form-group">
<label for="username" class="form-label"> Username </label>
<input <input
type="text" type="text"
id="username" id="username"
bind:value={username} bind:value={username}
placeholder="Username"
required required
autocomplete="username" autocomplete="username"
class="w-auto self-center" class="w-full"
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password" class="form-label"> Password </label>
<input <input
type="password" type="password"
id="password" id="password"
bind:value={password} bind:value={password}
placeholder="Password"
required required
autocomplete={formName === "Login" ? "current-password" : "new-password"} autocomplete={formName === "Login" ? "current-password" : "new-password"}
class="w-full" class="w-full"
/> />
</div> </div>
<button type="submit" class="btn-primary w-full rounded-full"> <button type="submit" class="btn-primary w-full rounded-lg">
{formName} {formName}
</button> </button>
</form> </form>
@ -90,6 +90,8 @@
</div> </div>
</div> </div>
<!-- TODO: add footer with a random quote (font-copernicus) -->
<div class="absolute right-4 top-4"> <div class="absolute right-4 top-4">
<ThemeToggle /> <ThemeToggle />
</div> </div>

View File

@ -19,6 +19,8 @@
import VersionArrow from "$lib/icons/VersionArrow.svelte" import VersionArrow from "$lib/icons/VersionArrow.svelte"
import Close from "$lib/icons/Close.svelte" import Close from "$lib/icons/Close.svelte"
import { ERR_NOTIFICATION_DUR, SUC_NOTIFICATION_DUR } from "$lib/const" import { ERR_NOTIFICATION_DUR, SUC_NOTIFICATION_DUR } from "$lib/const"
import { generateGreeting } from "$lib/util/greetMessage"
import { get } from "svelte/store"
// State // State
let isComponentReady = false let isComponentReady = false
@ -28,6 +30,7 @@
let isEditing = false let isEditing = false
let errorTimeout: ReturnType<typeof setTimeout> | null = null let errorTimeout: ReturnType<typeof setTimeout> | null = null
let successTimeout: ReturnType<typeof setTimeout> | null = null let successTimeout: ReturnType<typeof setTimeout> | null = null
let greetMessage = "What's up?"
onMount(async (): Promise<any> => { onMount(async (): Promise<any> => {
// The following fetch attempts to refresh any expired tokens automatically // The following fetch attempts to refresh any expired tokens automatically
@ -41,6 +44,8 @@
} }
await loadNotes() await loadNotes()
const cUser = get(currentUser)
greetMessage = generateGreeting(cUser ? cUser.username : "friend")
// Default to sidebar closed on mobile // Default to sidebar closed on mobile
const handleResize = () => { const handleResize = () => {
@ -274,7 +279,7 @@
<header class="main-header"> <header class="main-header">
<button <button
on:click={toggleSidebar} on:click={toggleSidebar}
class="btn-secondary rounded-full p-2" class="btn-secondary rounded-lg p-2"
aria-label="Toggle sidebar" aria-label="Toggle sidebar"
> >
<ToggleSidebar /> <ToggleSidebar />
@ -339,9 +344,22 @@
{#if $currentFullNote} {#if $currentFullNote}
<NoteEditor note={$currentFullNote} bind:isEditing {saveNote} /> <NoteEditor note={$currentFullNote} bind:isEditing {saveNote} />
{:else} {:else}
<div class="flex h-full flex-col items-center justify-center"> <div class="greeting-container">
<p class="mb-4 text-lg">None selected</p> <p class="greeting-message">{greetMessage}</p>
<button on:click={createNewNote} class="btn-primary">Create note</button> <p class="greeting-message text-base">
Want to create a new note?<span class="hidden sm:inline"> </span>
<span class="block whitespace-nowrap sm:inline"
>Click
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="greeting-bottom-link"
on:click={createNewNote}
aria-roledescription="create-note-text-link">here</span
></span
>
<!-- TODO: italic & grayed out text new note creation keyboard shortcut -->
</p>
</div> </div>
{/if} {/if}
</main> </main>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { NoteMetadata, FullNote } from "$lib/client" import type { NoteMetadata, FullNote } from "$lib/client"
import Close from "$lib/icons/Close.svelte" import Close from "$lib/icons/Close.svelte"
import CreateNew from "$lib/icons/CreateNew.svelte" import Create from "$lib/icons/Create.svelte"
import Delete from "$lib/icons/Delete.svelte" import Delete from "$lib/icons/Delete.svelte"
import Logout from "$lib/icons/Logout.svelte" import Logout from "$lib/icons/Logout.svelte"
import Search from "$lib/icons/Search.svelte" import Search from "$lib/icons/Search.svelte"
@ -71,26 +71,26 @@
> >
<!-- Sidebar header --> <!-- Sidebar header -->
<div class="sidebar-header"> <div class="sidebar-header">
<div class="flex items-center justify-between"> <div class="mx-2 flex items-center justify-center">
<button <button
on:click={createNewNote} on:click={createNewNote}
class="btn-primary rounded-full p-2" class="btn-secondary h-9 w-9 p-2 text-center"
aria-label="Create new note" aria-label="Create new note"
> >
<CreateNew /> <Create />
</button> </button>
<!-- Search bar --> <!-- Search bar -->
<div class="relative pl-4"> <div class="pl-4.5 relative">
<input type="text" placeholder="Search" bind:value={searchQuery} class="search-bar" /> <input type="text" placeholder="Search" bind:value={searchQuery} class="search-bar" />
<Search /> <Search />
</div> </div>
<!-- Close button visible only on mobile --> <!-- Close button visible only on mobile -->
<div class="relative pl-4"> <div class="pl-4.5 relative">
<button <button
on:click={closeSidebar} on:click={closeSidebar}
class="btn-secondary h-9 w-9 rounded-full p-2 md:hidden" class="btn-secondary h-9 w-9 p-2 text-center md:hidden"
aria-label="Close sidebar" aria-label="Close sidebar"
> >
<Close /> <Close />
@ -149,7 +149,6 @@
</button> </button>
<!-- It's better for UX that the logout button isn't on the right side --> <!-- It's better for UX that the logout button isn't on the right side -->
<button <button
on:click={toggleSettings} on:click={toggleSettings}
class="btn-secondary rounded-full p-2" class="btn-secondary rounded-full p-2"

View File

@ -4,7 +4,6 @@
import { themeState } from "$lib/state.svelte" import { themeState } from "$lib/state.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
// The `themeState` rune can't be imported here so we must explicitly call the setup
onMount(() => { onMount(() => {
themeState.isDarkMode = localStorage.getItem("darkMode") === "true" themeState.isDarkMode = localStorage.getItem("darkMode") === "true"
}) })
@ -16,7 +15,7 @@
document.documentElement.classList.toggle("dark", themeState.isDarkMode) document.documentElement.classList.toggle("dark", themeState.isDarkMode)
localStorage.setItem("darkMode", themeState.isDarkMode ? "true" : "false") localStorage.setItem("darkMode", themeState.isDarkMode ? "true" : "false")
}} }}
class="btn-secondary rounded-full p-2" class="btn-secondary p-2"
aria-label={themeState.isDarkMode ? "Switch to light mode" : "Switch to dark mode"} aria-label={themeState.isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
> >
{#if themeState.isDarkMode} {#if themeState.isDarkMode}

View File

@ -1,5 +1,4 @@
// export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api" export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
export const API_BASE_ADDR = "/api"
// Lifetimes of *in-memory* authentication tokens in milliseconds // Lifetimes of *in-memory* authentication tokens in milliseconds
export const AT_EXP_MS = 15 * 60 * 1000 // 15 min. export const AT_EXP_MS = 15 * 60 * 1000 // 15 min.

View File

@ -0,0 +1,217 @@
interface HolidayMap {
[month: number]: {
[day: number]: {
name: string // Metadata for debugging
greetings: string[]
}
}
}
export const generateGreeting = (username: string, date: Date = new Date()): string => {
const hGreeting = getHolidayGreeting(username, date)
if (hGreeting) {
const [holidayName, holidayGreeting] = hGreeting
console.log(`[UTIL] Holiday: ${holidayName}`)
return holidayGreeting
}
return getTimeBasedGreeting(username, date)
}
const getHolidayGreeting = (username: string, date: Date): [string, string] | null => {
const holidays: HolidayMap = {
1: {
1: {
name: "New Year",
greetings: [
`Happy New Year, ${username}!`,
`Welcome to a fresh start, ${username}!`,
`Here's to new beginnings, ${username}!`
]
}
},
2: {
14: {
name: "Valentine's Day",
greetings: [
`Happy Valentine's Day, ${username}!`,
`Love is in the air, ${username}!`,
`Wishing you a lovely day, ${username}!`
]
}
},
10: {
31: {
name: "Halloween",
greetings: [
`Happy Halloween, ${username}!`,
`Spooky greetings, ${username}!`,
`Trick or treat, ${username}?`
]
}
},
12: {
25: {
name: "Christmas",
greetings: [
`Merry Christmas, ${username}!`,
`Happy holidays, ${username}!`,
`Season's greetings, ${username}!`
]
},
31: {
name: "New Year's Eve",
greetings: [
`Happy New Year's Eve, ${username}!`,
`Ready to ring in the new year, ${username}?`,
`Goodbye to this year, ${username}!`
]
}
}
}
const currentYear = date.getFullYear()
const currentMonth = date.getMonth() + 1 // 0-indexed by default (lol)
const currentDay = date.getDate()
const [easterDay, easterMonth] = gaussEaster(currentYear)
if (currentDay === easterDay && currentMonth === easterMonth) {
return [
"Easter",
getRandomGreeting([
`Happy Easter, ${username}!`,
`Easter blessings, ${username}!`,
`Hoppy Easter, ${username}!`
])
]
}
if (currentMonth === 6) {
const midsummerDay = calculateMidsummer(currentYear)
if (currentDay === midsummerDay) {
return [
"Midsummer",
getRandomGreeting([
`Happy Midsummer, ${username}!`,
`Enjoying the longest days, ${username}?`,
`Summer solstice greetings, ${username}!`,
`Glad Midsommar, ${username}!`,
`Hyvää Juhannusta, ${username}!`
])
]
}
}
const holidayData = holidays[currentMonth]?.[currentDay]
if (holidayData) {
return [holidayData.name, getRandomGreeting(holidayData.greetings)]
}
return null
}
const calculateMidsummer = (year: number): number => {
// Saturday between 20th and 26th of June
const startDate = new Date(year, 5, 20)
const dayOfWeek = startDate.getDay()
const daysUntilSaturday = dayOfWeek === 6 ? 0 : (6 - dayOfWeek + 7) % 7
const resultDate = 20 + daysUntilSaturday
if (resultDate > 26) {
return resultDate - 7
}
return resultDate
}
const gaussEaster = (year: number): [day: number, month: number] => {
// Directly from Gauss's Easter algorithm
const a = year % 19
const b = year % 4
const c = year % 7
const p = Math.floor(year / 100.0)
const q = Math.floor((13 + 8 * p) / 25.0)
const m = Math.floor(15 - q + p - (Math.floor(p / 4) % 30))
const n = Math.floor(4 + p - Math.floor(p / 4)) % 7
const d = Math.floor(19 * a + m) % 30
const e = Math.floor(2 * b + 4 * c + 6 * d + n) % 7
const days = Math.floor(22 + d + e)
if (d === 29 && e === 6) {
return [19, 4]
} else if (d === 28 && e === 6) {
return [18, 4]
}
if (days > 31) {
// Jump to April
return [days - 31, 4]
}
return [days, 3]
}
const getTimeBasedGreeting = (username: string, date: Date): string => {
const hour = date.getHours()
// Early morning
if (0 <= hour && hour < 5) {
return getRandomGreeting([
`Up late, ${username}?`,
`Burning the midnight oil, ${username}?`,
`Hello night owl, ${username}!`,
`The stars are beautiful tonight, ${username}.`
])
}
// Morning
if (5 <= hour && hour < 12) {
return getRandomGreeting([
`Good morning, ${username}!`,
`Rise and shine, ${username}!`,
`Top of the morning to you, ${username}!`,
`Have a wonderful morning, ${username}!`
])
}
// Afternoon
if (12 <= hour && hour < 17) {
return getRandomGreeting([
`Good afternoon, ${username}!`,
`Hello there, ${username}!`,
`Hope your day is going well, ${username}!`,
`Afternoon greetings, ${username}!`
])
}
// Evening
if (17 <= hour && hour < 21) {
return getRandomGreeting([
`Good evening, ${username}!`,
`Hope you had a nice day, ${username}!`,
`Evening greetings, ${username}!`,
`Winding down for the day, ${username}?`
])
}
// Night
return getRandomGreeting([
`Good night, ${username}!`,
`Having a pleasant evening, ${username}?`,
`How was your day, ${username}?`,
`Greetings at this fine hour, ${username}!`
])
}
const getRandomGreeting = (greetings: string[]): string => {
const index = Math.floor(Math.random() * greetings.length)
return greetings[index]
}

View File

@ -26,7 +26,7 @@ export const isPasswordValid = (password: string): [boolean, string] => {
return [true, ""] return [true, ""]
} }
const calculateEntropy = (password: string) => { const calculateEntropy = (password: string): number => {
let poolSize = 0 let poolSize = 0
for (const [eClass, poolPlus] of ENTROPY_CLASSES) { for (const [eClass, poolPlus] of ENTROPY_CLASSES) {

Binary file not shown.