aboutsummaryrefslogtreecommitdiffstats
path: root/old-apps/portal/src/app
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2022-09-20 09:24:27 +0200
committerivarlovlie <git@ivarlovlie.no>2022-09-20 09:24:27 +0200
commita9072370ca1eb9a5cce928b1d487db0f307edea6 (patch)
tree59c3c23df930a8b5f888dc7813923abf4ceefed4 /old-apps/portal/src/app
parent56fa963a1d63cbe0bf28e29e717cceaa417c45c1 (diff)
downloadgreatoffice-a9072370ca1eb9a5cce928b1d487db0f307edea6.tar.xz
greatoffice-a9072370ca1eb9a5cce928b1d487db0f307edea6.zip
feat: Move old apps into it's own directory
Diffstat (limited to 'old-apps/portal/src/app')
-rw-r--r--old-apps/portal/src/app/components/user-menu.svelte70
-rw-r--r--old-apps/portal/src/app/index.d.ts48
-rw-r--r--old-apps/portal/src/app/index.scss27
-rw-r--r--old-apps/portal/src/app/index.svelte87
-rw-r--r--old-apps/portal/src/app/index.ts14
-rw-r--r--old-apps/portal/src/app/pages/_layout.svelte62
-rw-r--r--old-apps/portal/src/app/pages/_layout@loggedin.svelte75
-rw-r--r--old-apps/portal/src/app/pages/admin/index.svelte18
-rw-r--r--old-apps/portal/src/app/pages/forgot.svelte102
-rw-r--r--old-apps/portal/src/app/pages/home.svelte103
-rw-r--r--old-apps/portal/src/app/pages/login.svelte142
-rw-r--r--old-apps/portal/src/app/pages/profile/index.svelte167
-rw-r--r--old-apps/portal/src/app/pages/reset-password.svelte138
-rw-r--r--old-apps/portal/src/app/pages/sign-up.svelte131
14 files changed, 1184 insertions, 0 deletions
diff --git a/old-apps/portal/src/app/components/user-menu.svelte b/old-apps/portal/src/app/components/user-menu.svelte
new file mode 100644
index 0000000..b0cfc8a
--- /dev/null
+++ b/old-apps/portal/src/app/components/user-menu.svelte
@@ -0,0 +1,70 @@
+<script>
+ import {end_session} from "$shared/lib/session";
+ import {onMount} from "svelte";
+ import {Menu, MenuItem, MenuItemSeparator} from "$shared/components/menu";
+ import {replace} from "svelte-spa-router";
+
+ let userMenuTrigger;
+ let showUserMenu = false;
+
+ export let avatar = "";
+ export let name;
+ export let secondary = "";
+ let userMenuId;
+
+ async function on_logout() {
+ await end_session(() => {
+ replace("/login");
+ });
+ }
+
+ onMount(() => {
+ userMenuTrigger = document.getElementById("open-user-menu");
+ });
+</script>
+
+<button class="reset user-menu-control"
+ id="open-user-menu"
+ aria-controls="{userMenuId}"
+ on:click={() => showUserMenu = true}>
+ {#if avatar}
+ <figure class="user-menu-control__img-wrapper radius-50%">
+ <img class="user-menu-control__img"
+ src="{avatar}"
+ alt="Avatar">
+ </figure>
+ {/if}
+
+ <div class="margin-x-xs user-menu__meta">
+ <p class="user-menu__meta-title text-sm line-height-1 padding-y-xxxxs font-semibold color-contrast-higher text-truncate">{name}</p>
+ {#if secondary}
+ <p class="text-xs color-contrast-medium line-height-1 padding-bottom-xxxxs">{secondary}</p>
+ {/if}
+ </div>
+
+ <svg class="icon icon--xxs"
+ aria-hidden="true"
+ viewBox="0 0 12 12">
+ <polyline points="1 4 6 9 11 4"
+ fill="none"
+ stroke="currentColor"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ stroke-width="2"/>
+ </svg>
+</button>
+
+<Menu trigger={userMenuTrigger}
+ bind:id={userMenuId}
+ bind:show="{showUserMenu}">
+ <div slot="options">
+ <MenuItem on:click={() => replace("/profile")}>
+ <span>Profile</span>
+ </MenuItem>
+ <MenuItemSeparator/>
+ <MenuItem danger="true"
+ on:click={() => on_logout()}>
+ Logout
+ </MenuItem>
+ </div>
+</Menu>
diff --git a/old-apps/portal/src/app/index.d.ts b/old-apps/portal/src/app/index.d.ts
new file mode 100644
index 0000000..c044583
--- /dev/null
+++ b/old-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/old-apps/portal/src/app/index.scss b/old-apps/portal/src/app/index.scss
new file mode 100644
index 0000000..718adf2
--- /dev/null
+++ b/old-apps/portal/src/app/index.scss
@@ -0,0 +1,27 @@
+@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';
+@use '../../web-shared/src/styles/components/light-dark-switch';
+@use '../../web-shared/src/styles/components/link-card';
+@use '../../web-shared/src/styles/components/auto-sized-grid';
+@use '../../web-shared/src/styles/components/menu';
+@use '../../web-shared/src/styles/components/user-menu';
+@use '../../web-shared/src/styles/components/breadcrumbs';
diff --git a/old-apps/portal/src/app/index.svelte b/old-apps/portal/src/app/index.svelte
new file mode 100644
index 0000000..af2b6d0
--- /dev/null
+++ b/old-apps/portal/src/app/index.svelte
@@ -0,0 +1,87 @@
+<svelte:options immutable={true}/>
+<svelte:window bind:online={online}/>
+
+<script>
+ import Router, {replace} from "svelte-spa-router";
+ import {wrap} from "svelte-spa-router/wrap";
+ import {is_active} from "$shared/lib/session";
+ 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 Home from "$app/pages/home.svelte";
+ import ProfileHome from "$app/pages/profile/index.svelte";
+ import AdminHome from "$app/pages/admin/index.svelte";
+ import PreHeader from "$shared/components/pre-header.svelte";
+
+ let online = true;
+
+ const publicRoutes = ["/login", "/signup", "/reset-password", "/forgot"];
+ const guardedRoutes = ["/", "/home", "/profile", "/admin"];
+
+ async function user_is_logged_in(event) {
+ const isActive = await is_active();
+ if (!isActive && !publicRoutes.includes(event.route)) {
+ return false;
+ }
+ if (isActive && !guardedRoutes.includes(event.route)) {
+ await replace("/");
+ }
+ return true;
+ }
+
+ function route_guarded(event) {
+ if (!publicRoutes.includes(event.detail.route)) {
+ replace("/login");
+ }
+ }
+
+ const routes = {
+ "/login": wrap({
+ component: Login,
+ conditions: [user_is_logged_in],
+ }),
+ "/home": wrap({
+ component: Home,
+ conditions: [user_is_logged_in],
+ }),
+ "/admin": wrap({
+ component: AdminHome,
+ conditions: [user_is_logged_in],
+ }),
+ "/profile": wrap({
+ component: ProfileHome,
+ conditions: [user_is_logged_in],
+ }),
+ "/": wrap({
+ component: Home,
+ 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],
+ })
+ };
+</script>
+
+<PreHeader show="{!online}">You seem to be offline, please check your internet connection.</PreHeader>
+
+<Router
+ {routes}
+ restoreScrollState={true}
+ on:conditionsFailed={route_guarded}
+ on:routeLoading={() => {
+ document.getElementById("loader").style.display = "inline-block";
+ }}
+ on:routeLoaded={() => {
+ document.getElementById("loader").style.display = "none";
+ }}
+/>
diff --git a/old-apps/portal/src/app/index.ts b/old-apps/portal/src/app/index.ts
new file mode 100644
index 0000000..0bfb30d
--- /dev/null
+++ b/old-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/old-apps/portal/src/app/pages/_layout.svelte b/old-apps/portal/src/app/pages/_layout.svelte
new file mode 100644
index 0000000..8c75cb9
--- /dev/null
+++ b/old-apps/portal/src/app/pages/_layout.svelte
@@ -0,0 +1,62 @@
+<script>
+ import BlowoutToolbelt from "$shared/components/blowout-toolbelt.svelte";
+</script>
+
+<style>
+ #decoration {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ z-index: 1;
+ }
+
+ #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>
+<BlowoutToolbelt/>
+
+<main class="container-fluid padding-x-xs padding-x-xxl@xs padding-y-md padding-y-lg@md max-width-sm">
+ <div class="z-index-2 position-relative">
+ <slot/>
+ </div>
+
+ <figure id="decoration"
+ class="z-index-1"
+ aria-hidden="true">
+ <svg class="color-contrast-higher opacity-10%"
+ viewBox="0 0 1920 450"
+ fill="none">
+ <g stroke="currentColor"
+ stroke-width="2"
+ stroke-linejoin="round"
+ stroke-linecap="round">
+ <path d="M1449 94.9993V3L1354 48.9995L1259 3V94.9993L1354 140.999L1449 94.9993Z"/>
+ <path d="M1639 94.9993V3L1544 48.9995L1449 3V94.9993L1544 140.999L1639 94.9993Z"/>
+ <path d="M1354 49.0002V141"/>
+ <path d="M1544 49.0002V141"/>
+ <path d="M1449 94.9995L1544 140.999L1449 186.999L1354 140.999L1449 94.9995Z"/>
+ <path d="M1544 141V232.999L1449 278.999L1354 232.999V141"/>
+ <path d="M1449 187V279"/>
+ <path d="M1544 264L1639 310L1544 355.999L1449 310L1544 264Z"/>
+ <path d="M1639 310V402L1544 447.999L1449 402V310"/>
+ <path d="M1544 356.001V448"/>
+ <path d="M1639 94.9995L1734 140.999L1639 186.999L1544 140.999L1639 94.9995Z"/>
+ <path d="M1734 141V232.999L1639 278.999L1544 232.999V141"/>
+ <path d="M1639 187V279"/>
+ </g>
+ </svg>
+ </figure>
+</main>
diff --git a/old-apps/portal/src/app/pages/_layout@loggedin.svelte b/old-apps/portal/src/app/pages/_layout@loggedin.svelte
new file mode 100644
index 0000000..44e2e4a
--- /dev/null
+++ b/old-apps/portal/src/app/pages/_layout@loggedin.svelte
@@ -0,0 +1,75 @@
+<script>
+ import BlowoutToolbelt from "$shared/components/blowout-toolbelt.svelte";
+ import {end_session, get_session_data} from "$shared/lib/session";
+ import {replace} from "svelte-spa-router";
+
+ const session = get_session_data();
+</script>
+
+<style>
+ #decoration {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ z-index: 1;
+ }
+
+ #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>
+<BlowoutToolbelt/>
+<main class="container max-width-xl padding-x-xs padding-x-xxl@xs padding-y-md padding-y-lg@md">
+ <div class="z-index-2 position-relative">
+ <slot/>
+ </div>
+
+ <div class="flex flex-row gap-xs position-fixed left-0 top-0 margin-md z-index-2">
+ <span on:click={async () => {
+ if (confirm("Are you sure?")) await end_session(() => {
+ replace("/login");
+ })
+ }} class="btn btn--sm">
+ Logout
+ </span>
+ </div>
+
+ <figure id="decoration"
+ class="z-index-1"
+ aria-hidden="true">
+ <svg class="color-contrast-higher opacity-10%"
+ viewBox="0 0 1920 450"
+ fill="none">
+ <g stroke="currentColor"
+ stroke-width="2"
+ stroke-linejoin="round"
+ stroke-linecap="round">
+ <path d="M1449 94.9993V3L1354 48.9995L1259 3V94.9993L1354 140.999L1449 94.9993Z"/>
+ <path d="M1639 94.9993V3L1544 48.9995L1449 3V94.9993L1544 140.999L1639 94.9993Z"/>
+ <path d="M1354 49.0002V141"/>
+ <path d="M1544 49.0002V141"/>
+ <path d="M1449 94.9995L1544 140.999L1449 186.999L1354 140.999L1449 94.9995Z"/>
+ <path d="M1544 141V232.999L1449 278.999L1354 232.999V141"/>
+ <path d="M1449 187V279"/>
+ <path d="M1544 264L1639 310L1544 355.999L1449 310L1544 264Z"/>
+ <path d="M1639 310V402L1544 447.999L1449 402V310"/>
+ <path d="M1544 356.001V448"/>
+ <path d="M1639 94.9995L1734 140.999L1639 186.999L1544 140.999L1639 94.9995Z"/>
+ <path d="M1734 141V232.999L1639 278.999L1544 232.999V141"/>
+ <path d="M1639 187V279"/>
+ </g>
+ </svg>
+ </figure>
+</main>
diff --git a/old-apps/portal/src/app/pages/admin/index.svelte b/old-apps/portal/src/app/pages/admin/index.svelte
new file mode 100644
index 0000000..f9b91d2
--- /dev/null
+++ b/old-apps/portal/src/app/pages/admin/index.svelte
@@ -0,0 +1,18 @@
+<script>
+ import Layout from "../_layout@loggedin.svelte";
+ import {Bread, Crumb} from "$shared/components/breadcrumb";
+ import {push} from "svelte-spa-router";
+
+</script>
+
+<Layout>
+ <Bread>
+ <Crumb name="Home"
+ withArrow="true"
+ isLink="true"
+ on:click={() => push("/")}/>
+ <Crumb name="Organisation"/>
+ </Bread>
+
+ <main class="max-width-sm"></main>
+</Layout>
diff --git a/old-apps/portal/src/app/pages/forgot.svelte b/old-apps/portal/src/app/pages/forgot.svelte
new file mode 100644
index 0000000..156deab
--- /dev/null
+++ b/old-apps/portal/src/app/pages/forgot.svelte
@@ -0,0 +1,102 @@
+<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 Tile from "$shared/components/tile.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>
+ <Tile>
+ <form on:submit|preventDefault={submit_form}
+ class="max-width-xxs">
+ <fieldset>
+ <legend class="form-legend">
+ <span class="margin-bottom-xs text-xl">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>
+ </Tile>
+</Layout>
diff --git a/old-apps/portal/src/app/pages/home.svelte b/old-apps/portal/src/app/pages/home.svelte
new file mode 100644
index 0000000..0e325ee
--- /dev/null
+++ b/old-apps/portal/src/app/pages/home.svelte
@@ -0,0 +1,103 @@
+<script>
+ import {projects_base} from "$shared/lib/configuration";
+ import {get_session_data} from "$shared/lib/session";
+ import {push} from "svelte-spa-router";
+ import Layout from "./_layout@loggedin.svelte";
+ import LinkCard from "$shared/components/link-card.svelte";
+ import Alert from "$shared/components/alert.svelte";
+ import {UserIcon, UsersIcon, WatchIcon, SendIcon, ListIcon} from "svelte-feather-icons";
+
+ let showUsers = true;
+ const session = get_session_data();
+</script>
+
+<Layout>
+ <div class="grid gap-md">
+ <div class="row">
+ <Alert closeable="true"
+ closeableCooldown="~"
+ id="welcome-note"
+ title="Hello {session.profile?.username}"
+ message="This is your portal to Greatoffice, here you will find all your great apps and management options."/>
+ </div>
+ <div class="row">
+ <h2 class="margin-bottom-xs">Apps</h2>
+ <div class="grid-auto-md gap-sm">
+ <LinkCard name="Projects"
+ description="The home for your projects"
+ text="Open in a new tab"
+ title="Open Projects"
+ href="{projects_base()}">
+ <figure slot="icon">
+ <div class="bg-primary bg-opacity-10% padding-xs border-left border-primary border-2">
+ <WatchIcon size="42"
+ class="color-primary"
+ strokeWidth="1.2"/>
+ </div>
+ </figure>
+ </LinkCard>
+ <LinkCard name="Tickets"
+ description="The home for your tickets"
+ class="c-disabled user-select-none"
+ text="Coming soon"
+ title="Open Tickets"
+ href="{projects_base()}">
+ <figure slot="icon">
+ <div class="bg-primary bg-opacity-10% padding-xs border-left border-primary border-2">
+ <SendIcon size="42"
+ class="color-primary"
+ strokeWidth="1.2"/>
+ </div>
+ </figure>
+ </LinkCard>
+ <LinkCard name="Todo"
+ description="The home for your todos"
+ class="c-disabled user-select-none"
+ text="Coming soon"
+ title="Open Todo"
+ href="{projects_base()}">
+ <figure slot="icon">
+ <div class="bg-primary bg-opacity-10% padding-xs border-left border-primary border-2">
+ <ListIcon size="42"
+ class="color-primary"
+ strokeWidth="1.2"/>
+ </div>
+ </figure>
+ </LinkCard>
+ </div>
+ </div>
+ <div class="row">
+ <h2 class="margin-bottom-xs">Manage</h2>
+ <div class="grid-auto-md gap-sm">
+ <LinkCard name="Profile"
+ description="Manage your profile"
+ text="Open"
+ title="Go to your profile management page"
+ on:click={() => push("/profile")}>
+ <figure slot="icon">
+ <div class="bg-primary bg-opacity-10% padding-xs border-left border-primary border-2">
+ <UserIcon size="42"
+ class="color-primary"
+ strokeWidth="1.2"/>
+ </div>
+ </figure>
+ </LinkCard>
+ {#if showUsers}
+ <LinkCard name="Organisation"
+ description="Manage your organisation"
+ title="Go to your organisations management page"
+ text="Open"
+ on:click={() => push("/admin")}>
+ <figure slot="icon">
+ <div class="bg-primary bg-opacity-10% padding-xs border-left border-primary border-2">
+ <UsersIcon size="42"
+ class="color-primary"
+ strokeWidth="1.2"/>
+ </div>
+ </figure>
+ </LinkCard>
+ {/if}
+ </div>
+ </div>
+ </div>
+</Layout>
diff --git a/old-apps/portal/src/app/pages/login.svelte b/old-apps/portal/src/app/pages/login.svelte
new file mode 100644
index 0000000..1ca6b61
--- /dev/null
+++ b/old-apps/portal/src/app/pages/login.svelte
@@ -0,0 +1,142 @@
+<script>
+ import {onMount} from "svelte";
+ import {link, replace, querystring} from "svelte-spa-router";
+ import {api_base, IconNames, frontpage_base} from "$shared/lib/configuration";
+ import Button from "$shared/components/button.svelte";
+ import Alert from "$shared/components/alert.svelte";
+ import Tile from "$shared/components/tile.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: "",
+ persist: true
+ },
+ 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) {
+ await replace("#/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) {
+ 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>
+ <a href="{frontpage_base()}" class="block margin-bottom-xs">Go to {frontpage_base()}</a>
+ <Tile>
+ <form on:submit|preventDefault={loginForm.submit_form}
+ class="max-width-xxs">
+ <fieldset>
+ <legend class="form-legend">
+ <span class="margin-bottom-xs text-xl">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"
+ type="submit"
+ variant="primary"
+ loading={loginForm.loading}/>
+ </div>
+ </fieldset>
+ </form>
+ </Tile>
+</Layout>
diff --git a/old-apps/portal/src/app/pages/profile/index.svelte b/old-apps/portal/src/app/pages/profile/index.svelte
new file mode 100644
index 0000000..a7291d6
--- /dev/null
+++ b/old-apps/portal/src/app/pages/profile/index.svelte
@@ -0,0 +1,167 @@
+<script>
+ import {push} from "svelte-spa-router";
+ import {Bread, Crumb} from "$shared/components/breadcrumb/index";
+ import Layout from "$app/pages/_layout@loggedin.svelte";
+ import {update_profile} from "$shared/lib/api/user";
+ import Alert from "$shared/components/alert.svelte";
+ import Button from "$shared/components/button.svelte";
+ import {is_email} from "$shared/lib/helpers";
+ import {api_base} from "$shared/lib/configuration";
+ import {get_session_data} from "$shared/lib/session";
+
+ const archiveLink = api_base("_/account/archive");
+
+ let modal;
+ let understands = false;
+
+ let formIsLoading = false;
+ let formError;
+
+ let username = get_session_data()?.profile.username;
+ let usernameFieldMessage;
+ let usernameFieldMessageClass = "color-error";
+
+ let password;
+ let passwordFieldMessage;
+ let passwordFieldMessageClass = "color-error";
+
+ async function submit_form(e) {
+ e.preventDefault();
+ if (!username && !password) {
+ console.error("Not submitting because both values is empty");
+ return;
+ }
+
+ usernameFieldMessage = "";
+ passwordFieldMessage = "";
+
+ if (username && !is_email(username)) {
+ usernameFieldMessage = "Username has to be a valid email";
+ return;
+ }
+
+ if (password && password?.length < 6) {
+ passwordFieldMessage = "The new password must contain at least 6 characters";
+ return;
+ }
+
+ formIsLoading = true;
+
+ const response = await update_profile({
+ username,
+ password,
+ });
+
+ formIsLoading = false;
+
+ if (response.ok) {
+ if (password) {
+ passwordFieldMessage = "Successfully updated";
+ passwordFieldMessageClass = "color-success";
+ password = "";
+ }
+ if (username) {
+ usernameFieldMessage = "Successfully updated";
+ usernameFieldMessageClass = "color-success";
+ password = "";
+ }
+ } else {
+ formError = response.data.title ?? "An unknown error occured";
+ }
+ }
+
+ async function handle_delete_account_button_click() {
+ alert("Not implemented");
+ return;
+ if (understands && confirm("Are you absolutely sure that you want to delete your account?")) {
+ }
+ }
+
+ export const functions = {
+ open() {
+ modal.open();
+ },
+ close() {
+ // modal.close();
+ },
+ };
+</script>
+
+<Layout>
+ <Bread>
+ <Crumb name="Home"
+ withArrow="true"
+ isLink="true"
+ on:click={() => push("/")}/>
+ <Crumb name="Profile"/>
+ </Bread>
+
+ <main class="max-width-sm">
+ <section class="margin-bottom-md">
+ <p class="text-md margin-bottom-sm">Update your information</p>
+ <form on:submit={submit_form}
+ autocomplete="new-password">
+ {#if formError}
+ <small class="color-danger">{formError}</small>
+ {/if}
+ <div class="margin-bottom-sm">
+ <label for="email"
+ class="form-label margin-bottom-xxs">New username</label>
+ <input type="email"
+ class="form-control width-100%"
+ id="email"
+ placeholder={username}
+ bind:value={username}/>
+ {#if usernameFieldMessage}
+ <small class={usernameFieldMessageClass}>{usernameFieldMessage}</small>
+ {/if}
+ </div>
+ <div class="margin-bottom-sm">
+ <label for="password"
+ class="form-label margin-bottom-xxs">New password</label>
+ <input type="password"
+ class="form-control width-100%"
+ id="password"
+ bind:value={password}/>
+ {#if passwordFieldMessage}
+ <small class={passwordFieldMessageClass}>{passwordFieldMessage}</small>
+ {/if}
+ </div>
+ <div class="flex justify-end">
+ <Button text="Save"
+ on:click={submit_form}
+ variant="primary"
+ loading={formIsLoading}/>
+ </div>
+ </form>
+ </section>
+ <section class="margin-bottom-md">
+ <p class="text-md margin-bottom-sm">Download your data</p>
+ <a class="btn btn--subtle"
+ href={archiveLink}
+ download>Click here to download your data</a>
+ </section>
+ <section>
+ <p class="text-md margin-bottom-sm">Delete account</p>
+ <div class="margin-bottom-sm">
+ <Alert
+ message="Deleting your account and data means that all of your data (entries, categories, etc.) will be unrecoverable forever.<br>You should probably download your data before continuing."
+ type="info"
+ />
+ </div>
+ <div class="form-check margin-bottom-sm">
+ <input type="checkbox"
+ class="checkbox"
+ id="the-consequences"
+ bind:checked={understands}/>
+ <label for="the-consequences">I understand the consequences of deleting my account and data.</label>
+ </div>
+ <div class="flex justify-end">
+ <Button text="Delete everything"
+ variant="accent"
+ disabled={!understands}
+ on:click={handle_delete_account_button_click}/>
+ </div>
+ </section>
+ </main>
+</Layout>
diff --git a/old-apps/portal/src/app/pages/reset-password.svelte b/old-apps/portal/src/app/pages/reset-password.svelte
new file mode 100644
index 0000000..dabf5c9
--- /dev/null
+++ b/old-apps/portal/src/app/pages/reset-password.svelte
@@ -0,0 +1,138 @@
+<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 Tile from "$shared/components/tile.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>
+ <Tile>
+ <form on:submit|preventDefault={submit}
+ class="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 text-xl">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>
+ </Tile>
+</Layout>
diff --git a/old-apps/portal/src/app/pages/sign-up.svelte b/old-apps/portal/src/app/pages/sign-up.svelte
new file mode 100644
index 0000000..3bcab6d
--- /dev/null
+++ b/old-apps/portal/src/app/pages/sign-up.svelte
@@ -0,0 +1,131 @@
+<script>
+ import {create_account} from "$shared/lib/api/user";
+ import {frontpage_base} from "$shared/lib/configuration";
+ import {is_email} from "$shared/lib/helpers";
+ import Alert from "$shared/components/alert.svelte";
+ import Button from "$shared/components/button.svelte";
+ import Tile from "$shared/components/tile.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>
+ <a href="{frontpage_base()}"
+ class="block margin-bottom-xs">Go to {frontpage_base()}</a>
+ <Tile>
+ <form on:submit|preventDefault={signupForm.submit_form}
+ class="max-width-xxs">
+ <fieldset>
+ <legend class="form-legend">
+ <span class="margin-bottom-xs text-xl">Create your account</span> <br/>
+ <span class="text-sm"
+ >... or <a href="/login"
+ use:link>log in</a></span
+ >
+ </legend>
+ <div class="margin-bottom-xs">
+ <p>Provide an email and password to get immediate access to your new environment (30 days full access, no billing details required, no promotion emails).</p>
+ </div>
+ <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="Create account"
+ type="primary"
+ loading={signupForm.loading}
+ />
+ </div>
+ </fieldset>
+ </form>
+ </Tile>
+</Layout>