summaryrefslogtreecommitdiffstats
path: root/apps/portal/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'apps/portal/src/app')
-rw-r--r--apps/portal/src/app/index.d.ts48
-rw-r--r--apps/portal/src/app/index.scss21
-rw-r--r--apps/portal/src/app/index.svelte61
-rw-r--r--apps/portal/src/app/index.ts14
-rw-r--r--apps/portal/src/app/pages/_layout.svelte142
-rw-r--r--apps/portal/src/app/pages/forgot.svelte99
-rw-r--r--apps/portal/src/app/pages/login.svelte145
-rw-r--r--apps/portal/src/app/pages/not-found.svelte23
-rw-r--r--apps/portal/src/app/pages/reset-password.svelte135
-rw-r--r--apps/portal/src/app/pages/sign-up.svelte128
10 files changed, 816 insertions, 0 deletions
diff --git a/apps/portal/src/app/index.d.ts b/apps/portal/src/app/index.d.ts
new file mode 100644
index 0000000..c044583
--- /dev/null
+++ b/apps/portal/src/app/index.d.ts
@@ -0,0 +1,48 @@
+/* Use this file to declare any custom file extensions for importing */
+/* Use this folder to also add/extend a package d.ts file, if needed. */
+
+/* CSS MODULES */
+declare module "*.module.css" {
+ const classes: { [key: string]: string };
+ export default classes;
+}
+declare module "*.module.scss" {
+ const classes: { [key: string]: string };
+ export default classes;
+}
+
+/* CSS */
+declare module "*.css";
+declare module "*.scss";
+
+/* IMAGES */
+declare module "*.svg" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.bmp" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.gif" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.jpg" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.jpeg" {
+ const ref: string;
+ export default ref;
+}
+declare module "*.png" {
+ const ref: string;
+ export default ref;
+}
+
+/* CUSTOM: ADD YOUR OWN HERE */
+declare module "*.svelte" {
+ const value: any;
+ export default value;
+}
diff --git a/apps/portal/src/app/index.scss b/apps/portal/src/app/index.scss
new file mode 100644
index 0000000..56ac1c0
--- /dev/null
+++ b/apps/portal/src/app/index.scss
@@ -0,0 +1,21 @@
+@use '../../web-shared/src/styles/base'as * with ($breakpoints: ('xs': "768px",
+ 'sm': "768px",
+ 'md': "1200px",
+ 'lg': "1200px",
+ 'xl': "1600px",
+ ),
+ $grid-columns: 12);
+
+@use '../../web-shared/src/styles/custom-style/colors';
+@use '../../web-shared/src/styles/custom-style/spacing';
+@use '../../web-shared/src/styles/custom-style/shared-styles';
+@use '../../web-shared/src/styles/custom-style/typography';
+@use '../../web-shared/src/styles/custom-style/icons';
+@use '../../web-shared/src/styles/custom-style/buttons';
+@use '../../web-shared/src/styles/custom-style/forms';
+@use '../../web-shared/src/styles/custom-style/util';
+
+@use '../../web-shared/src/styles/components/radios-checkboxes';
+@use '../../web-shared/src/styles/components/btn-states';
+@use '../../web-shared/src/styles/components/alert';
+@use '../../web-shared/src/styles/components/details';
diff --git a/apps/portal/src/app/index.svelte b/apps/portal/src/app/index.svelte
new file mode 100644
index 0000000..40fe6ae
--- /dev/null
+++ b/apps/portal/src/app/index.svelte
@@ -0,0 +1,61 @@
+<svelte:options immutable={true}/>
+<svelte:window bind:online={online}/>
+
+<script>
+ import {projects_base} from "$shared/lib/configuration";
+ import Router from "svelte-spa-router";
+ import {wrap} from "svelte-spa-router/wrap";
+ import {is_active} from "$shared/lib/session";
+ import NotFound from "$app/pages/not-found.svelte";
+ import SignUp from "$app/pages/sign-up.svelte";
+ import Login from "$app/pages/login.svelte";
+ import Forgot from "$app/pages/forgot.svelte";
+ import Reset from "$app/pages/reset-password.svelte";
+ import PreHeader from "$shared/components/pre-header.svelte";
+
+ let online = true;
+
+ async function user_is_logged_in() {
+ if (await is_active()) {
+ location.replace(projects_base("#/home"));
+ }
+ return true;
+ }
+
+ const routes = {
+ "/login": wrap({
+ component: Login,
+ conditions: [user_is_logged_in],
+ }),
+ "/": wrap({
+ component: Login,
+ conditions: [user_is_logged_in],
+ }),
+ "/signup": wrap({
+ component: SignUp,
+ conditions: [user_is_logged_in],
+ }),
+ "/reset-password": wrap({
+ component: Reset,
+ conditions: [user_is_logged_in],
+ }),
+ "/forgot": wrap({
+ component: Forgot,
+ conditions: [user_is_logged_in],
+ }),
+ "*": NotFound,
+ };
+</script>
+
+<PreHeader show="{!online}">You seem to be offline, please check your internet connection.</PreHeader>
+
+<Router
+ {routes}
+ restoreScrollState={true}
+ on:routeLoading={() => {
+ document.getElementById("loader").style.display = "inline-block";
+ }}
+ on:routeLoaded={() => {
+ document.getElementById("loader").style.display = "none";
+ }}
+/>
diff --git a/apps/portal/src/app/index.ts b/apps/portal/src/app/index.ts
new file mode 100644
index 0000000..0bfb30d
--- /dev/null
+++ b/apps/portal/src/app/index.ts
@@ -0,0 +1,14 @@
+import App from "./index.svelte";
+import "./index.scss";
+import {is_debug, is_development} from "$shared/lib/configuration";
+import {noop} from "$shared/lib/helpers";
+
+if (!is_development() && !is_debug()) {
+ console.log("%c Production; Suppressing logs", "background-color:yellow;color:black;font-size:18px;");
+ console.log = noop;
+}
+
+// @ts-ignore
+export default new App({
+ target: document.getElementById("root"),
+});
diff --git a/apps/portal/src/app/pages/_layout.svelte b/apps/portal/src/app/pages/_layout.svelte
new file mode 100644
index 0000000..8c2e4a8
--- /dev/null
+++ b/apps/portal/src/app/pages/_layout.svelte
@@ -0,0 +1,142 @@
+<script>
+ import Details from "$shared/components/details.svelte";
+ import Button from "$shared/components/button.svelte";
+ import {switch_theme} from "$shared/lib/helpers";
+</script>
+
+<style>
+ #decoration {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ #decoration svg {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ -webkit-transform: translateX(-50%);
+ transform: translateX(-50%);
+ width: 134%;
+ min-width: 1280px;
+ max-width: 1920px;
+ height: auto;
+ }
+</style>
+
+<main class="container-fluid padding-x-xs padding-x-xxl@xs padding-y-md padding-y-lg@md max-width-sm">
+ <slot/>
+
+ <Details summary="About">
+ <p>Time Tracker is a tool to keep track of time spent.</p>
+ <p>Use demo@demo.demo 123456 to demo the app.</p>
+ <a href="https://git.ivarlovlie.no/time-tracker">Source</a>
+ <a href="https://git.ivarlovlie.no/time-tracker/tree/LICENSE">License</a>
+ <a href="/assets/third-party-licenses.txt">License notices</a>
+ </Details>
+
+ <Details summary="Pricing"/>
+
+ <Details summary="Privacy policy">
+ <h3>Information we collect</h3>
+ <p>We collect information you the user provide, explicitly this means:</p>
+ <ul>
+ <li>Username</li>
+ <li>Password</li>
+ <li>Entries generated by you</li>
+ <li>Labels generated by you</li>
+ <li>Categories generated by you</li>
+ <li>Your IP address when making requests to our API (using the service)</li>
+ </ul>
+
+ <h3>How we use your information</h3>
+ <p>We use your information to provide the time-tracker service.</p>
+
+ <h3>How we share your information</h3>
+ <p>
+ We do not share your information with anyone nor any entity. All information is handled by us the provider and you the user
+ exclusively.
+ </p>
+
+ <h3>Right to delete</h3>
+ <p>
+ You can at any time delete any data related to your personal information by navigating to your profile page inside of the
+ service.
+ </p>
+
+ <h3>Right to inspect</h3>
+ <p>You can at any time download all of your generated data by navigating to your profile page inside of the service.</p>
+
+ <h3>Contact</h3>
+ <p>Please direct any inquires about your personal data to time-tracker@ivarlovlie.no.</p>
+ </Details>
+
+ <Details summary="Terms of service"/>
+
+ <Button on:click={() => switch_theme()}
+ text="Switch theme"
+ variant="secondary"/>
+
+ <figure id="decoration"
+ aria-hidden="true">
+ <svg class="color-contrast-higher opacity-10%"
+ viewBox="0 0 1920 450"
+ fill="none">
+ <g stroke="currentColor"
+ stroke-width="2">
+ <rect x="1286"
+ y="64"
+ width="128"
+ height="128"/>
+ <circle cx="1350"
+ cy="128"
+ r="64"/>
+ <path d="M1286 64L1414 192"/>
+ <circle cx="1478"
+ cy="128"
+ r="64"/>
+ <rect x="1414"
+ y="192"
+ width="128"
+ height="128"/>
+ <circle cx="1478"
+ cy="256"
+ r="64"/>
+ <path d="M1414 192L1542 320"/>
+ <circle cx="1606"
+ cy="256"
+ r="64"/>
+ <rect x="1542"
+ y="320"
+ width="128"
+ height="128"/>
+ <circle cx="1606"
+ cy="384"
+ r="64"/>
+ <path d="M1542 320L1670 448"/>
+ <rect x="1690"
+ y="192"
+ width="128"
+ height="128"/>
+ <circle cx="1754"
+ cy="256"
+ r="64"/>
+ <path d="M1690 192L1818 320"/>
+ <rect x="1542"
+ y="64"
+ width="128"
+ height="128"/>
+ <circle cx="1606"
+ cy="128"
+ r="64"/>
+ <path d="M1542 64L1670 192"/>
+ <circle cx="1478"
+ r="64"/>
+ </g>
+ </svg>
+ </figure>
+</main>
diff --git a/apps/portal/src/app/pages/forgot.svelte b/apps/portal/src/app/pages/forgot.svelte
new file mode 100644
index 0000000..f22d664
--- /dev/null
+++ b/apps/portal/src/app/pages/forgot.svelte
@@ -0,0 +1,99 @@
+<script>
+ import {onMount} from "svelte";
+ import {link} from "svelte-spa-router";
+ import {create_forgot_password_request} from "$shared/lib/api/user";
+ import {is_email} from "$shared/lib/helpers";
+ import Alert from "$shared/components/alert.svelte";
+ import Button from "$shared/components/button.svelte";
+ import Layout from "./_layout.svelte";
+
+ let isLoading = false;
+ let username;
+
+ const alert = {
+ title: "",
+ type: "",
+ message: "",
+ isVisible: false,
+ show(type, obj) {
+ alert.title = obj.title;
+ alert.message = obj.text;
+ alert.type = type;
+ alert.isVisible = true;
+ isLoading = false;
+ },
+ hide() {
+ alert.isVisible = false;
+ alert.title = "";
+ alert.message = "";
+ alert.type = "";
+ isLoading = false;
+ },
+ };
+
+ function is_valid() {
+ return is_email(username);
+ }
+
+ async function submit_form() {
+ if (isLoading) {
+ return;
+ }
+ if (is_valid()) {
+ isLoading = true;
+ const response = await create_forgot_password_request(username);
+ if (response.ok) {
+ alert.show("success", {
+ title: "Request is sent",
+ text: "If we find an account associated with this email address, you will receive an email with a reset link very soon.",
+ });
+ } else {
+ console.error(response.data);
+ alert.show("error", {
+ title: response.data?.title ?? "An error occured",
+ text: response.data?.text ?? "Please try again soon",
+ });
+ }
+ }
+ }
+
+ onMount(() => {
+ document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("email-address").focus();
+ });
+ });
+</script>
+
+<Layout>
+ <form on:submit|preventDefault={submit_form}
+ class="margin-bottom-md max-width-xxs">
+ <fieldset>
+ <legend class="form-legend">
+ <span class="margin-bottom-xs">Send reset link</span> <br/>
+ <span class="text-sm">... or <a href="/login"
+ use:link>log in</a></span>
+ </legend>
+ <div class="margin-bottom-xs">
+ <p>Provide your email address, and we'll send you a link to set your new password.</p>
+ </div>
+ <div class="margin-bottom-xxs max-width-xxs">
+ <Alert visible={alert.isVisible}
+ title={alert.title}
+ message={alert.message}
+ type={alert.type}/>
+ </div>
+ <div class="margin-bottom-xs">
+ <input type="email"
+ id="email-address"
+ placeholder="Email address"
+ class="form-control width-100%"
+ bind:value={username}/>
+ </div>
+ <div class="flex justify-end">
+ <Button text="Send reset link"
+ type="primary"
+ loading={isLoading}/>
+ </div>
+ </fieldset>
+ </form>
+</Layout>
diff --git a/apps/portal/src/app/pages/login.svelte b/apps/portal/src/app/pages/login.svelte
new file mode 100644
index 0000000..3324056
--- /dev/null
+++ b/apps/portal/src/app/pages/login.svelte
@@ -0,0 +1,145 @@
+<script>
+ import {onMount} from "svelte";
+ import {link, querystring} from "svelte-spa-router";
+ import {api_base, projects_base, IconNames} from "$shared/lib/configuration";
+ import Button from "$shared/components/button.svelte";
+ import Alert from "$shared/components/alert.svelte";
+ import {login} from "$shared/lib/api/user";
+ import {is_email} from "$shared/lib/helpers";
+ import Layout from "./_layout.svelte";
+
+ const loginForm = {
+ loading: false,
+ values: {
+ username: "",
+ password: "",
+ },
+ alert: {
+ title: "",
+ type: "",
+ message: "",
+ isVisible: false,
+ show(type, obj) {
+ loginForm.alert.title = obj.title;
+ loginForm.alert.message = obj.text;
+ loginForm.alert.type = type;
+ loginForm.alert.isVisible = true;
+ loginForm.loading = false;
+ },
+ hide() {
+ loginForm.alert.isVisible = false;
+ loginForm.alert.title = "";
+ loginForm.alert.message = "";
+ loginForm.alert.type = "";
+ },
+ },
+ is_valid() {
+ return is_email(loginForm.values.username) && loginForm.values.password.length > 0;
+ },
+ async submit_form() {
+ if (loginForm.loading) {
+ return;
+ }
+ if (loginForm.is_valid()) {
+ loginForm.alert.hide();
+ loginForm.loading = true;
+ try {
+ const response = await login(loginForm.values);
+ if (response.ok) {
+ location.replace(projects_base("#/home"));
+ } else {
+ if (response.data.title || response.data.text) {
+ loginForm.alert.show("error", {
+ title: response.data.title ?? "",
+ text: response.data.text ?? "",
+ });
+ } else {
+ loginForm.alert.show("error", {
+ title: "An unknown error occured",
+ text: "Try again soon",
+ });
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ loginForm.alert.show("error", {
+ title: "An error occured",
+ text: "Could not connect to server, please check your internet connection",
+ });
+ }
+ } else {
+ loginForm.alert.show("error", {
+ title: "Invalid form",
+ });
+ }
+ },
+ };
+
+ onMount(() => {
+ if ($querystring === "deleted") {
+ loginForm.alert.show("info", {
+ title: "Account deleted",
+ text: "Your account and all its data was successfully deleted.",
+ });
+ }
+ if ($querystring === "expired") {
+ loginForm.alert.show("info", {
+ title: "Session expired",
+ text: "Your session has expired, feel free to log in again.",
+ });
+ }
+ });
+</script>
+
+<Layout>
+ <form on:submit|preventDefault={loginForm.submit_form}
+ class="margin-bottom-md max-width-xxs">
+ <fieldset>
+ <legend class="form-legend">
+ <span class="margin-bottom-xs">Log into your account</span>
+ <br/>
+ <span class="text-sm">... or <a href="/signup"
+ use:link>create a new one</a></span>
+ </legend>
+ <div class="margin-bottom-xxs max-width-xxs">
+ <Alert visible={loginForm.alert.isVisible}
+ title={loginForm.alert.title}
+ message={loginForm.alert.message}
+ type={loginForm.alert.type}/>
+ </div>
+ <div class="margin-bottom-xxs">
+ <input type="email"
+ placeholder="Email address"
+ class="form-control width-100%"
+ id="username"
+ bind:value={loginForm.values.username}/>
+ </div>
+ <div class="margin-bottom-xxs">
+ <input type="password"
+ placeholder="Password"
+ id="password"
+ class="form-control width-100%"
+ bind:value={loginForm.values.password}/>
+ <div class="flex justify-end">
+ <a tabindex="-1"
+ class="text-sm"
+ href="/forgot"
+ use:link>Reset password</a>
+ </div>
+ </div>
+ <div class="flex justify-between">
+ <Button text="Login with Github"
+ variant="secondary"
+ icon="{IconNames.github}"
+ icon_right_aligned="true"
+ href={api_base("_/account/create-github-session")}
+ loading={loginForm.loading}
+ />
+ <Button text="Login"
+ type="submit"
+ variant="primary"
+ loading={loginForm.loading}/>
+ </div>
+ </fieldset>
+ </form>
+</Layout>
diff --git a/apps/portal/src/app/pages/not-found.svelte b/apps/portal/src/app/pages/not-found.svelte
new file mode 100644
index 0000000..34568ba
--- /dev/null
+++ b/apps/portal/src/app/pages/not-found.svelte
@@ -0,0 +1,23 @@
+<script>
+ import {link} from "svelte-spa-router";
+</script>
+<style>
+ header {
+ font-size: 12rem;
+ }
+
+ main {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+ }
+</style>
+
+<main>
+ <header>404</header>
+ <p>Page not found!</p>
+ <a use:link
+ href="/">Go to front</a>
+</main>
diff --git a/apps/portal/src/app/pages/reset-password.svelte b/apps/portal/src/app/pages/reset-password.svelte
new file mode 100644
index 0000000..56c4f62
--- /dev/null
+++ b/apps/portal/src/app/pages/reset-password.svelte
@@ -0,0 +1,135 @@
+<script>
+ import {querystring, link} from "svelte-spa-router";
+ import {check_forgot_password_request, fulfill_forgot_password_request} from "$shared/lib/api/user";
+ import Alert from "$shared/components/alert.svelte";
+ import Button from "$shared/components/button.svelte";
+ import Layout from "./_layout.svelte";
+
+ const requestId = new URLSearchParams($querystring).get("id");
+ let isLoading = false;
+ let newPassword;
+ let newPasswordError;
+ let alert = {
+ title: "",
+ type: "",
+ message: "",
+ isVisible: false,
+ show(type, obj) {
+ alert.title = obj.title;
+ alert.message = obj.text;
+ alert.type = type;
+ alert.isVisible = true;
+ isLoading = false;
+ },
+ hide() {
+ alert.isVisible = false;
+ alert.title = "";
+ alert.message = "";
+ alert.type = "";
+ isLoading = false;
+ },
+ };
+
+ function is_valid() {
+ let isValid = true;
+ if (!newPassword.length > 5) {
+ newPasswordError = "The new password must be at least 5 characters";
+ isValid = false;
+ }
+ return isValid;
+ }
+
+ async function submit() {
+ if (isLoading) {
+ return;
+ }
+ if (is_valid()) {
+ isLoading = true;
+ const response = await fulfill_forgot_password_request(requestId, newPassword);
+ if (response.ok) {
+ alert.show("success", {
+ title: "Your new password is set",
+ text: "<a href='/#/login'>Click here to log in</a>",
+ });
+ } else {
+ console.error(response.data);
+ alert.show("error", {
+ title: response.data?.title ?? "An error occured",
+ text: response.data?.text ?? "Please try again soon",
+ });
+ }
+ }
+ }
+
+ async function is_valid_password_reset_request() {
+ const response = await check_forgot_password_request(requestId);
+ if (response.ok) {
+ return response.data === true;
+ }
+ return false;
+ }
+</script>
+
+<Layout>
+ <form on:submit|preventDefault={submit}
+ class="margin-bottom-md max-width-xxs {isLoading ? 'c-disabled loading' : ''}">
+ {#if requestId}
+ {#await is_valid_password_reset_request()}
+ <p>Checking your request...</p>
+ <a href="/login"
+ use:link>cancel</a>
+ {:then isActive}
+ {#if isActive === true}
+ <fieldset>
+ <legend class="form-legend">
+ <span class="margin-bottom-xs">Set your new password</span> <br/>
+ <span class="text-sm">
+ ... or
+ <a href="/login"
+ use:link> log in </a>
+ </span>
+ </legend>
+ <div class="margin-bottom-xxs max-width-xxs">
+ <Alert visible={alert.isVisible}
+ title={alert.title}
+ message={alert.message}
+ type={alert.type}/>
+ </div>
+ <div class="margin-bottom-xs">
+ <input
+ type="password"
+ id="new-password"
+ placeholder="New password"
+ class="form-control width-100%"
+ bind:value={newPassword}
+ />
+ {#if newPasswordError}
+ <small class="color-danger">{newPasswordError}</small>
+ {/if}
+ </div>
+ <div class="flex justify-end">
+ <Button text="Set new password"
+ type="primary"
+ loading={isLoading}
+ on:click={submit}/>
+ </div>
+ </fieldset>
+ {:else}
+ <Alert title="This request is expired"
+ message="Please submit the forgot password form again"
+ type="warning"/>
+ <div class="flex justify-between width-100% margin-y-sm">
+ <a href="/forgot"
+ use:link>Go to forgot form</a>
+ <a href="/login"
+ use:link>Go to login form</a>
+ </div>
+ {/if}
+ {:catch _}
+ <Alert title="An error occured"
+ message="Please try again soon"
+ type="error"/>
+ {/await}
+ {/if}
+ </form>
+</Layout>
diff --git a/apps/portal/src/app/pages/sign-up.svelte b/apps/portal/src/app/pages/sign-up.svelte
new file mode 100644
index 0000000..80780e0
--- /dev/null
+++ b/apps/portal/src/app/pages/sign-up.svelte
@@ -0,0 +1,128 @@
+<script>
+ import {create_account} from "$shared/lib/api/user";
+ import {is_email} from "$shared/lib/helpers";
+ import Alert from "$shared/components/alert.svelte";
+ import Button from "$shared/components/button.svelte";
+ import {link} from "svelte-spa-router";
+ import Layout from "./_layout.svelte";
+
+ const signupForm = {
+ loading: false,
+ values: {
+ username: "",
+ password: "",
+ },
+ alert: {
+ title: "",
+ type: "",
+ message: "",
+ isVisible: false,
+ show(type, obj) {
+ signupForm.alert.title = obj.title;
+ signupForm.alert.message = obj.text;
+ signupForm.alert.type = type;
+ signupForm.alert.isVisible = true;
+ signupForm.loading = false;
+ },
+ hide() {
+ signupForm.alert.isVisible = false;
+ signupForm.alert.title = "";
+ signupForm.alert.message = "";
+ signupForm.alert.type = "";
+ },
+ },
+ is_valid() {
+ return (
+ is_email(signupForm.values.username) &&
+ signupForm.values.password.length > 0
+ );
+ },
+ async submit_form() {
+ if (signupForm.loading) {
+ return;
+ }
+ if (signupForm.is_valid()) {
+ signupForm.alert.hide();
+ signupForm.loading = true;
+ try {
+ const response = await create_account(signupForm.values);
+ if (response.ok) {
+ location.reload();
+ } else {
+ if (response.data.title || response.data.text) {
+ signupForm.alert.show("error", {
+ title: response.data.title ?? "",
+ text: response.data.text ?? "",
+ });
+ } else {
+ signupForm.alert.show("error", {
+ title: "An unknown error occured",
+ text: "Try again soon",
+ });
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ signupForm.alert.show("error", {
+ title: "An error occured",
+ text: "Could not connect to server, please check your internet connection",
+ });
+ }
+ } else {
+ signupForm.alert.show("error", {
+ title: "Invalid form",
+ });
+ }
+ },
+ };
+</script>
+
+<Layout>
+ <form
+ on:submit|preventDefault={signupForm.submit_form}
+ class="margin-bottom-md max-width-xxs"
+ >
+ <fieldset>
+ <legend class="form-legend">
+ <span class="margin-bottom-xs">Create your account</span> <br/>
+ <span class="text-sm"
+ >... or <a href="/login"
+ use:link>log in</a></span
+ >
+ </legend>
+ <div class="margin-bottom-xxs max-width-xxs">
+ <Alert
+ visible={signupForm.alert.isVisible}
+ title={signupForm.alert.title}
+ message={signupForm.alert.message}
+ type={signupForm.alert.type}
+ />
+ </div>
+ <div class="margin-bottom-xxs">
+ <input
+ type="email"
+ placeholder="Email address"
+ class="form-control width-100%"
+ id="email-address"
+ bind:value={signupForm.values.username}
+ />
+ </div>
+ <div class="margin-bottom-xxs">
+ <input
+ type="password"
+ placeholder="Password"
+ class="form-control width-100%"
+ bind:value={signupForm.values.password}
+ />
+ </div>
+ <div class="flex justify-end">
+ <Button
+ class="margin-bottom-xs"
+ text="Submit"
+ type="primary"
+ loading={signupForm.loading}
+ />
+ </div>
+ </fieldset>
+ </form>
+</Layout>