Compare commits
No commits in common. "b2f7533d886a65334fc96c3221c85ceaf97ecd5e" and "06a235315336cd181640c1ecb5454d01ef66c262" have entirely different histories.
b2f7533d88
...
06a2353153
@ -17,4 +17,4 @@ FRONTEND_URL=""
|
|||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
VITE_VIEW_COOKIE_PATH="/"
|
VITE_VIEW_COOKIE_PATH="/"
|
||||||
VITE_COOKIE_SAME_SITE="strict"
|
VITE_COKOIE_SAME_SITE="strict"
|
||||||
|
@ -1,15 +1,5 @@
|
|||||||
@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;
|
||||||
@ -64,7 +54,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@apply text-[var(--light-accent)] transition-colors duration-200 hover:underline;
|
@apply text-[var(--light-accent)] transition-colors duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
@apply underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark a {
|
.dark a {
|
||||||
@ -94,11 +88,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@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;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark button {
|
.dark button {
|
||||||
@apply bg-[var(--dark-accent)] text-[var(--dark-background)] hover:bg-[var(--dark-accent)]/80 focus:ring-[var(--dark-accent)]/50;
|
@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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +157,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply rounded-lg bg-[var(--light-accent)] text-[var(--light-background)];
|
@apply bg-[var(--light-accent)] text-[var(--light-background)];
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .btn-primary {
|
.dark .btn-primary {
|
||||||
@ -151,7 +165,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@apply rounded-lg border border-[var(--light-text)]/20 bg-[var(--light-foreground)] text-[var(--light-text)];
|
@apply border border-[var(--light-text)]/20 bg-[var(--light-foreground)] text-[var(--light-text)];
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .btn-secondary {
|
.dark .btn-secondary {
|
||||||
@ -187,10 +201,14 @@
|
|||||||
@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 {
|
||||||
/* Height should align with the navbar height for visual consistency */
|
@apply border-[var(--light-text)]/20 p-4;
|
||||||
@apply flex h-20 items-center justify-center border-b border-[var(--light-text)]/20 p-2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .sidebar-header,
|
.dark .sidebar-header,
|
||||||
@ -198,6 +216,10 @@
|
|||||||
@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;
|
||||||
}
|
}
|
||||||
@ -251,11 +273,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
@apply w-full rounded-lg py-2.5 pr-4 pl-11;
|
@apply w-full rounded-md py-2 pr-3 pl-9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar-icon {
|
.search-bar-icon {
|
||||||
@apply absolute top-3 left-8 h-5 w-5 text-[var(--light-text)]/60;
|
@apply absolute top-3 left-7 h-4 w-4 text-[var(--light-text)]/60;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .search-bar-icon {
|
.dark .search-bar-icon {
|
||||||
@ -619,20 +641,4 @@
|
|||||||
.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,8 +17,6 @@
|
|||||||
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">
|
<div class="flex min-h-screen items-center justify-center px-4">
|
||||||
<div class="card rounded-4x1 grid w-[16rem] justify-items-center space-y-6 p-6">
|
<div class="card grid w-auto max-w-md justify-items-center space-y-6 rounded-3xl">
|
||||||
{#if $cError}
|
{#if $cError}
|
||||||
<div class="error max-w-full overflow-hidden text-ellipsis text-center">
|
<div class="error">
|
||||||
{$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-full"
|
class="w-auto self-center"
|
||||||
/>
|
/>
|
||||||
</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-lg">
|
<button type="submit" class="btn-primary w-full rounded-full">
|
||||||
{formName}
|
{formName}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@ -90,8 +90,6 @@
|
|||||||
</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,8 +19,6 @@
|
|||||||
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
|
||||||
@ -30,7 +28,6 @@
|
|||||||
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
|
||||||
@ -44,8 +41,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = () => {
|
||||||
@ -279,7 +274,7 @@
|
|||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<button
|
<button
|
||||||
on:click={toggleSidebar}
|
on:click={toggleSidebar}
|
||||||
class="btn-secondary rounded-lg p-2"
|
class="btn-secondary rounded-full p-2"
|
||||||
aria-label="Toggle sidebar"
|
aria-label="Toggle sidebar"
|
||||||
>
|
>
|
||||||
<ToggleSidebar />
|
<ToggleSidebar />
|
||||||
@ -344,22 +339,9 @@
|
|||||||
{#if $currentFullNote}
|
{#if $currentFullNote}
|
||||||
<NoteEditor note={$currentFullNote} bind:isEditing {saveNote} />
|
<NoteEditor note={$currentFullNote} bind:isEditing {saveNote} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="greeting-container">
|
<div class="flex h-full flex-col items-center justify-center">
|
||||||
<p class="greeting-message">{greetMessage}</p>
|
<p class="mb-4 text-lg">None selected</p>
|
||||||
<p class="greeting-message text-base">
|
<button on:click={createNewNote} class="btn-primary">Create note</button>
|
||||||
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 Create from "$lib/icons/Create.svelte"
|
import CreateNew from "$lib/icons/CreateNew.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="mx-2 flex items-center justify-center">
|
<div class="flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
on:click={createNewNote}
|
on:click={createNewNote}
|
||||||
class="btn-secondary h-9 w-9 p-2 text-center"
|
class="btn-primary rounded-full p-2"
|
||||||
aria-label="Create new note"
|
aria-label="Create new note"
|
||||||
>
|
>
|
||||||
<Create />
|
<CreateNew />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Search bar -->
|
<!-- Search bar -->
|
||||||
<div class="pl-4.5 relative">
|
<div class="relative pl-4">
|
||||||
<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="pl-4.5 relative">
|
<div class="relative pl-4">
|
||||||
<button
|
<button
|
||||||
on:click={closeSidebar}
|
on:click={closeSidebar}
|
||||||
class="btn-secondary h-9 w-9 p-2 text-center md:hidden"
|
class="btn-secondary h-9 w-9 rounded-full p-2 md:hidden"
|
||||||
aria-label="Close sidebar"
|
aria-label="Close sidebar"
|
||||||
>
|
>
|
||||||
<Close />
|
<Close />
|
||||||
@ -149,6 +149,7 @@
|
|||||||
</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,6 +4,7 @@
|
|||||||
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"
|
||||||
})
|
})
|
||||||
@ -15,7 +16,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 p-2"
|
class="btn-secondary rounded-full 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,4 +1,5 @@
|
|||||||
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.
|
||||||
|
@ -1,217 +0,0 @@
|
|||||||
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): number => {
|
const calculateEntropy = (password: string) => {
|
||||||
let poolSize = 0
|
let poolSize = 0
|
||||||
|
|
||||||
for (const [eClass, poolPlus] of ENTROPY_CLASSES) {
|
for (const [eClass, poolPlus] of ENTROPY_CLASSES) {
|
||||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user