From 147b8c0afdb7d2fe6d86fb310fb7308dd2cebfe7 Mon Sep 17 00:00:00 2001 From: "$(pass /github/name)" <$(pass /github/email)> Date: Sat, 7 Sep 2024 13:36:15 +0200 Subject: [PATCH] frontend: initial commit --- frontend/README.md | 12 + frontend/src/app.d.ts | 64 +++ frontend/src/app.html | 18 + frontend/src/hooks.server.js | 58 ++ frontend/src/index.test.js | 7 + frontend/src/lib/components/Footer.svelte | 24 + frontend/src/lib/components/Header.svelte | 79 +++ frontend/src/lib/components/Transition.svelte | 14 + frontend/src/lib/css/styles.css | 521 ++++++++++++++++++ frontend/src/lib/css/styles.min.css | 505 +++++++++++++++++ frontend/src/lib/utils/constants.js | 3 + frontend/src/lib/utils/helpers.js | 105 ++++ frontend/src/lib/utils/utils.js | 105 ++++ frontend/src/routes/+layout.js | 5 + frontend/src/routes/+layout.server.js | 6 + frontend/src/routes/+layout.svelte | 17 + frontend/src/routes/+page.svelte | 19 + .../src/routes/auth/login/+page.server.js | 98 ++++ frontend/src/routes/auth/login/+page.svelte | 76 +++ .../src/routes/auth/logout/+page.server.js | 43 ++ frontend/svelte.config.js | 16 + frontend/vite.config.js | 9 + 22 files changed, 1804 insertions(+) create mode 100644 frontend/README.md create mode 100644 frontend/src/app.d.ts create mode 100644 frontend/src/app.html create mode 100644 frontend/src/hooks.server.js create mode 100644 frontend/src/index.test.js create mode 100644 frontend/src/lib/components/Footer.svelte create mode 100644 frontend/src/lib/components/Header.svelte create mode 100644 frontend/src/lib/components/Transition.svelte create mode 100644 frontend/src/lib/css/styles.css create mode 100644 frontend/src/lib/css/styles.min.css create mode 100644 frontend/src/lib/utils/constants.js create mode 100644 frontend/src/lib/utils/helpers.js create mode 100644 frontend/src/lib/utils/utils.js create mode 100644 frontend/src/routes/+layout.js create mode 100644 frontend/src/routes/+layout.server.js create mode 100644 frontend/src/routes/+layout.svelte create mode 100644 frontend/src/routes/+page.svelte create mode 100644 frontend/src/routes/auth/login/+page.server.js create mode 100644 frontend/src/routes/auth/login/+page.svelte create mode 100644 frontend/src/routes/auth/logout/+page.server.js create mode 100644 frontend/svelte.config.js create mode 100644 frontend/vite.config.js diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..3a84f18 --- /dev/null +++ b/frontend/README.md @@ -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 +``` diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 0000000..d9faa97 --- /dev/null +++ b/frontend/src/app.d.ts @@ -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 {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 0000000..8705da0 --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,18 @@ + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/hooks.server.js b/frontend/src/hooks.server.js new file mode 100644 index 0000000..f9c5ea9 --- /dev/null +++ b/frontend/src/hooks.server.js @@ -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); +} diff --git a/frontend/src/index.test.js b/frontend/src/index.test.js new file mode 100644 index 0000000..e07cbbd --- /dev/null +++ b/frontend/src/index.test.js @@ -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); + }); +}); diff --git a/frontend/src/lib/components/Footer.svelte b/frontend/src/lib/components/Footer.svelte new file mode 100644 index 0000000..104da8b --- /dev/null +++ b/frontend/src/lib/components/Footer.svelte @@ -0,0 +1,24 @@ + + + diff --git a/frontend/src/lib/components/Header.svelte b/frontend/src/lib/components/Header.svelte new file mode 100644 index 0000000..ea54c67 --- /dev/null +++ b/frontend/src/lib/components/Header.svelte @@ -0,0 +1,79 @@ + + +
+
+
+
+ + +
+
+
+
+ home +
+ {#if !$page.data.user} +
+ login +
+ + {:else} +
+ + {`${$page.data.user.first_name} + +
+ +
{ + return async ({ result }) => { + await applyAction(result); + }; + }} + > + +
+ {/if} +
+
+
diff --git a/frontend/src/lib/components/Transition.svelte b/frontend/src/lib/components/Transition.svelte new file mode 100644 index 0000000..e7f2d51 --- /dev/null +++ b/frontend/src/lib/components/Transition.svelte @@ -0,0 +1,14 @@ + + +{#key key} +
+ +
+{/key} diff --git a/frontend/src/lib/css/styles.css b/frontend/src/lib/css/styles.css new file mode 100644 index 0000000..366084f --- /dev/null +++ b/frontend/src/lib/css/styles.css @@ -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); + } +} diff --git a/frontend/src/lib/css/styles.min.css b/frontend/src/lib/css/styles.min.css new file mode 100644 index 0000000..4174e87 --- /dev/null +++ b/frontend/src/lib/css/styles.min.css @@ -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); + } +} diff --git a/frontend/src/lib/utils/constants.js b/frontend/src/lib/utils/constants.js new file mode 100644 index 0000000..43db098 --- /dev/null +++ b/frontend/src/lib/utils/constants.js @@ -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; diff --git a/frontend/src/lib/utils/helpers.js b/frontend/src/lib/utils/helpers.js new file mode 100644 index 0000000..0c73b18 --- /dev/null +++ b/frontend/src/lib/utils/helpers.js @@ -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} 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; +} diff --git a/frontend/src/lib/utils/utils.js b/frontend/src/lib/utils/utils.js new file mode 100644 index 0000000..0c73b18 --- /dev/null +++ b/frontend/src/lib/utils/utils.js @@ -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} 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; +} diff --git a/frontend/src/routes/+layout.js b/frontend/src/routes/+layout.js new file mode 100644 index 0000000..88edb1d --- /dev/null +++ b/frontend/src/routes/+layout.js @@ -0,0 +1,5 @@ +/** @type {import('./$types').LayoutLoad} */ +export async function load({ fetch, url, data }) { + const { user } = data; + return { fetch, url: url.pathname, user }; +} diff --git a/frontend/src/routes/+layout.server.js b/frontend/src/routes/+layout.server.js new file mode 100644 index 0000000..d76e6b4 --- /dev/null +++ b/frontend/src/routes/+layout.server.js @@ -0,0 +1,6 @@ +/** @type {import('./$types').LayoutServerLoad} */ +export async function load({ locals }) { + return { + user: locals.user, + }; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..fbdbf8c --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,17 @@ + + + +
+ + + +