feat/fix: head theme setter & greet messages (broken sidebar)
This commit is contained in:
parent
9e9a77f53a
commit
b2f7533d88
@ -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)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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}
|
||||||
|
@ -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.
|
||||||
|
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, ""]
|
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) {
|
||||||
|
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