aboutsummaryrefslogtreecommitdiffstats
path: root/code/app/src/routes/(main)/(public)
diff options
context:
space:
mode:
Diffstat (limited to 'code/app/src/routes/(main)/(public)')
-rw-r--r--code/app/src/routes/(main)/(public)/+layout.svelte18
-rw-r--r--code/app/src/routes/(main)/(public)/portal/+page.svelte26
-rw-r--r--code/app/src/routes/(main)/(public)/portal/+page.ts9
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/+page.svelte81
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte82
-rw-r--r--code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/+page.svelte155
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/+page.ts11
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/index.spec.js12
-rw-r--r--code/app/src/routes/(main)/(public)/sign-in/index.ts20
-rw-r--r--code/app/src/routes/(main)/(public)/sign-up/+page.svelte106
-rw-r--r--code/app/src/routes/(main)/(public)/sign-up/+page.ts11
14 files changed, 564 insertions, 0 deletions
diff --git a/code/app/src/routes/(main)/(public)/+layout.svelte b/code/app/src/routes/(main)/(public)/+layout.svelte
new file mode 100644
index 0000000..6da653c
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/+layout.svelte
@@ -0,0 +1,18 @@
+<script>
+ import { LocaleSwitcher } from "$components";
+ import LL from "$i18n/i18n-svelte";
+</script>
+
+<LocaleSwitcher tabindex={-1} />
+<slot />
+<footer class="grid sm:gap-5 grid-flow-row sm:justify-center px-2 sm:grid-flow-col">
+ <a href="https://greatoffice.life/privacy" class="link">
+ {$LL.privacyPolicy()}
+ </a>
+ <a href="https://greatoffice.life/terms" class="link">
+ {$LL.tos()}
+ </a>
+ <a href="https://greatoffice.life/docs" class="link">
+ {$LL.documentation()}
+ </a>
+</footer>
diff --git a/code/app/src/routes/(main)/(public)/portal/+page.svelte b/code/app/src/routes/(main)/(public)/portal/+page.svelte
new file mode 100644
index 0000000..b363e4b
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/portal/+page.svelte
@@ -0,0 +1,26 @@
+<script lang="ts">
+ import { onMount } from "svelte";
+ import type { PageData } from "./$types";
+ import type { PortalMessage } from "$configuration";
+ import { goto } from "$app/navigation";
+ import { sgs } from "$utilities/global-state";
+
+ export let data: PageData;
+
+ onMount(async () => {
+ switch (data.message as PortalMessage) {
+ case "emailValidated": {
+ sgs("showEmailValidatedAlertWhenLoggedIn", true);
+ await goto("/home");
+ break;
+ }
+ default: {
+ await goto("/home");
+ }
+ }
+ });
+</script>
+
+<div class="p-3">
+ <h1>Warping...</h1>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/portal/+page.ts b/code/app/src/routes/(main)/(public)/portal/+page.ts
new file mode 100644
index 0000000..72338cb
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/portal/+page.ts
@@ -0,0 +1,9 @@
+import type { PortalMessage } from '$configuration';
+import { redirect } from '@sveltejs/kit';
+import type { PageLoad } from './$types';
+
+export const load: PageLoad = async ({ url }) => {
+ const message = url.searchParams.get("msg") as PortalMessage;
+ if (!message) throw redirect(302, "/");
+ return { message };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/reset-password/+page.svelte b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
new file mode 100644
index 0000000..a45ccdd
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/+page.svelte
@@ -0,0 +1,81 @@
+<script lang="ts">
+ import { Alert, Input, Button } from "$components";
+ import LL from "$i18n/i18n-svelte";
+ import { FormError } from "$models/internal/FormError";
+ import { PasswordResetService } from "$services/password-reset-service";
+
+ const formData = {
+ email: {
+ value: "",
+ errors: [],
+ },
+ };
+
+ const formError = new FormError();
+ const passwordResetService = PasswordResetService.resolve();
+
+ let loading = false;
+ let showSuccessAlert = false;
+ let showErrorAlert = false;
+
+ async function submit_form_async() {
+ formError.set();
+ showSuccessAlert = false;
+ showErrorAlert = false;
+ loading = true;
+ const response = await passwordResetService.create_request_async(formData.email.value);
+ loading = false;
+ if (response.isCreated) {
+ showSuccessAlert = true;
+ } else if (response.knownProblem) {
+ formError.set_from_known_problem(response.knownProblem);
+ for (const error of Object.entries(response.knownProblem.errors)) {
+ if (error[0] === "email") {
+ let errors = [];
+ error[1].forEach((e) => errors.push(e));
+ formData.email.errors = errors;
+ }
+ }
+ } else {
+ formError.set($LL.unexpectedError(), $LL.tryAgainSoon());
+ }
+ showErrorAlert = formError.has_error() && !showSuccessAlert;
+ }
+</script>
+
+<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
+ <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
+ <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
+ {$LL.resetPasswordPage.requestAPasswordReset()}
+ </h2>
+ <p class="mt-2 text-sm text-gray-600">
+ {$LL.or().toLowerCase()}
+ <a href="/sign-in" class="link">
+ {$LL.signIntoYourAccount().toLowerCase()}
+ </a>
+ </p>
+ </div>
+
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
+ <form class="space-y-6" on:submit|preventDefault={submit_form_async}>
+ {#if showErrorAlert}
+ <Alert title={formError.title} message={formError.subtitle} type="error" />
+ {:else if showSuccessAlert}
+ <Alert type="success" title={$LL.success()} message={$LL.resetPasswordPage.requestSentMessage()} />
+ {/if}
+ <Input
+ id="email"
+ name="email"
+ type="email"
+ autocomplete="email"
+ errors={formData.email.errors}
+ bind:value={formData.email.value}
+ required
+ label={$LL.emailAddress()}
+ />
+ <Button text={$LL.submit()} type="submit" {loading} fullWidth />
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/reset-password/+page.ts b/code/app/src/routes/(main)/(public)/reset-password/+page.ts
new file mode 100644
index 0000000..c0859e0
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/+page.ts
@@ -0,0 +1,11 @@
+import LL from '$i18n/i18n-svelte';
+import { get } from 'svelte/store';
+import type { PageLoad } from './$types';
+
+const l = get(LL);
+
+export const load: PageLoad = async () => {
+ return {
+ title: l.resetPasswordPage.title(),
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts
new file mode 100644
index 0000000..22fa29d
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.server.ts
@@ -0,0 +1,11 @@
+import { is_guid } from "$utilities/validators";
+import { redirect } from "@sveltejs/kit";
+import type { PageServerLoad } from "./$types";
+
+export const load: PageServerLoad = async ({ params }) => {
+ const resetRequestId = params.id ?? "";
+ if (!is_guid(resetRequestId)) throw redirect(302, "/reset-password");
+ return {
+ resetRequestId,
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte
new file mode 100644
index 0000000..27a1af5
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.svelte
@@ -0,0 +1,82 @@
+<script lang="ts">
+ import { onMount } from "svelte";
+ import LL from "$i18n/i18n-svelte";
+ import { Alert, Input, Button } from "$components";
+ import type { PageServerData } from "./$types";
+ import { goto } from "$app/navigation";
+ import { SignInPageMessage, signInPageMessageQueryKey } from "$routes/(main)/(public)/sign-in";
+ import { PasswordResetService } from "$services/password-reset-service";
+
+ export let data: PageServerData;
+ const passwordResetService = PasswordResetService.resolve();
+
+ const formData = {
+ newPassword: {
+ value: "",
+ errors: [],
+ },
+ };
+
+ let finishedPreliminaryLoading = false;
+ let loading = false;
+ let canSubmit = true;
+ let requestIsInvalid = false;
+
+ async function submitFormAsync() {
+ if (!canSubmit) return;
+ loading = true;
+ const request = await passwordResetService.fulfill_request_async(data.resetRequestId, formData.newPassword.value);
+ if (request.isFulfilled) {
+ goto("/sign-in?" + signInPageMessageQueryKey + "=" + SignInPageMessage.AFTER_PASSWORD_RESET);
+ } else if (request.knownProblem) {
+ }
+ loading = false;
+ }
+
+ onMount(async () => {
+ const response = await passwordResetService.request_is_valid_async(data.resetRequestId);
+ requestIsInvalid = !response.isValid;
+ finishedPreliminaryLoading = true;
+ });
+</script>
+
+<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
+ {#if finishedPreliminaryLoading}
+ <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
+ <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
+ {$LL.resetPasswordPage.setANewPassword()}
+ </h2>
+ <p class="mt-2 text-sm text-gray-600">
+ {$LL.or().toLowerCase()}
+ <a href="/sign-in" class="link">
+ {$LL.signIntoYourAccount().toLowerCase()}
+ </a>
+ </p>
+ </div>
+
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
+ <form class="space-y-6" on:submit|preventDefault={submitFormAsync}>
+ {#if requestIsInvalid}
+ <Alert
+ title={$LL.resetPasswordPage.invalidRequestTitle()}
+ message={$LL.resetPasswordPage.invalidRequestMessage()}
+ />
+ {/if}
+ <Input
+ id="password"
+ name="password"
+ type="password"
+ autocomplete="new-password"
+ required
+ bind:value={formData.newPassword.value}
+ label={$LL.resetPasswordPage.newPassword()}
+ />
+ <Button text={$LL.submit()} type="submit" {loading} fullWidth />
+ </form>
+ </div>
+ </div>
+ {:else}
+ <p>Checking your request...</p>
+ {/if}
+</div>
diff --git a/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts
new file mode 100644
index 0000000..3252b7a
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/reset-password/[id]/+page.ts
@@ -0,0 +1,11 @@
+import LL from '$i18n/i18n-svelte';
+import { get } from 'svelte/store';
+import type { PageLoad } from './$types';
+
+const l = get(LL);
+
+export const load: PageLoad = async () => {
+ return {
+ title: l.resetPasswordPage.fulfillTitle(),
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/sign-in/+page.svelte b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
new file mode 100644
index 0000000..66d4575
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-in/+page.svelte
@@ -0,0 +1,155 @@
+<script lang="ts">
+ import { goto } from "$app/navigation";
+ import { Button, Checkbox, Input, Alert } from "$components";
+ import LL from "$i18n/i18n-svelte";
+ import pwKey from "$actions/pwKey";
+ import { onMount } from "svelte";
+ import { signInPageMessageQueryKey, signInPageTestKeys, type SignInPageMessage } from ".";
+ import { AccountService } from "$services/account-service";
+ import type { LoginPayload } from "$services/abstractions/IAccountService";
+ import { FormError } from "$models/internal/FormError";
+ import type { IForm } from "$models/internal/IForm";
+
+ let messageType: SignInPageMessage | undefined = undefined;
+
+ const accountService = AccountService.resolve();
+ const form = {
+ fields: {
+ username: {
+ value: "",
+ errors: [],
+ },
+ password: {
+ value: "",
+ errors: [],
+ },
+ persist: {
+ value: false,
+ errors: [],
+ },
+ },
+ error: new FormError(),
+ isLoading: false,
+ showError: false,
+ get_payload(): LoginPayload {
+ return {
+ password: form.fields.password.value,
+ username: form.fields.username.value,
+ persist: !form.fields.persist.value,
+ };
+ },
+ async submit_async() {
+ console.log("sadf");
+ form.error.set();
+ form.showError = form.error.has_error();
+ form.isLoading = true;
+ const loginResponse = await accountService.login_async(form.get_payload());
+ if (loginResponse.isLoggedIn) {
+ await goto("/home");
+ } else if (loginResponse.knownProblem) {
+ form.error.set_from_known_problem(loginResponse.knownProblem);
+ } else {
+ form.error.set($LL.unexpectedError(), $LL.tryAgainSoon());
+ }
+ form.isLoading = false;
+ form.showError = form.error.has_error();
+ },
+ } as IForm;
+
+ onMount(() => {
+ const queryParams = new URLSearchParams(window.location.search);
+ if (queryParams.get(signInPageMessageQueryKey)) {
+ messageType = queryParams.get(signInPageMessageQueryKey) as SignInPageMessage;
+ queryParams.delete(signInPageMessageQueryKey);
+ window.history.replaceState(null, "", window.location.origin + window.location.pathname);
+ }
+ });
+</script>
+
+<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
+ {#if messageType}
+ <div class="sm:max-w-md sm:mx-auto sm:w-full">
+ {#if messageType === "after-password-reset"}
+ <Alert
+ title={$LL.signInPage.yourNewPasswordIsApplied()}
+ _pwKey={signInPageTestKeys.afterPasswordResetAlert}
+ message={$LL.signInPage.signInBelow()}
+ closeable
+ />
+ {:else if messageType === "user-disabled"}
+ <Alert
+ title={$LL.signInPage.yourAccountIsDisabled()}
+ _pwKey={signInPageTestKeys.userDisabledAlert}
+ message={$LL.signInPage.contactYourAdminIfDisabled()}
+ closeable
+ />
+ {:else if messageType === "user-inactivity"}
+ <Alert
+ title={$LL.signInPage.youHaveReachedInactivityLimit()}
+ _pwKey={signInPageTestKeys.userInactivityAlert}
+ message={$LL.signInPage.feelFreeToSignInAgain()}
+ closeable
+ />
+ {/if}
+ </div>
+ {/if}
+ <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
+ <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
+ {$LL.signInPage.signIn()}
+ </h2>
+ <p class="mt-2 text-sm text-gray-600">
+ {$LL.or().toLowerCase()}
+ <a href="/sign-up" use:pwKey={signInPageTestKeys.signUpAnchor} class="link">
+ {$LL.createANewAccount().toLowerCase()}
+ </a>
+ </p>
+ </div>
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
+ {#if form.showError}
+ <Alert title={form.error.title} message={form.error.subtitle} type="error" _pwKey={signInPageTestKeys.formErrorAlert} />
+ {/if}
+ <form class="space-y-6 mt-2" use:pwKey={signInPageTestKeys.signInForm} on:submit|preventDefault={() => form.submit_async()}>
+ <Input
+ id="username"
+ _pwKey={signInPageTestKeys.usernameInput}
+ name="username"
+ type="email"
+ label={$LL.emailAddress()}
+ required
+ errors={form.fields.username.errors}
+ bind:value={form.fields.username.value}
+ />
+
+ <Input
+ id="password"
+ name="password"
+ type="password"
+ label={$LL.password()}
+ _pwKey={signInPageTestKeys.passwordInput}
+ autocomplete="current-password"
+ required
+ errors={form.fields.password.errors}
+ bind:value={form.fields.password.value}
+ />
+
+ <div class="flex items-center justify-between">
+ <Checkbox
+ id="remember-me"
+ _pwKey={signInPageTestKeys.rememberMeCheckbox}
+ name="remember-me"
+ bind:checked={form.fields.persist.value}
+ label={$LL.signInPage.notMyComputer()}
+ />
+ <div class="text-sm">
+ <a href="/reset-password" class="link" use:pwKey={signInPageTestKeys.resetPasswordAnchor}>
+ {$LL.signInPage.resetPassword()}
+ </a>
+ </div>
+ </div>
+
+ <Button text={$LL.submit()} fullWidth type="submit" loading={form.isLoading} />
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/sign-in/+page.ts b/code/app/src/routes/(main)/(public)/sign-in/+page.ts
new file mode 100644
index 0000000..bebc459
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-in/+page.ts
@@ -0,0 +1,11 @@
+import LL from '$i18n/i18n-svelte';
+import { get } from 'svelte/store';
+import type { PageLoad } from './$types';
+
+const l = get(LL);
+
+export const load: PageLoad = async () => {
+ return {
+ title: l.signInPage.title(),
+ };
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/sign-in/index.spec.js b/code/app/src/routes/(main)/(public)/sign-in/index.spec.js
new file mode 100644
index 0000000..9d0122d
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-in/index.spec.js
@@ -0,0 +1,12 @@
+import { test, expect } from "@playwright/test";
+import { signInPageTestKeys } from "./index.js";
+import { get_test_context } from "$configuration";
+import { get_pw_key_selector } from "$utilities/testing-helpers";
+
+const context = get_test_context();
+
+test("form loads", async ({ page }) => {
+ page.goto("/sign-in");
+ const form = page.locator(get_pw_key_selector(signInPageTestKeys.signInForm));
+ expect(form.isVisible()).toBeTruthy();
+});
diff --git a/code/app/src/routes/(main)/(public)/sign-in/index.ts b/code/app/src/routes/(main)/(public)/sign-in/index.ts
new file mode 100644
index 0000000..c1a1929
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-in/index.ts
@@ -0,0 +1,20 @@
+export enum SignInPageMessage {
+ AFTER_PASSWORD_RESET = "after-password-reset",
+ USER_INACTIVITY = "user-inactivity",
+ USER_DISABLED = "user-disabled",
+ LOGGED_OUT = "logged-out"
+}
+
+export const signInPageMessageQueryKey = "m";
+export const signInPageTestKeys = {
+ passwordInput: "password-input",
+ usernameInput: "username-input",
+ rememberMeCheckbox: "remember-me-checkbox",
+ signInForm: "sign-in-form",
+ userInactivityAlert: SignInPageMessage.USER_INACTIVITY + "-alert",
+ userDisabledAlert: SignInPageMessage.USER_DISABLED + "-alert",
+ afterPasswordResetAlert: SignInPageMessage.AFTER_PASSWORD_RESET + "-alert",
+ formErrorAlert: "form-error-alert",
+ resetPasswordAnchor: "reset-password-anchor",
+ signUpAnchor: "sign-up-anchor",
+}; \ No newline at end of file
diff --git a/code/app/src/routes/(main)/(public)/sign-up/+page.svelte b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte
new file mode 100644
index 0000000..470ac5d
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-up/+page.svelte
@@ -0,0 +1,106 @@
+<script lang="ts">
+ import { goto } from "$app/navigation";
+ import { Button, Input, Alert } from "$components";
+ import LL from "$i18n/i18n-svelte";
+ import { FormError } from "$models/internal/FormError";
+ import type { CreateAccountPayload } from "$services/abstractions/IAccountService";
+ import { AccountService } from "$services/account-service";
+
+ const formData = {
+ username: {
+ value: "",
+ errors: [],
+ },
+ password: {
+ value: "",
+ errors: [],
+ },
+ as_payload(): CreateAccountPayload {
+ return {
+ username: formData.username.value,
+ password: formData.password.value,
+ };
+ },
+ };
+
+ const formError = new FormError();
+ const accountService = new AccountService();
+
+ let loading = false;
+ let showErrorAlert = false;
+
+ async function submit_form_async() {
+ loading = true;
+ showErrorAlert = false;
+ formError.set();
+ formData.username.errors = [];
+ formData.password.errors = [];
+ const response = await accountService.create_account_async(formData.as_payload());
+ if (response.isCreated) {
+ await goto("/home");
+ } else if (response.knownProblem) {
+ formError.set_from_known_problem(response.knownProblem);
+ for (const error of Object.entries(response.knownProblem.errors)) {
+ if (error[0] === "username") {
+ const errors = [];
+ error[1].forEach((e) => errors.push(e));
+ formData.username.errors = errors;
+ }
+ if (error[0] === "password") {
+ const errors = [];
+ error[1].forEach((e) => errors.push(e));
+ formData.password.errors = errors;
+ }
+ }
+ } else {
+ formError.set($LL.unexpectedError(), $LL.tryAgainSoon());
+ }
+ loading = false;
+ showErrorAlert = formError.has_error();
+ }
+</script>
+
+<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
+ <div class="sm:mx-auto sm:w-full p-2 sm:p-0 sm:max-w-md">
+ <h2 class="mt-6 text-3xl tracking-tight font-bold text-gray-900">
+ {$LL.signUpPage.createYourNewAccount()}
+ </h2>
+ <p class="mt-2 text-sm text-gray-600">
+ {$LL.or().toLowerCase()}
+ <a href="/sign-in" class="link">
+ {$LL.signIntoYourAccount().toLowerCase()}
+ </a>
+ </p>
+ </div>
+
+ <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
+ <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
+ {#if showErrorAlert}
+ <Alert title={formError.title} message={formError.subtitle} type="error" class="mb-2" />
+ {/if}
+ <form class="space-y-6" on:submit|preventDefault={submit_form_async}>
+ <Input
+ label={$LL.emailAddress()}
+ id="email"
+ name="email"
+ autocomplete="email"
+ required
+ type="email"
+ bind:value={formData.username.value}
+ errors={formData.username.errors}
+ />
+
+ <Input
+ label={$LL.password()}
+ id="password"
+ name="password"
+ required
+ type="password"
+ bind:value={formData.password.value}
+ errors={formData.password.errors}
+ />
+ <Button type="submit" text={$LL.submit()} {loading} fullWidth />
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/code/app/src/routes/(main)/(public)/sign-up/+page.ts b/code/app/src/routes/(main)/(public)/sign-up/+page.ts
new file mode 100644
index 0000000..8c86f55
--- /dev/null
+++ b/code/app/src/routes/(main)/(public)/sign-up/+page.ts
@@ -0,0 +1,11 @@
+import LL from '$i18n/i18n-svelte';
+import { get } from 'svelte/store';
+import type { PageLoad } from './$types';
+
+const l = get(LL);
+
+export const load: PageLoad = async () => {
+ return {
+ title: l.signUpPage.title(),
+ };
+}; \ No newline at end of file