frontend: initial commit
This commit is contained in:
12
frontend/README.md
Normal file
12
frontend/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Frontend
|
||||||
|
|
||||||
|
## Run locally
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# or start the server and open the app in a new browser tab
|
||||||
|
npm run dev -- --open
|
||||||
|
```
|
||||||
64
frontend/src/app.d.ts
vendored
Normal file
64
frontend/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
|
||||||
|
interface Subscription {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
details: string;
|
||||||
|
conditions: string | null;
|
||||||
|
monthly_fee: number;
|
||||||
|
hourly_rate: number;
|
||||||
|
included_hours_per_year: number | null;
|
||||||
|
included_hours_per_month: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Membership {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
parent_member_id: number | null;
|
||||||
|
subscription_model: Subscription | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BankAccount {
|
||||||
|
id: string;
|
||||||
|
mandate_date_signed: string;
|
||||||
|
bank: string;
|
||||||
|
account_holder_name: string;
|
||||||
|
iban: string;
|
||||||
|
bic: string;
|
||||||
|
mandate_reference: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
email: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
phone: string | null;
|
||||||
|
notes: string;
|
||||||
|
address: string;
|
||||||
|
zip_code: string;
|
||||||
|
city: string;
|
||||||
|
status: number;
|
||||||
|
id: string;
|
||||||
|
role_id: number;
|
||||||
|
date_of_birth: string;
|
||||||
|
membership: Membership;
|
||||||
|
bank_account: BankAccount;
|
||||||
|
company: string | null;
|
||||||
|
profile_picture: string | null;
|
||||||
|
payment_status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
interface Locals {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
18
frontend/src/app.html
Normal file
18
frontend/src/app.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Poiret+One&family=Quicksand:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
58
frontend/src/hooks.server.js
Normal file
58
frontend/src/hooks.server.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { BASE_API_URI } from "$lib/utils/constants.js";
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Handle} */
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
if (event.locals.user) {
|
||||||
|
// if there is already a user in session load page as normal
|
||||||
|
return await resolve(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get cookies from browser
|
||||||
|
const jwt = event.cookies.get("jwt");
|
||||||
|
|
||||||
|
if (!jwt) {
|
||||||
|
// if there is no jwt load page as normal
|
||||||
|
return await resolve(event);
|
||||||
|
}
|
||||||
|
const response = await fetch(`${BASE_API_URI}/users/backend/current-user`, {
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Cookie: `jwt=${jwt}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
// Clear the invalid JWT cookie
|
||||||
|
event.cookies.delete("jwt", { path: "/" });
|
||||||
|
return await resolve(event);
|
||||||
|
}
|
||||||
|
// find the user based on the jwt
|
||||||
|
|
||||||
|
const userData = await response.json();
|
||||||
|
|
||||||
|
event.locals.user = userData;
|
||||||
|
// event.locals.user = await response.json();
|
||||||
|
if (event.locals.user.date_of_birth) {
|
||||||
|
event.locals.user.date_of_birth =
|
||||||
|
event.locals.user.date_of_birth.split("T")[0];
|
||||||
|
}
|
||||||
|
if (event.locals.user.membership) {
|
||||||
|
if (event.locals.user.membership.start_date) {
|
||||||
|
event.locals.user.membership.start_date =
|
||||||
|
event.locals.user.membership.start_date.split("T")[0];
|
||||||
|
}
|
||||||
|
if (event.locals.user.membership.end_date) {
|
||||||
|
event.locals.user.membership.end_date =
|
||||||
|
event.locals.user.membership.end_date.split("T")[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.locals.user.bank_account &&
|
||||||
|
event.locals.user.bank_account.mandate_date_signed
|
||||||
|
) {
|
||||||
|
event.locals.user.bank_account.mandate_date_signed =
|
||||||
|
event.locals.user.bank_account.mandate_date_signed.split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// load page as normal
|
||||||
|
return await resolve(event);
|
||||||
|
}
|
||||||
7
frontend/src/index.test.js
Normal file
7
frontend/src/index.test.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('sum test', () => {
|
||||||
|
it('adds 1 + 2 to equal 3', () => {
|
||||||
|
expect(1 + 2).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
24
frontend/src/lib/components/Footer.svelte
Normal file
24
frontend/src/lib/components/Footer.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
// import Developer from "$lib/img/hero-image.png";
|
||||||
|
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer class="footer-container">
|
||||||
|
<div class="footer-branding-container">
|
||||||
|
<div class="footer-branding">
|
||||||
|
<a class="footer-crafted-by-container" href="https://github.com/Sirneij">
|
||||||
|
<span>Developed by</span>
|
||||||
|
<!-- <img
|
||||||
|
class="footer-branded-crafted-img"
|
||||||
|
src={Developer}
|
||||||
|
alt="Alexander Stölting"
|
||||||
|
/> -->
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="footer-copyright"
|
||||||
|
>© {year} Alexander Stölting. All Rights Reserved.</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
79
frontend/src/lib/components/Header.svelte
Normal file
79
frontend/src/lib/components/Header.svelte
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { applyAction, enhance } from "$app/forms";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
// import Developer from "$lib/img/hero-image.png";
|
||||||
|
import Avatar from "$lib/img/teamavatar.png";
|
||||||
|
onMount(() => {
|
||||||
|
console.log("Page data in Header:", $page);
|
||||||
|
});
|
||||||
|
|
||||||
|
$: {
|
||||||
|
console.log("Page data updated:", $page);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="header-crafted-by-container">
|
||||||
|
<!-- <a href="https://tiny-bits.net/">
|
||||||
|
<span>Developed by</span><img
|
||||||
|
src={Developer}
|
||||||
|
alt="Alexander Stölting"
|
||||||
|
/> -->
|
||||||
|
<!-- </a> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="header-nav-item" class:active={$page.url.pathname === "/"}>
|
||||||
|
<a href="/">home</a>
|
||||||
|
</div>
|
||||||
|
{#if !$page.data.user}
|
||||||
|
<div
|
||||||
|
class="header-nav-item"
|
||||||
|
class:active={$page.url.pathname === "/auth/login"}
|
||||||
|
>
|
||||||
|
<a href="/auth/login">login</a>
|
||||||
|
</div>
|
||||||
|
<!-- <div
|
||||||
|
class="header-nav-item"
|
||||||
|
class:active={$page.url.pathname === "/auth/register"}
|
||||||
|
>
|
||||||
|
<a href="/auth/register">register</a>
|
||||||
|
</div> -->
|
||||||
|
{:else}
|
||||||
|
<div class="header-nav-item">
|
||||||
|
<a href="/auth/about/{$page.data.user.id}">
|
||||||
|
<img
|
||||||
|
src={$page.data.user.thumbnail
|
||||||
|
? $page.data.user.thumbnail
|
||||||
|
: Avatar}
|
||||||
|
alt={`${$page.data.user.first_name} ${$page.data.user.last_name}`}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<!-- {#if $page.data.user.is_superuser}
|
||||||
|
<div
|
||||||
|
class="header-nav-item"
|
||||||
|
class:active={$page.url.pathname.startsWith("/auth/admin")}
|
||||||
|
>
|
||||||
|
<a href="/auth/admin">admin</a>
|
||||||
|
</div>
|
||||||
|
{/if} -->
|
||||||
|
<form
|
||||||
|
class="header-nav-item"
|
||||||
|
action="/auth/logout"
|
||||||
|
method="POST"
|
||||||
|
use:enhance={async () => {
|
||||||
|
return async ({ result }) => {
|
||||||
|
await applyAction(result);
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button type="submit">logout</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
14
frontend/src/lib/components/Transition.svelte
Normal file
14
frontend/src/lib/components/Transition.svelte
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
/** @type {string} */
|
||||||
|
export let key;
|
||||||
|
|
||||||
|
/** @type {number} */
|
||||||
|
export let duration = 300;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key key}
|
||||||
|
<div in:slide={{ duration, delay: duration }} out:slide={{ duration }}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
521
frontend/src/lib/css/styles.css
Normal file
521
frontend/src/lib/css/styles.css
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Roboto Mono";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url(https://fonts.gstatic.com/s/robotomono/v22/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_gPq_ROW9.ttf)
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto Mono";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(https://fonts.gstatic.com/s/robotomono/v22/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW9.ttf)
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
padding: 0 30px;
|
||||||
|
background-color: black;
|
||||||
|
color: #9b9b9b;
|
||||||
|
font-family: "Quicksand", sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 5em auto 0 auto;
|
||||||
|
}
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
display: inline;
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
color: white;
|
||||||
|
border-style: none;
|
||||||
|
height: 21px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 45px 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 2rem 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 45px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0 0 32px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
transition: border 0.2s ease-in-out;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #00b7ef;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
border-bottom-color: #00b7ef;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
li strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 32px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.optanon-alert-box-wrapper {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.hide-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
body {
|
||||||
|
margin: 8em auto 0 auto;
|
||||||
|
}
|
||||||
|
.hide-mobile {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 3em 0 0;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
.header.top-banner-open {
|
||||||
|
margin-top: 5px;
|
||||||
|
transition: all 0.2s linear;
|
||||||
|
}
|
||||||
|
.header .header-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: calc(1200px + 10em);
|
||||||
|
height: 5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container a {
|
||||||
|
display: flex;
|
||||||
|
color: #9b9b9b;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container a img {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container a span {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 2px 1ch 0 0;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container .auth0 {
|
||||||
|
margin-left: 1ch;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item {
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item button {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item.active a,
|
||||||
|
.header .header-container .header-right .header-nav-item.active button {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-container .header-right a img {
|
||||||
|
margin-top: -0.4rem;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-container .header-right .header-nav-item a,
|
||||||
|
.header .header-container .header-right .header-nav-item button {
|
||||||
|
transition: color 0.3s ease-in-out;
|
||||||
|
display: block;
|
||||||
|
padding: 20px 0;
|
||||||
|
border: none;
|
||||||
|
color: #9b9b9b;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item:hover a,
|
||||||
|
.header .header-container .header-right .header-nav-item:hover button {
|
||||||
|
color: #fdfff5;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.header {
|
||||||
|
padding: 3em 5rem 0;
|
||||||
|
}
|
||||||
|
.header.top-banner-open {
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
.header .header-container {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item {
|
||||||
|
margin-left: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button-dark {
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease-in-out,
|
||||||
|
background-color 0.3s ease-in-out;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 28px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #595b5c;
|
||||||
|
}
|
||||||
|
.button-dark:hover {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
.button-colorful {
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease-in-out,
|
||||||
|
background-color 0.3s ease-in-out;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 28px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #d43aff;
|
||||||
|
border: 1px solid #d43aff;
|
||||||
|
}
|
||||||
|
.button-colorful:hover {
|
||||||
|
background-color: #c907ff;
|
||||||
|
border-color: #c907ff;
|
||||||
|
}
|
||||||
|
.button-orange {
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease-in-out,
|
||||||
|
background-color 0.3s ease-in-out;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 28px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #eb5424;
|
||||||
|
border: 1px solid #eb5424;
|
||||||
|
}
|
||||||
|
.button-orange:hover {
|
||||||
|
background-color: #ca3f12;
|
||||||
|
border-color: #ca3f12;
|
||||||
|
}
|
||||||
|
.button-colorful:disabled {
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease-in-out,
|
||||||
|
background-color 0.3s ease-in-out;
|
||||||
|
color: white;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 28px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #9a9a9a;
|
||||||
|
border: 1px solid #9a9a9a;
|
||||||
|
}
|
||||||
|
.hero-container {
|
||||||
|
max-width: 795px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto 70px auto;
|
||||||
|
}
|
||||||
|
.hero-container .hero-logo {
|
||||||
|
margin-top: 88px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.hero-container .hero-subtitle {
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0 0 45px 0;
|
||||||
|
}
|
||||||
|
.hero-container .hero-buttons-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.hero-container .hero-buttons-container button {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.hero-container {
|
||||||
|
margin: 0 auto 140px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
color: white;
|
||||||
|
letter-spacing: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .content {
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.container .content .step-title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 86px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.container .content .step-subtitle {
|
||||||
|
position: relative;
|
||||||
|
top: -5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.container .content {
|
||||||
|
margin-top: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
left: 100px;
|
||||||
|
display: flex;
|
||||||
|
width: calc(100% - 100px);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container .content {
|
||||||
|
max-width: 795px;
|
||||||
|
padding-left: 116px;
|
||||||
|
}
|
||||||
|
.container .content .step-title {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.input-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 73px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.input-box .label {
|
||||||
|
text-transform: lowercase;
|
||||||
|
margin: 0 1ch 0 0;
|
||||||
|
}
|
||||||
|
.input-box .input {
|
||||||
|
background-color: #494848;
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: none;
|
||||||
|
border: 3px solid #494848;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 444px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.input-box {
|
||||||
|
padding: 0 30px;
|
||||||
|
margin: 32px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: first baseline;
|
||||||
|
}
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.btn-container {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.btn-container p {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: rgb(255 228 230);
|
||||||
|
border: 1px solid rgb(225 29 72);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgb(225 29 72);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.warning a {
|
||||||
|
color: rgb(225 29 72);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.warning.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin-top: 10rem;
|
||||||
|
padding: 30px 40px;
|
||||||
|
background: #2f3132;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.error p {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
.error p.intro {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
.error .button-colorful {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.error {
|
||||||
|
padding: 65px 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-branding-container {
|
||||||
|
color: white;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 73px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-branding-container .footer-branding {
|
||||||
|
display: flex;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.footer-branding-container .footer-branding {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
margin: 30px 0 0;
|
||||||
|
}
|
||||||
|
.footer-branding-container .footer-branding .footer-crafted-by-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.footer-branding-container .footer-branding .footer-crafted-by-container span {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 3px 1ch 0 0;
|
||||||
|
}
|
||||||
|
.footer-branding-container
|
||||||
|
.footer-branding
|
||||||
|
.footer-crafted-by-container
|
||||||
|
.footer-branded-crafted-img {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-branding-container .footer-branding .footer-copyright {
|
||||||
|
color: #696969;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.footer-container {
|
||||||
|
width: 100%;
|
||||||
|
color: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.footer-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-loader {
|
||||||
|
--b: 20px; /* border thickness */
|
||||||
|
--n: 15; /* number of dashes*/
|
||||||
|
--g: 7deg; /* gap between dashes*/
|
||||||
|
--c: #d43aff; /* the color */
|
||||||
|
|
||||||
|
width: 40px; /* size */
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 1px; /* get rid of bad outlines */
|
||||||
|
background: conic-gradient(#0000, var(--c)) content-box;
|
||||||
|
--_m: /* we use +/-1deg between colors to avoid jagged edges */ repeating-conic-gradient(
|
||||||
|
#0000 0deg,
|
||||||
|
#000 1deg calc(360deg / var(--n) - var(--g) - 1deg),
|
||||||
|
#0000 calc(360deg / var(--n) - var(--g)) calc(360deg / var(--n))
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
farthest-side,
|
||||||
|
#0000 calc(98% - var(--b)),
|
||||||
|
#000 calc(100% - var(--b))
|
||||||
|
);
|
||||||
|
-webkit-mask: var(--_m);
|
||||||
|
mask: var(--_m);
|
||||||
|
-webkit-mask-composite: destination-in;
|
||||||
|
mask-composite: intersect;
|
||||||
|
animation: load 1s infinite steps(var(--n));
|
||||||
|
}
|
||||||
|
@keyframes load {
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
505
frontend/src/lib/css/styles.min.css
vendored
Normal file
505
frontend/src/lib/css/styles.min.css
vendored
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Roboto Mono";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url(https://fonts.gstatic.com/s/robotomono/v22/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_gPq_ROW9.ttf)
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto Mono";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(https://fonts.gstatic.com/s/robotomono/v22/L0xuDF4xlVMF-BfR8bXMIhJHg45mwgGEFl0_3vq_ROW9.ttf)
|
||||||
|
format("truetype");
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
padding: 0 30px;
|
||||||
|
background-color: #000;
|
||||||
|
color: #9b9b9b;
|
||||||
|
font-family: Quicksand, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 5em auto 0 auto;
|
||||||
|
}
|
||||||
|
code,
|
||||||
|
pre {
|
||||||
|
display: inline;
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
color: #fff;
|
||||||
|
border-style: none;
|
||||||
|
height: 21px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 45px 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 2rem 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 45px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0 0 32px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
transition: border 0.2s ease-in-out;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #00b7ef;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
border-bottom-color: #00b7ef;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
li strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 32px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.image img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.optanon-alert-box-wrapper {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.hide-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
body {
|
||||||
|
margin: 8em auto 0 auto;
|
||||||
|
}
|
||||||
|
.hide-mobile {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 3em 0 0;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
.header.top-banner-open {
|
||||||
|
margin-top: 5px;
|
||||||
|
transition: all 0.2s linear;
|
||||||
|
}
|
||||||
|
.header .header-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: calc(1200px + 10em);
|
||||||
|
height: 5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container a {
|
||||||
|
display: flex;
|
||||||
|
color: #9b9b9b;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container a img {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container a span {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 2px 1ch 0 0;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container .auth0 {
|
||||||
|
margin-left: 1ch;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item {
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item button {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item.active a,
|
||||||
|
.header .header-container .header-right .header-nav-item.active button {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right a img {
|
||||||
|
margin-top: -0.4rem;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item a,
|
||||||
|
.header .header-container .header-right .header-nav-item button {
|
||||||
|
transition: color 0.3s ease-in-out;
|
||||||
|
display: block;
|
||||||
|
padding: 20px 0;
|
||||||
|
border: none;
|
||||||
|
color: #9b9b9b;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item:hover a,
|
||||||
|
.header .header-container .header-right .header-nav-item:hover button {
|
||||||
|
color: #fdfff5;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.header {
|
||||||
|
padding: 3em 5rem 0;
|
||||||
|
}
|
||||||
|
.header.top-banner-open {
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
.header .header-container {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item {
|
||||||
|
margin-left: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button-dark {
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease-in-out,
|
||||||
|
background-color 0.3s ease-in-out;
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 28px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid #595b5c;
|
||||||
|
}
|
||||||
|
.button-dark:hover {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
|
.button-colorful {
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease-in-out,
|
||||||
|
background-color 0.3s ease-in-out;
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 28px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #d43aff;
|
||||||
|
border: 1px solid #d43aff;
|
||||||
|
}
|
||||||
|
.button-colorful:hover {
|
||||||
|
background-color: #c907ff;
|
||||||
|
border-color: #c907ff;
|
||||||
|
}
|
||||||
|
.button-orange {
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease-in-out,
|
||||||
|
background-color 0.3s ease-in-out;
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 28px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #eb5424;
|
||||||
|
border: 1px solid #eb5424;
|
||||||
|
}
|
||||||
|
.button-orange:hover {
|
||||||
|
background-color: #ca3f12;
|
||||||
|
border-color: #ca3f12;
|
||||||
|
}
|
||||||
|
.button-colorful:disabled {
|
||||||
|
transition:
|
||||||
|
border-color 0.3s ease-in-out,
|
||||||
|
background-color 0.3s ease-in-out;
|
||||||
|
color: #fff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 18px 28px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #9a9a9a;
|
||||||
|
border: 1px solid #9a9a9a;
|
||||||
|
}
|
||||||
|
.hero-container {
|
||||||
|
max-width: 795px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto 70px auto;
|
||||||
|
}
|
||||||
|
.hero-container .hero-logo {
|
||||||
|
margin-top: 88px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.hero-container .hero-subtitle {
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0 0 45px 0;
|
||||||
|
}
|
||||||
|
.hero-container .hero-buttons-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.hero-container .hero-buttons-container button {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.hero-container {
|
||||||
|
margin: 0 auto 140px auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.container .content {
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.container .content .step-title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 86px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.container .content .step-subtitle {
|
||||||
|
position: relative;
|
||||||
|
top: -5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.container .content {
|
||||||
|
margin-top: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
left: 100px;
|
||||||
|
display: flex;
|
||||||
|
width: calc(100% - 100px);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container .content {
|
||||||
|
max-width: 795px;
|
||||||
|
padding-left: 116px;
|
||||||
|
}
|
||||||
|
.container .content .step-title {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.input-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
width: 100%;
|
||||||
|
height: 73px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: "Roboto Mono", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.input-box .label {
|
||||||
|
text-transform: lowercase;
|
||||||
|
margin: 0 1ch 0 0;
|
||||||
|
}
|
||||||
|
.input-box .input {
|
||||||
|
background-color: #494848;
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: 0;
|
||||||
|
border: 3px solid #494848;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 444px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.input-box {
|
||||||
|
padding: 0 30px;
|
||||||
|
margin: 32px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: first baseline;
|
||||||
|
}
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.btn-container {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.btn-container p {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: rgb(255 228 230);
|
||||||
|
border: 1px solid rgb(225 29 72);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: rgb(225 29 72);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.warning a {
|
||||||
|
color: rgb(225 29 72);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.warning.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
margin-top: 10rem;
|
||||||
|
padding: 30px 40px;
|
||||||
|
background: #2f3132;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.error p {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
.error p.intro {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
.error .button-colorful {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.error {
|
||||||
|
padding: 65px 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.footer-branding-container {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 73px;
|
||||||
|
}
|
||||||
|
.footer-branding-container .footer-branding {
|
||||||
|
display: flex;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.footer-branding-container .footer-branding {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
margin: 30px 0 0;
|
||||||
|
}
|
||||||
|
.footer-branding-container .footer-branding .footer-crafted-by-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.footer-branding-container .footer-branding .footer-crafted-by-container span {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 3px 1ch 0 0;
|
||||||
|
}
|
||||||
|
.footer-branding-container
|
||||||
|
.footer-branding
|
||||||
|
.footer-crafted-by-container
|
||||||
|
.footer-branded-crafted-img {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
.footer-branding-container .footer-branding .footer-copyright {
|
||||||
|
color: #696969;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.footer-container {
|
||||||
|
width: 100%;
|
||||||
|
color: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
@media (min-width: 680px) {
|
||||||
|
.footer-container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.simple-loader {
|
||||||
|
--b: 20px;
|
||||||
|
--n: 15;
|
||||||
|
--g: 7deg;
|
||||||
|
--c: #d43aff;
|
||||||
|
width: 40px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 1px;
|
||||||
|
background: conic-gradient(#0000, var(--c)) content-box;
|
||||||
|
--_m: repeating-conic-gradient(
|
||||||
|
#0000 0deg,
|
||||||
|
#000 1deg calc(360deg / var(--n) - var(--g) - 1deg),
|
||||||
|
#0000 calc(360deg / var(--n) - var(--g)) calc(360deg / var(--n))
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
farthest-side,
|
||||||
|
#0000 calc(98% - var(--b)),
|
||||||
|
#000 calc(100% - var(--b))
|
||||||
|
);
|
||||||
|
-webkit-mask: var(--_m);
|
||||||
|
mask: var(--_m);
|
||||||
|
-webkit-mask-composite: destination-in;
|
||||||
|
mask-composite: intersect;
|
||||||
|
animation: load 1s infinite steps(var(--n));
|
||||||
|
}
|
||||||
|
@keyframes load {
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
frontend/src/lib/utils/constants.js
Normal file
3
frontend/src/lib/utils/constants.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const BASE_API_URI = import.meta.env.DEV
|
||||||
|
? import.meta.env.VITE_BASE_API_URI_DEV
|
||||||
|
: import.meta.env.VITE_BASE_API_URI_PROD;
|
||||||
105
frontend/src/lib/utils/helpers.js
Normal file
105
frontend/src/lib/utils/helpers.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { quintOut } from "svelte/easing";
|
||||||
|
import { crossfade } from "svelte/transition";
|
||||||
|
|
||||||
|
export const [send, receive] = crossfade({
|
||||||
|
duration: (d) => Math.sqrt(d * 200),
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
fallback(node, params) {
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
const transform = style.transform === "none" ? "" : style.transform;
|
||||||
|
|
||||||
|
return {
|
||||||
|
duration: 600,
|
||||||
|
easing: quintOut,
|
||||||
|
css: (t) => `
|
||||||
|
transform: ${transform} scale(${t});
|
||||||
|
opacity: ${t}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an email field
|
||||||
|
* @file lib/utils/helpers/input.validation.ts
|
||||||
|
* @param {string} email - The email to validate
|
||||||
|
*/
|
||||||
|
export const isValidEmail = (email) => {
|
||||||
|
const EMAIL_REGEX =
|
||||||
|
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||||
|
return EMAIL_REGEX.test(email.trim());
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Validates a strong password field
|
||||||
|
* @file lib/utils/helpers/input.validation.ts
|
||||||
|
* @param {string} password - The password to validate
|
||||||
|
*/
|
||||||
|
export const isValidPasswordStrong = (password) => {
|
||||||
|
const strongRegex = new RegExp(
|
||||||
|
"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})"
|
||||||
|
);
|
||||||
|
|
||||||
|
return strongRegex.test(password.trim());
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Validates a medium password field
|
||||||
|
* @file lib/utils/helpers/input.validation.ts
|
||||||
|
* @param {string} password - The password to validate
|
||||||
|
*/
|
||||||
|
export const isValidPasswordMedium = (password) => {
|
||||||
|
const mediumRegex = new RegExp(
|
||||||
|
"^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})"
|
||||||
|
);
|
||||||
|
|
||||||
|
return mediumRegex.test(password.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether or not an object is empty.
|
||||||
|
* @param {Record<string, string>} obj - The object to test
|
||||||
|
* @returns `true` or `false`
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function isEmpty(obj) {
|
||||||
|
for (const _i in obj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Test whether or not an object is empty.
|
||||||
|
* @param {any} obj - The object to test
|
||||||
|
* @returns `true` or `false`
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function formatError(obj) {
|
||||||
|
const errors = [];
|
||||||
|
if (typeof obj === "object" && obj !== null) {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
obj.forEach((/** @type {Object} */ error) => {
|
||||||
|
Object.keys(error).map((k) => {
|
||||||
|
errors.push({
|
||||||
|
error: error[k],
|
||||||
|
id: Math.random() * 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.keys(obj).map((k) => {
|
||||||
|
errors.push({
|
||||||
|
error: obj[k],
|
||||||
|
id: Math.random() * 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push({
|
||||||
|
error: obj.charAt(0).toUpperCase() + obj.slice(1),
|
||||||
|
id: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
105
frontend/src/lib/utils/utils.js
Normal file
105
frontend/src/lib/utils/utils.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { quintOut } from "svelte/easing";
|
||||||
|
import { crossfade } from "svelte/transition";
|
||||||
|
|
||||||
|
export const [send, receive] = crossfade({
|
||||||
|
duration: (d) => Math.sqrt(d * 200),
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
fallback(node, params) {
|
||||||
|
const style = getComputedStyle(node);
|
||||||
|
const transform = style.transform === "none" ? "" : style.transform;
|
||||||
|
|
||||||
|
return {
|
||||||
|
duration: 600,
|
||||||
|
easing: quintOut,
|
||||||
|
css: (t) => `
|
||||||
|
transform: ${transform} scale(${t});
|
||||||
|
opacity: ${t}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates an email field
|
||||||
|
* @file lib/utils/helpers/input.validation.ts
|
||||||
|
* @param {string} email - The email to validate
|
||||||
|
*/
|
||||||
|
export const isValidEmail = (email) => {
|
||||||
|
const EMAIL_REGEX =
|
||||||
|
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||||
|
return EMAIL_REGEX.test(email.trim());
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Validates a strong password field
|
||||||
|
* @file lib/utils/helpers/input.validation.ts
|
||||||
|
* @param {string} password - The password to validate
|
||||||
|
*/
|
||||||
|
export const isValidPasswordStrong = (password) => {
|
||||||
|
const strongRegex = new RegExp(
|
||||||
|
"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})"
|
||||||
|
);
|
||||||
|
|
||||||
|
return strongRegex.test(password.trim());
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Validates a medium password field
|
||||||
|
* @file lib/utils/helpers/input.validation.ts
|
||||||
|
* @param {string} password - The password to validate
|
||||||
|
*/
|
||||||
|
export const isValidPasswordMedium = (password) => {
|
||||||
|
const mediumRegex = new RegExp(
|
||||||
|
"^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})"
|
||||||
|
);
|
||||||
|
|
||||||
|
return mediumRegex.test(password.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether or not an object is empty.
|
||||||
|
* @param {Record<string, string>} obj - The object to test
|
||||||
|
* @returns `true` or `false`
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function isEmpty(obj) {
|
||||||
|
for (const _i in obj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Test whether or not an object is empty.
|
||||||
|
* @param {any} obj - The object to test
|
||||||
|
* @returns `true` or `false`
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function formatError(obj) {
|
||||||
|
const errors = [];
|
||||||
|
if (typeof obj === "object" && obj !== null) {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
obj.forEach((/** @type {Object} */ error) => {
|
||||||
|
Object.keys(error).map((k) => {
|
||||||
|
errors.push({
|
||||||
|
error: error[k],
|
||||||
|
id: Math.random() * 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.keys(obj).map((k) => {
|
||||||
|
errors.push({
|
||||||
|
error: obj[k],
|
||||||
|
id: Math.random() * 1000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push({
|
||||||
|
error: obj.charAt(0).toUpperCase() + obj.slice(1),
|
||||||
|
id: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
5
frontend/src/routes/+layout.js
Normal file
5
frontend/src/routes/+layout.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('./$types').LayoutLoad} */
|
||||||
|
export async function load({ fetch, url, data }) {
|
||||||
|
const { user } = data;
|
||||||
|
return { fetch, url: url.pathname, user };
|
||||||
|
}
|
||||||
6
frontend/src/routes/+layout.server.js
Normal file
6
frontend/src/routes/+layout.server.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('./$types').LayoutServerLoad} */
|
||||||
|
export async function load({ locals }) {
|
||||||
|
return {
|
||||||
|
user: locals.user,
|
||||||
|
};
|
||||||
|
}
|
||||||
17
frontend/src/routes/+layout.svelte
Normal file
17
frontend/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
import Footer from "$lib/components/Footer.svelte";
|
||||||
|
import Header from "$lib/components/Header.svelte";
|
||||||
|
import Transition from "$lib/components/Transition.svelte";
|
||||||
|
import "$lib/css/styles.min.css";
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageData} */
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Transition key={data.url} duration={600}>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</Transition>
|
||||||
19
frontend/src/routes/+page.svelte
Normal file
19
frontend/src/routes/+page.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- <script>
|
||||||
|
import Developer from "$lib/img/hero-image.png";
|
||||||
|
</script> -->
|
||||||
|
|
||||||
|
<div class="hero-container">
|
||||||
|
<!-- <div class="hero-logo"><img src={Developer} alt="Alexander Stölting" /></div> -->
|
||||||
|
<h3 class="hero-subtitle subtitle">
|
||||||
|
This application is the demonstration of a series of tutorials on
|
||||||
|
session-based authentication using Go at the backend and JavaScript
|
||||||
|
(SvelteKit) on the front-end.
|
||||||
|
</h3>
|
||||||
|
<div class="hero-buttons-container">
|
||||||
|
<a
|
||||||
|
class="button-dark"
|
||||||
|
href="https://dev.to/sirneij/series/23239"
|
||||||
|
data-learn-more>Learn more</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
98
frontend/src/routes/auth/login/+page.server.js
Normal file
98
frontend/src/routes/auth/login/+page.server.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { BASE_API_URI } from "$lib/utils/constants";
|
||||||
|
import { formatError } from "$lib/utils/helpers";
|
||||||
|
import { fail, redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
|
export async function load({ locals }) {
|
||||||
|
// redirect user if logged in
|
||||||
|
if (locals.user) {
|
||||||
|
throw redirect(302, "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('./$types').Actions} */
|
||||||
|
export const actions = {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param request - The request object
|
||||||
|
* @param fetch - Fetch object from sveltekit
|
||||||
|
* @param cookies - SvelteKit's cookie object
|
||||||
|
* @returns Error data or redirects user to the home page or the previous page
|
||||||
|
*/
|
||||||
|
login: async ({ request, fetch, cookies }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const email = String(data.get("email"));
|
||||||
|
const password = String(data.get("password"));
|
||||||
|
const next = String(data.get("next"));
|
||||||
|
|
||||||
|
/** @type {RequestInit} */
|
||||||
|
const requestInitOptions = {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${BASE_API_URI}/users/login/`, requestInitOptions);
|
||||||
|
|
||||||
|
console.log("Login response status:", res.status);
|
||||||
|
console.log("Login response headers:", Object.fromEntries(res.headers));
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let errorMessage = `HTTP error! status: ${res.status}`;
|
||||||
|
try {
|
||||||
|
const errorData = await res.json();
|
||||||
|
errorMessage = errorData.error || errorMessage;
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error("Failed to parse error response:", parseError);
|
||||||
|
errorMessage = await res.text(); // Get the raw response text if JSON parsing fails
|
||||||
|
}
|
||||||
|
console.error("Login failed:", errorMessage);
|
||||||
|
return fail(res.status, {
|
||||||
|
errors: [{ error: errorMessage, id: Date.now() }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseBody = await res.json();
|
||||||
|
console.log("Login response body:", responseBody);
|
||||||
|
|
||||||
|
// Check for the cookie in the response headers
|
||||||
|
const setCookieHeader = res.headers.get("set-cookie");
|
||||||
|
console.log("Set-Cookie header:", setCookieHeader);
|
||||||
|
|
||||||
|
if (setCookieHeader) {
|
||||||
|
// Parse the Set-Cookie header to get the JWT
|
||||||
|
const jwtCookie = setCookieHeader.split(";")[0];
|
||||||
|
const [cookieName, cookieValue] = jwtCookie.split("=");
|
||||||
|
if (cookieName.trim() === "jwt") {
|
||||||
|
console.log("JWT cookie found in response");
|
||||||
|
cookies.set("jwt", cookieValue.trim(), {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("JWT cookie not found in response");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No Set-Cookie header in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Redirecting to:", next || "/");
|
||||||
|
throw redirect(303, next || "/");
|
||||||
|
},
|
||||||
|
// if (!res.ok) {
|
||||||
|
// const response = await res.json();
|
||||||
|
// const errors = formatError(response.error);
|
||||||
|
// return fail(400, { errors: errors });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// throw redirect(303, next || "/");
|
||||||
|
// },
|
||||||
|
};
|
||||||
76
frontend/src/routes/auth/login/+page.svelte
Normal file
76
frontend/src/routes/auth/login/+page.svelte
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script>
|
||||||
|
import { applyAction, enhance } from "$app/forms";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { receive, send } from "$lib/utils/helpers";
|
||||||
|
|
||||||
|
/** @type {import('./$types').ActionData} */
|
||||||
|
export let form;
|
||||||
|
|
||||||
|
/** @type {import('./$types').SubmitFunction} */
|
||||||
|
const handleLogin = async () => {
|
||||||
|
return async ({ result }) => {
|
||||||
|
await applyAction(result);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = "";
|
||||||
|
if ($page.url.searchParams.get("message")) {
|
||||||
|
message = $page.url.search.split("=")[1].replaceAll("%20", " ");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<form
|
||||||
|
class="content"
|
||||||
|
method="POST"
|
||||||
|
action="?/login"
|
||||||
|
use:enhance={handleLogin}
|
||||||
|
>
|
||||||
|
<h1 class="step-title">Login User</h1>
|
||||||
|
{#if form?.errors}
|
||||||
|
{#each form?.errors as error (error.id)}
|
||||||
|
<h4
|
||||||
|
class="step-subtitle warning"
|
||||||
|
in:receive={{ key: error.id }}
|
||||||
|
out:send={{ key: error.id }}
|
||||||
|
>
|
||||||
|
{error.error}
|
||||||
|
</h4>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if message}
|
||||||
|
<h4 class="step-subtitle">{message}</h4>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="next"
|
||||||
|
value={$page.url.searchParams.get("next")}
|
||||||
|
/>
|
||||||
|
<div class="input-box">
|
||||||
|
<span class="label">Email:</span>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="Email address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input-box">
|
||||||
|
<span class="label">Password:</span>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Password"
|
||||||
|
/>
|
||||||
|
<a href="/auth/password/request-change" style="margin-left: 1rem;"
|
||||||
|
>Forgot password?</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="btn-container">
|
||||||
|
<button class="button-dark">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
43
frontend/src/routes/auth/logout/+page.server.js
Normal file
43
frontend/src/routes/auth/logout/+page.server.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { BASE_API_URI } from "$lib/utils/constants";
|
||||||
|
import { fail, redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
|
export async function load({ locals }) {
|
||||||
|
// redirect user if not logged in
|
||||||
|
if (!locals.user) {
|
||||||
|
throw redirect(302, `/auth/login?next=/auth/logout`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import('./$types').Actions} */
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ fetch, cookies }) => {
|
||||||
|
/** @type {RequestInit} */
|
||||||
|
const requestInitOptions = {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Cookie: `jwt=${cookies.get("jwt")}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`${BASE_API_URI}/users/backend/logout/`,
|
||||||
|
requestInitOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const response = await res.json();
|
||||||
|
const errors = [];
|
||||||
|
errors.push({ error: response.error, id: 0 });
|
||||||
|
return fail(400, { errors: errors });
|
||||||
|
}
|
||||||
|
|
||||||
|
// eat the cookie
|
||||||
|
cookies.delete("jwt", { path: "/" });
|
||||||
|
|
||||||
|
// redirect the user
|
||||||
|
throw redirect(302, "/auth/login");
|
||||||
|
},
|
||||||
|
};
|
||||||
16
frontend/svelte.config.js
Normal file
16
frontend/svelte.config.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// import adapter from '@sveltejs/adapter-auto';
|
||||||
|
import adapter from '@sveltejs/adapter-vercel';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
|
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
|
adapter: adapter({
|
||||||
|
runtime: 'edge'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
9
frontend/vite.config.js
Normal file
9
frontend/vite.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user