feat(frontend): styling

This commit is contained in:
Julian Lobbes 2023-06-01 21:40:36 +01:00
parent ab1e9ea0da
commit 2882db284d
15 changed files with 599 additions and 124 deletions

44
frontend/docs/theme/colorscheme.gpl vendored Normal file
View File

@ -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

View File

@ -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;
}
}

View File

@ -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<void> {
id: user.id,
email: user.email,
isAdmin: user.is_admin,
sessionExpires: new Date(parsedToken.exp),
sessionExpires: new Date(parsedToken.exp * 1000),
});
loadUserIntoStore();
}

View File

@ -0,0 +1,14 @@
<script lang="ts">
</script>
<footer>
Built using Sveltekit.
</footer>
<style class="flex">
footer {
@apply bg-primary-700;
@apply text-center text-white;
@apply p-4;
}
</style>

View File

@ -0,0 +1,67 @@
<script lang="ts">
export let isOpen = false;
export let isHidden = false;
function handleClick() {
isOpen = !isOpen;
}
</script>
<button
class="menu"
class:hidden={isHidden}
class:opened={isOpen}
on:click={handleClick}
aria-label="Main Menu"
aria-expanded={isOpen}
aria-hidden={isHidden}
>
<svg width="32" height="32" viewBox="0 0 100 100">
<path class="line line1" d="M 20,29.000046 H 80.000231 C 80.000231,29.000046 94.498839,28.817352 94.532987,66.711331 94.543142,77.980673 90.966081,81.670246 85.259173,81.668997 79.552261,81.667751 75.000211,74.999942 75.000211,74.999942 L 25.000021,25.000058" />
<path class="line line2" d="M 20,50 H 80" />
<path class="line line3" d="M 20,70.999954 H 80.000231 C 80.000231,70.999954 94.498839,71.182648 94.532987,33.288669 94.543142,22.019327 90.966081,18.329754 85.259173,18.331003 79.552261,18.332249 75.000211,25.000058 75.000211,25.000058 L 25.000021,74.999942" />
</svg>
</button>
<style>
.menu {
background-color: transparent;
border: none;
cursor: pointer;
}
.line {
fill: none;
stroke: white;
stroke-width: 6;
transition: stroke-dasharray 600ms cubic-bezier(0.4, 0, 0.2, 1),
stroke-dashoffset 600ms cubic-bezier(0.4, 0, 0.2, 1);
}
.line1 {
stroke-dasharray: 60 207;
stroke-width: 6;
}
.line2 {
stroke-dasharray: 60 60;
stroke-width: 6;
}
.line3 {
stroke-dasharray: 60 207;
stroke-width: 6;
}
.opened .line1 {
stroke-dasharray: 90 207;
stroke-dashoffset: -134;
stroke-width: 6;
}
.opened .line2 {
stroke-dasharray: 1 60;
stroke-dashoffset: -30;
stroke-width: 6;
}
.opened .line3 {
stroke-dasharray: 90 207;
stroke-dashoffset: -134;
stroke-width: 6;
}
</style>

View File

@ -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('/');
}
</script>
<nav>
<ul>
<li class="inline">
<a href="/">Home</a>
{#if user}
<a href={`/users/${user.id}/todos`}>Todos</a>
<a href={`/users/${user.id}`}>My Profile</a>
{#if user.isAdmin}
<a href={`/users/all`}>All Users</a>
<svelte:window bind:outerWidth={viewportWidth}/>
<nav
class="sticky top-0 transition-transform ease-linear z-40"
class:navbarHidden={isHidden}
bind:clientHeight={currentHeight}
>
{#if !largeScreen}
<div id="navHead"
class="flex"
class:smallScreen={!largeScreen}
>
<a
href="/"
on:click={closeMenu}
class="group"
>
<img
src="/images/common/logo.svg"
alt="Dewit Logo"
class="w-11 h-11 p-2 transition-all bg-secondary-700 rounded-lg group-hover:p-0.5 group-hover:bg-secondary"
/>
</a>
<Hamburger bind:isOpen={menuIsOpen} />
</div>
{/if}
{#if menuIsOpen || largeScreen}
<div id="navContent"
transition:slide="{{delay: largeScreen ? 0 : 100, duration: largeScreen ? 0 : 500}}"
class:flex-col={!largeScreen}
class="flex gap-1 items-center"
>
{#if largeScreen}
<a href="/" class="group">
<img
src="/images/common/logo.svg"
alt="Dewit Logo"
class="w-11 h-11 p-2 transition-all bg-secondary-700 rounded-lg group-hover:p-0.5 group-hover:bg-secondary"
/>
</a>
{/if}
<button on:click={handleLogout}>Log Out</button>
{:else}
<a href="/login">Log In</a>
{/if}
</li>
</ul>
{#if user}
<a
href={`/users/${user.id}/todos`}
on:click={closeMenu}
>
Todos
</a>
<a
href={`/users/${user.id}`}
on:click={closeMenu}
>
My Profile
</a>
{#if user.isAdmin}
<a
href={`/users/all`}
on:click={closeMenu}
>
All Users
</a>
{/if}
{#if largeScreen}<div class="grow" aria-hidden />{/if}
<button
class="outlineButton"
on:click={handleLogout}
>
Log Out
</button>
{:else}
{#if largeScreen}<div class="grow" aria-hidden />{/if}
<a
href="/login"
on:click={closeMenu}
>
Log In
</a>
{/if}
</div>
{/if}
</nav>
<style>
nav {
@apply text-xl font-semibold;
@apply p-2;
@apply bg-primary;
}
a {
@apply block max-w-fit p-2;
@apply text-secondary text-center no-underline;
@apply drop-shadow-md;
@apply transition-all;
}
a:hover {
@apply text-accent font-bold;
}
.smallScreen {
@apply justify-between w-full;
}
.navbarHidden {
@apply -translate-y-full;
}
.outlineButton {
@apply bg-primary-400/50;
@apply border-2 border-secondary;
@apply text-secondary font-medium;
}
.outlineButton:hover {
@apply bg-secondary;
@apply text-secondary-900 font-bold;
}
.outlineButton:active {
@apply bg-secondary-600;
@apply text-secondary-900;
}
.outlineButton:focus {
@apply ring-secondary-400;
}
</style>

View File

@ -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(', ');
}

View File

@ -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();
});
</script>
<Navbar />
<slot />
<svelte:window on:scroll={handleScroll} />
<Navbar bind:isHidden={navbarHidden} bind:currentHeight={navbarHeight} />
<div class="flex flex-col grow justify-center">
<slot />
</div>
<Footer />

View File

@ -2,5 +2,25 @@
console.log(import.meta.env.MODE)
</script>
<h1>Hello World!</h1>
<p>This is a simple Todo-App as a tech stack template for new projects.</p>
<div id="container">
<div class="flex flex-col sm:flex-row items-center">
<img src="images/common/logo.svg" alt="Dewit Logo" class="w-32 h-32" />
<h1 class="text-6xl sm:text-8xl font-accent font-semibold">Dewit</h1>
</div>
<p class="text-2xl sm:text-3xl">
This is a simple Todo-App as a tech stack template for new projects.
</p>
</div>
<style>
#container {
@apply flex flex-col gap-32 justify-center items-center grow;
@apply text-center;
}
p {
@apply p-2;
}
h1 {
@apply p-2;
}
</style>

View File

@ -59,3 +59,9 @@
</fieldset>
</form>
</div>
<style>
button {
@apply bg-accent;
}
</style>

View File

@ -1,8 +1,63 @@
<script lang="ts">
import type { UserProfilePage } from "./+page";
import { getUserFromLocalstorage as getUser } from '$lib/auth/session';
import { formatTime } from '$lib/utils/utils';
import { onMount } from 'svelte';
export let data: UserProfilePage;
const user = getUser();
const userCreatedDate = Date.parse(data.user.created);
let dateNow: number;
$: timeSinceCreation = formatTime(dateNow - userCreatedDate);
function updateCurrentTime() {
dateNow = Date.now()
setTimeout(updateCurrentTime, 1000);
}
onMount(async () => {
updateCurrentTime();
});
</script>
<h1>Profile page for {data.user.email}</h1>
<a href={`/users/${data.user.id}/todos`}>{data.user.first_name}'s Todos</a>
<div class="grow flex flex-col items-center gap-y-4 px-4">
{#if user.id === data.user.id}
<h1>My Profile</h1>
{:else}
<h1>{`${data.user.first_name} ${data.user.last_name}'s Profile`}</h1>
{/if}
<div class="profileInfo">
<p class="description">First Name</p><p class="data">{data.user.first_name}</p>
<p class="description">Last Name</p><p class="data">{data.user.last_name}</p>
<p class="description">Email</p><p class="data">{data.user.email}</p>
<p class="description">Member Since</p><p class="data">{timeSinceCreation}</p>
</div>
<a class="button"
href={`/users/${data.user.id}/todos`}
>
View Todos
</a>
</div>
<style>
h1 {
@apply text-2xl;
}
div.profileInfo {
@apply grid grid-cols-3 gap-x-2 gap-y-4;
@apply p-4 rounded-lg;
@apply border-2 border-secondary/50;
}
p.description {
@apply text-secondary font-semibold text-end;
}
p.data{
@apply col-span-2;
}
</style>

View File

@ -1,25 +0,0 @@
@use 'sass:color';
@use '@material/theme/color-palette';
// Svelte Colors!
@use '@material/theme/index' as theme with (
$primary: #ff3e00,
$secondary: #676778,
$surface: #fff,
$background: #fff,
$error: color-palette.$red-900
);
html,
body {
background-color: theme.$surface;
color: theme.$on-surface;
}
a {
color: #40b3ff;
}
a:visited {
color: color.scale(#40b3ff, $lightness: -35%);
}

View File

@ -1,25 +0,0 @@
@use 'sass:color';
@use '@material/theme/color-palette';
// Svelte Colors! (Dark Theme)
@use '@material/theme/index' as theme with (
$primary: #ff3e00,
$secondary: color.scale(#676778, $whiteness: -10%),
$surface: color.adjust(color-palette.$grey-900, $blue: +4),
$background: #000,
$error: color-palette.$red-700
);
html,
body {
background-color: #000;
color: theme.$on-surface;
}
a {
color: #40b3ff;
}
a:visited {
color: color.scale(#40b3ff, $lightness: -35%);
}

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="800px"
height="800px"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg174"
sodipodi:docname="logo.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs178">
<linearGradient
inkscape:collect="always"
id="linearGradient920">
<stop
style="stop-color:#ff5c00;stop-opacity:0.49663314;"
offset="0"
id="stop916" />
<stop
style="stop-color:#d28a00;stop-opacity:1;"
offset="0.5"
id="stop924" />
<stop
style="stop-color:#ffec00;stop-opacity:0.5;"
offset="1"
id="stop918" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient920"
id="linearGradient922"
x1="2"
y1="12.48534"
x2="21.727"
y2="12.48534"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.1365,-0.48534)" />
</defs>
<sodipodi:namedview
id="namedview176"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="1.02625"
inkscape:cx="400.97442"
inkscape:cy="400"
inkscape:window-width="1900"
inkscape:window-height="1004"
inkscape:window-x="10"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="svg174" />
<path
id="Vector"
d="m 8.1365,12.00006 4.2426,4.2426 8.4844,-8.48532 m -17.727,4.24272 4.24264,4.2426 m 8.48526,-8.48532 -3.2279,3.25742"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="fill:none;stroke:url(#linearGradient922)" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -14,43 +14,56 @@ export default {
transparent: "transparent",
current: "currentColor",
primary: {
50: "#D1DAF0",
100: "#859DD6",
200: "#5778C7",
300: "#385AA8",
400: "#29417A",
500: "#1F315C",
DEFAULT: "#1F315C",
600: "#14213D",
700: "#0F192E",
800: "#0A111F",
900: "#05080F",
50: "#D5E8EC",
100: "#9CCAD3",
200: "#4B9AAA",
300: "#326671",
400: "#1F4047",
500: "#0D1B1E",
DEFAULT: "#0D1B1E",
600: "#0C191C",
700: "#070F11",
800: "#040809",
900: "#000000",
},
secondary: {
50: "#DBF5FA",
100: "#B7EBF5",
200: "#81DCEE",
300: "#4BCDE7",
400: "#1DB8D7",
500: "#189AB4",
DEFAULT: "#189AB4",
600: "#147B90",
700: "#0F5C6C",
800: "#0A3D48",
900: "#051E24",
50: "#F3F7F5",
100: "#DBE6E2",
200: "#B6CDC5",
300: "#9EBDB2",
400: "#86AC9F",
500: "#6C9A8B",
DEFAULT: "#6C9A8B",
600: "#5B8677",
700: "#4A6D61",
800: "#3A554B",
900: "#192420",
},
accent: {
50: "#FED7DE",
100: "#FD9BAD",
200: "#FC738C",
300: "#FB4B6B",
400: "#FB234B",
500: "#DC042C",
DEFAULT: "#DC042C",
600: "#9A031E",
700: "#780218",
800: "#500110",
900: "#280008",
50: "#FFF9EB",
100: "#FFEDC2",
200: "#FFE7AD",
300: "#FFDA85",
400: "#FFCE5C",
500: "#FFC233",
DEFAULT: "#FFC233",
600: "#FFB60A",
700: "#E09D00",
800: "#A37200",
900: "#523900",
},
background: {
50: "#FFFFFF",
100: "#FFFFFF",
200: "#FFFFFF",
300: "#FFFFFF",
400: "#FFFFFF",
500: "#FCFCFC",
DEFAULT: "#FCFCFC",
600: "#CCCCCC",
700: "#A3A3A3",
800: "#707070",
900: "#333333",
},
},
screens: {