feat/fix: head theme setter & greet messages (broken sidebar)
This commit is contained in:
parent
9e9a77f53a
commit
b2f7533d88
@ -1,5 +1,15 @@
|
||||
@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 {
|
||||
--light-background: #f5f5f5;
|
||||
--light-foreground: #e0e0e0;
|
||||
@ -54,11 +64,7 @@
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-[var(--light-accent)] transition-colors duration-200;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
@apply underline;
|
||||
@apply text-[var(--light-accent)] transition-colors duration-200 hover:underline;
|
||||
}
|
||||
|
||||
.dark a {
|
||||
@ -88,31 +94,11 @@
|
||||
}
|
||||
|
||||
button {
|
||||
@apply cursor-pointer rounded-md bg-[var(--light-accent)] px-4 py-2 text-[var(--light-background)] transition-all duration-200;
|
||||
}
|
||||
|
||||
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;
|
||||
@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;
|
||||
}
|
||||
|
||||
.dark button {
|
||||
@apply bg-[var(--dark-accent)] text-[var(--dark-background)];
|
||||
}
|
||||
|
||||
.dark button:hover {
|
||||
@apply bg-[var(--dark-accent)]/80;
|
||||
}
|
||||
|
||||
.dark button:focus {
|
||||
@apply ring-[var(--dark-accent)]/50;
|
||||
@apply bg-[var(--dark-accent)] text-[var(--dark-background)] hover:bg-[var(--dark-accent)]/80 focus:ring-[var(--dark-accent)]/50;
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +143,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -165,7 +151,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -201,14 +187,10 @@
|
||||
@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-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,
|
||||
@ -216,10 +198,6 @@
|
||||
@apply border-[var(--dark-text)]/20;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
@apply border-b;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
@apply border-t;
|
||||
}
|
||||
@ -273,11 +251,11 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@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 {
|
||||
@ -641,4 +619,20 @@
|
||||
.dark .main-content {
|
||||
@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)];
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@
|
||||
if (darkMode) {
|
||||
document.documentElement.classList.add("dark")
|
||||
}
|
||||
|
||||
localStorage.setItem("darkMode", darkMode)
|
||||
</script>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
@ -45,40 +45,40 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen items-center justify-center px-4">
|
||||
<div class="card grid w-auto max-w-md justify-items-center space-y-6 rounded-3xl">
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="card rounded-4x1 grid w-[16rem] justify-items-center space-y-6 p-6">
|
||||
{#if $cError}
|
||||
<div class="error">
|
||||
<div class="error max-w-full overflow-hidden text-ellipsis text-center">
|
||||
{$cError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form class="space-y-5" on:submit|preventDefault={handleSubmit}>
|
||||
<div class="form-group">
|
||||
<label for="username" class="form-label"> Username </label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
bind:value={username}
|
||||
placeholder="Username"
|
||||
required
|
||||
autocomplete="username"
|
||||
class="w-auto self-center"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password" class="form-label"> Password </label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
bind:value={password}
|
||||
placeholder="Password"
|
||||
required
|
||||
autocomplete={formName === "Login" ? "current-password" : "new-password"}
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary w-full rounded-full">
|
||||
<button type="submit" class="btn-primary w-full rounded-lg">
|
||||
{formName}
|
||||
</button>
|
||||
</form>
|
||||
@ -90,6 +90,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: add footer with a random quote (font-copernicus) -->
|
||||
|
||||
<div class="absolute right-4 top-4">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
@ -19,6 +19,8 @@
|
||||
import VersionArrow from "$lib/icons/VersionArrow.svelte"
|
||||
import Close from "$lib/icons/Close.svelte"
|
||||
import { ERR_NOTIFICATION_DUR, SUC_NOTIFICATION_DUR } from "$lib/const"
|
||||
import { generateGreeting } from "$lib/util/greetMessage"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
// State
|
||||
let isComponentReady = false
|
||||
@ -28,6 +30,7 @@
|
||||
let isEditing = false
|
||||
let errorTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
let successTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
let greetMessage = "What's up?"
|
||||
|
||||
onMount(async (): Promise<any> => {
|
||||
// The following fetch attempts to refresh any expired tokens automatically
|
||||
@ -41,6 +44,8 @@
|
||||
}
|
||||
|
||||
await loadNotes()
|
||||
const cUser = get(currentUser)
|
||||
greetMessage = generateGreeting(cUser ? cUser.username : "friend")
|
||||
|
||||
// Default to sidebar closed on mobile
|
||||
const handleResize = () => {
|
||||
@ -274,7 +279,7 @@
|
||||
<header class="main-header">
|
||||
<button
|
||||
on:click={toggleSidebar}
|
||||
class="btn-secondary rounded-full p-2"
|
||||
class="btn-secondary rounded-lg p-2"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<ToggleSidebar />
|
||||
@ -339,9 +344,22 @@
|
||||
{#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 class="greeting-container">
|
||||
<p class="greeting-message">{greetMessage}</p>
|
||||
<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>
|
||||
{/if}
|
||||
</main>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { NoteMetadata, FullNote } from "$lib/client"
|
||||
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 Logout from "$lib/icons/Logout.svelte"
|
||||
import Search from "$lib/icons/Search.svelte"
|
||||
@ -71,26 +71,26 @@
|
||||
>
|
||||
<!-- Sidebar header -->
|
||||
<div class="sidebar-header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="mx-2 flex items-center justify-center">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<CreateNew />
|
||||
<Create />
|
||||
</button>
|
||||
|
||||
<!-- Search bar -->
|
||||
<div class="relative pl-4">
|
||||
<div class="pl-4.5 relative">
|
||||
<input type="text" placeholder="Search" bind:value={searchQuery} class="search-bar" />
|
||||
<Search />
|
||||
</div>
|
||||
|
||||
<!-- Close button visible only on mobile -->
|
||||
<div class="relative pl-4">
|
||||
<div class="pl-4.5 relative">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Close />
|
||||
@ -149,7 +149,6 @@
|
||||
</button>
|
||||
|
||||
<!-- It's better for UX that the logout button isn't on the right side -->
|
||||
|
||||
<button
|
||||
on:click={toggleSettings}
|
||||
class="btn-secondary rounded-full p-2"
|
||||
|
@ -4,7 +4,6 @@
|
||||
import { themeState } from "$lib/state.svelte"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
// The `themeState` rune can't be imported here so we must explicitly call the setup
|
||||
onMount(() => {
|
||||
themeState.isDarkMode = localStorage.getItem("darkMode") === "true"
|
||||
})
|
||||
@ -16,7 +15,7 @@
|
||||
document.documentElement.classList.toggle("dark", themeState.isDarkMode)
|
||||
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"}
|
||||
>
|
||||
{#if themeState.isDarkMode}
|
||||
|
@ -1,5 +1,4 @@
|
||||
// export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
||||
export const API_BASE_ADDR = "/api"
|
||||
export const API_BASE_ADDR = import.meta.env.PROD ? "/api" : "http://localhost:8080/api"
|
||||
|
||||
// Lifetimes of *in-memory* authentication tokens in milliseconds
|
||||
export const AT_EXP_MS = 15 * 60 * 1000 // 15 min.
|
||||
|
217
web/src/lib/util/greetMessage.ts
Normal file
217
web/src/lib/util/greetMessage.ts
Normal 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]
|
||||
}
|
@ -26,7 +26,7 @@ export const isPasswordValid = (password: string): [boolean, string] => {
|
||||
return [true, ""]
|
||||
}
|
||||
|
||||
const calculateEntropy = (password: string) => {
|
||||
const calculateEntropy = (password: string): number => {
|
||||
let poolSize = 0
|
||||
|
||||
for (const [eClass, poolPlus] of ENTROPY_CLASSES) {
|
||||
|
BIN
web/static/fonts/Copernicus-Regular-Latin.woff2
Normal file
BIN
web/static/fonts/Copernicus-Regular-Latin.woff2
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user