From 2882db284d6a074280268c79f0d3e781020f16a4 Mon Sep 17 00:00:00 2001 From: Julian Lobbes Date: Thu, 1 Jun 2023 21:40:36 +0100 Subject: [PATCH] feat(frontend): styling --- frontend/docs/theme/colorscheme.gpl | 44 +++++ frontend/src/app.css | 73 +++++++-- frontend/src/lib/auth/session.ts | 4 +- .../src/lib/components/footer/Footer.svelte | 14 ++ .../lib/components/navbar/Hamburger.svelte | 67 ++++++++ .../src/lib/components/navbar/Navbar.svelte | 150 ++++++++++++++++-- frontend/src/lib/utils/utils.ts | 44 +++++ frontend/src/routes/+layout.svelte | 35 +++- frontend/src/routes/+page.svelte | 24 ++- frontend/src/routes/login/+page.svelte | 6 + frontend/src/routes/users/[user]/+page.svelte | 59 ++++++- frontend/src/theme/_smui-theme.scss | 25 --- frontend/src/theme/dark/_smui-theme.scss | 25 --- frontend/static/images/common/logo.svg | 74 +++++++++ frontend/tailwind.config.js | 79 +++++---- 15 files changed, 599 insertions(+), 124 deletions(-) create mode 100644 frontend/docs/theme/colorscheme.gpl create mode 100644 frontend/src/lib/components/footer/Footer.svelte create mode 100644 frontend/src/lib/components/navbar/Hamburger.svelte delete mode 100644 frontend/src/theme/_smui-theme.scss delete mode 100644 frontend/src/theme/dark/_smui-theme.scss create mode 100644 frontend/static/images/common/logo.svg diff --git a/frontend/docs/theme/colorscheme.gpl b/frontend/docs/theme/colorscheme.gpl new file mode 100644 index 0000000..11a6474 --- /dev/null +++ b/frontend/docs/theme/colorscheme.gpl @@ -0,0 +1,44 @@ +GIMP Palette +Name: colorscheme.gpl +Columns: 1 +# +213 232 236 primary-50 +156 202 211 primary-100 +75 154 170 primary-200 +50 102 113 primary-300 +31 64 71 primary-400 +13 27 30 primary-500 (default) +12 25 28 primary-600 +7 15 17 primary-700 +4 8 9 primary-800 +0 0 0 primary-900 +243 247 245 secondary-50 +219 230 226 secondary-100 +182 205 197 secondary-200 +158 189 178 secondary-300 +134 172 159 secondary-400 +108 154 139 secondary-500 (default) +91 134 119 secondary-600 +74 109 97 secondary-700 +58 85 75 secondary-800 +25 36 32 secondary-900 +255 249 235 accent-50 +255 237 194 accent-100 +255 231 173 accent-200 +255 218 133 accent-300 +255 206 92 accent-400 +255 194 51 accent-500 (default) +255 182 10 accent-600 +224 157 0 accent-700 +163 114 0 accent-800 +82 57 0 accent-900 +255 255 255 background-50 +255 255 255 background-100 +255 255 255 background-200 +255 255 255 background-300 +255 255 255 background-400 +252 252 252 background-500 (default) +204 204 204 background-600 +163 163 163 background-700 +112 112 112 background-800 +51 51 51 background-900 diff --git a/frontend/src/app.css b/frontend/src/app.css index 4ca388e..5206e31 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -8,6 +8,13 @@ /* Global defaults */ @layer base { + body { + @apply bg-background; + @apply text-primary; + @apply flex flex-col gap-4 justify-between; + @apply min-h-screen; + } + h1, h2, h3, h4, h5, h6 { @apply font-bold text-primary; } @@ -32,11 +39,10 @@ } a { - @apply underline text-secondary-500; + @apply underline text-primary-300; } - a:visited { - @apply text-secondary-700; + @apply underline text-primary-400; } } @@ -44,25 +50,53 @@ button { @apply p-2; @apply rounded-md; - @apply bg-neutral-100; - @apply border-2 border-neutral-200; + @apply bg-accent; + @apply text-primary-400 font-semibold; + @apply drop-shadow-md; @apply transition-all ease-in-out; } button:hover { - @apply bg-neutral-200; - @apply border-neutral-300; + @apply bg-accent-600; + @apply drop-shadow-lg; + @apply text-primary font-bold; } button:active { - @apply bg-neutral-300; - @apply border-neutral-400; + @apply bg-accent-700; + @apply drop-shadow-none; + @apply text-primary-600; } button:focus { - @apply ring-1 ring-neutral-400 ring-offset-1; + @apply ring-2 ring-accent-600; } button:disabled { @apply opacity-25; @apply bg-neutral-100; - @apply border-2 border-neutral-200; + } + a.button { + @apply p-2; + @apply rounded-md; + @apply bg-accent; + @apply text-primary-400 font-semibold; + @apply no-underline; + @apply drop-shadow-md; + @apply transition-all ease-in-out; + } + a.button:hover { + @apply bg-accent-600; + @apply drop-shadow-lg; + @apply text-primary font-bold; + } + a.button:active { + @apply bg-accent-700; + @apply drop-shadow-none; + @apply text-primary-600; + } + a.button:focus { + @apply ring-2 ring-accent-600; + } + a.button:disabled { + @apply opacity-25; + @apply bg-neutral-100; } table { @@ -101,22 +135,27 @@ } fieldset { - @apply border border-secondary-300; - @apply p-2; + @apply border border-primary/50; + @apply rounded-lg; + @apply p-4; } legend { - @apply text-black/50; + @apply text-primary/50 font-light; } input { + @apply text-primary font-semibold; @apply border border-secondary; - @apply bg-secondary-50; + @apply bg-secondary/20; @apply rounded-md; @apply p-1; } + input::placeholder { + @apply text-secondary/50 font-light; + } input:focus { - @apply outline-none border-none ring-2 ring-secondary-400; + @apply outline-none border-none ring-2 ring-secondary; } label { - @apply text-sm text-secondary; + @apply text-sm text-secondary font-light; } } diff --git a/frontend/src/lib/auth/session.ts b/frontend/src/lib/auth/session.ts index 04423c9..476e488 100644 --- a/frontend/src/lib/auth/session.ts +++ b/frontend/src/lib/auth/session.ts @@ -55,7 +55,7 @@ function saveUserToLocalstorage(user: StoredUser): void { /** * Retrieves and returns the user, if present, from localstorage. */ -function getUserFromLocalstorage(): StoredUser | null { +export function getUserFromLocalstorage(): StoredUser | null { let item: string | null = localStorage.getItem(userKey); if (typeof item !== 'string') { return null; @@ -141,7 +141,7 @@ export async function login(email: string, password: string): Promise { id: user.id, email: user.email, isAdmin: user.is_admin, - sessionExpires: new Date(parsedToken.exp), + sessionExpires: new Date(parsedToken.exp * 1000), }); loadUserIntoStore(); } diff --git a/frontend/src/lib/components/footer/Footer.svelte b/frontend/src/lib/components/footer/Footer.svelte new file mode 100644 index 0000000..903e91f --- /dev/null +++ b/frontend/src/lib/components/footer/Footer.svelte @@ -0,0 +1,14 @@ + + +
+ Built using Sveltekit. +
+ + diff --git a/frontend/src/lib/components/navbar/Hamburger.svelte b/frontend/src/lib/components/navbar/Hamburger.svelte new file mode 100644 index 0000000..7eefb22 --- /dev/null +++ b/frontend/src/lib/components/navbar/Hamburger.svelte @@ -0,0 +1,67 @@ + + + + + diff --git a/frontend/src/lib/components/navbar/Navbar.svelte b/frontend/src/lib/components/navbar/Navbar.svelte index e428363..644c25f 100644 --- a/frontend/src/lib/components/navbar/Navbar.svelte +++ b/frontend/src/lib/components/navbar/Navbar.svelte @@ -2,32 +2,150 @@ import { goto } from '$app/navigation'; import { logout, storedUser } from '$lib/auth/session'; import type { StoredUser } from '$lib/auth/session'; + import Hamburger from './Hamburger.svelte'; + import { slide } from 'svelte/transition'; - let user: StoredUser | null = null; + export let isHidden = false; + export let currentHeight: number; + + let user: StoredUser | unknown | null = null; storedUser.subscribe((value) => { user = value; }); + let viewportWidth: number; + + $: largeScreen = viewportWidth > 640; + let menuIsOpen = false; + + function closeMenu() { + menuIsOpen = false; + } + function handleLogout() { + menuIsOpen = false; logout(); goto('/'); } - + + diff --git a/frontend/src/lib/utils/utils.ts b/frontend/src/lib/utils/utils.ts index 096022d..950fc97 100644 --- a/frontend/src/lib/utils/utils.ts +++ b/frontend/src/lib/utils/utils.ts @@ -5,3 +5,47 @@ export function range(n: number): number[] { return Array.from(Array(n).keys()); } + +/** + * Converts a given number of milliseconds into a human-readable string representation of time. + * + * @param {number} milliseconds - The number of milliseconds to be formatted. + * @param {number} + * @returns {string} The formatted time string in years, months, days, hours, minutes, and seconds. + */ +export function formatTime(milliseconds: number, precision: number = 3): string { + const seconds = Math.round(milliseconds / 1000); + + if (seconds <= 0) { + return '0 seconds'; + } + + const years = Math.floor(seconds / (365 * 24 * 60 * 60)); + const months = Math.floor((seconds % (365 * 24 * 60 * 60)) / (30 * 24 * 60 * 60)); + const days = Math.floor((seconds % (30 * 24 * 60 * 60)) / (24 * 60 * 60)); + const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60)); + const minutes = Math.floor((seconds % (60 * 60)) / 60); + const remainingSeconds = seconds % 60; + + const timeParts = []; + if (years > 0) { + timeParts.push(`${years} year${years > 1 ? 's' : ''}`); + } + if (months > 0) { + timeParts.push(`${months} month${months > 1 ? 's' : ''}`); + } + if (days > 0) { + timeParts.push(`${days} day${days > 1 ? 's' : ''}`); + } + if (hours > 0) { + timeParts.push(`${hours} hour${hours > 1 ? 's' : ''}`); + } + if (minutes > 0) { + timeParts.push(`${minutes} minute${minutes > 1 ? 's' : ''}`); + } + if (remainingSeconds > 0) { + timeParts.push(`${remainingSeconds} second${remainingSeconds > 1 ? 's' : ''}`); + } + + return timeParts.slice(0, precision).join(', '); +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 0c4d902..8ef6dd3 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -27,6 +27,8 @@ import { page } from '$app/stores'; import { loadUserIntoStore } from '$lib/auth/session'; import Navbar from '$lib/components/navbar/Navbar.svelte'; + import Footer from '$lib/components/footer/Footer.svelte'; + import { slide } from 'svelte/transition'; export let title = import.meta.env.PUBLIC_TITLE; export let description = import.meta.env.PUBLIC_DESCRIPTION; @@ -34,10 +36,39 @@ const ogImageUrl = new URL('/images/common/og-image.webp', import.meta.url).href + let navbarHidden = false; + let lastKnownScrollPosY = 0; + const navbarToggleThreshold = 32; + let navbarHeight: number; + + function handleScroll(event: Event): void { + if (window.scrollY <= navbarHeight) { + navbarHidden = false; + } else { + let scrollDistance = Math.abs(window.scrollY - lastKnownScrollPosY); + let scrollDirection = (window.scrollY - lastKnownScrollPosY) > 0 ? 'down' : 'up'; + + if (scrollDistance > navbarToggleThreshold) { + if (scrollDirection == 'down') { + navbarHidden = true; + } else { + navbarHidden = false; + } + } + } + + lastKnownScrollPosY = window.scrollY; + } + onMount(async () => { loadUserIntoStore(); }); - - + + + +
+ +
+