From fa283d70cbd8820b9955d4de63668fa8ea7935c9 Mon Sep 17 00:00:00 2001 From: Brewhouse Digital <66521220+brewhousedigital@users.noreply.github.com> Date: Sat, 15 Oct 2022 10:36:15 -0500 Subject: [PATCH] Added new Navbar, Login/Register pages, and new Components (#5) * Added new Navbar, Login/Register pages, and new Components * Db logic fix * Fixed instance page * Default provisioning status * minor dashboard fixes * Form error handling * Provisioning status fix * github action * Enhanced home page signup flow * Whitespace fixes * Added responsive CSS for the dashboard * Merged all the latest PRs and added a new MediaQuery component and mobile nav * Removed the homepage animation on mobile but left for tablet sizes and up. Also improved the navigation bar for mobile use Co-authored-by: Brewhouse Digital Co-authored-by: Ben Allfree --- development.md | 6 +- packages/common/src/index.ts | 2 +- packages/pockethost.io/.prettierignore | 3 + packages/pockethost.io/.prettierrc | 33 ++- packages/pockethost.io/package.json | 80 +++---- packages/pockethost.io/src/app.html | 47 ++-- .../src/components/AlertBar.svelte | 21 ++ .../src/components/AuthCheck.svelte | 4 +- .../src/components/Caption/Caption.svelte | 10 +- .../src/components/CodeSample.svelte | 2 +- .../src/components/Error/Error.svelte | 2 +- .../src/components/FeatureCard.svelte | 77 ++++--- .../pockethost.io/src/components/Gap.svelte | 8 - .../components/HomepageHeroAnimation.svelte | 78 ++++--- .../components/InstanceGeneratorWidget.svelte | 154 +++++++++++++ .../src/components/MediaQuery.svelte | 42 ++++ .../src/components/Navbar.svelte | 207 ++++++++++++------ .../src/components/NavbarBrandImage.svelte | 12 - .../src/components/NavbarText.svelte | 1 - .../ProvisioningStatus.svelte | 5 +- .../src/components/Title/Title.svelte | 18 +- .../src/pocketbase/PocketbaseClient.ts} | 50 ++--- .../{pocketbase.ts => pocketbase/index.ts} | 2 +- .../pockethost.io/src/routes/+layout.svelte | 3 - .../pockethost.io/src/routes/+page.svelte | 107 ++++++--- .../app/instances/[instanceId]/+page.svelte | 30 +-- .../src/routes/app/new/+page.svelte | 3 +- .../src/routes/dashboard/+page.svelte | 21 +- .../src/routes/login/+page.svelte | 131 +++++++---- .../pockethost.io/src/routes/shh/+page.svelte | 10 +- .../src/routes/signup/+page.svelte | 171 ++++++++------- packages/pockethost.io/src/util/database.ts | 170 ++++++++++++++ packages/pockethost.io/src/util/stores.ts | 3 + packages/pockethost.io/src/util/utilities.ts | 3 + packages/pockethost.io/static/global.css | 46 +++- .../static/images/logo-square.png | Bin 0 -> 3044 bytes packages/pockethost.io/svelte.config.js | 24 +- packages/pockethost.io/tsconfig.json | 42 ++-- 38 files changed, 1106 insertions(+), 522 deletions(-) create mode 100644 packages/pockethost.io/src/components/AlertBar.svelte delete mode 100644 packages/pockethost.io/src/components/Gap.svelte create mode 100644 packages/pockethost.io/src/components/InstanceGeneratorWidget.svelte create mode 100644 packages/pockethost.io/src/components/MediaQuery.svelte delete mode 100644 packages/pockethost.io/src/components/NavbarBrandImage.svelte delete mode 100644 packages/pockethost.io/src/components/NavbarText.svelte rename packages/{common/src/pocketbase.ts => pockethost.io/src/pocketbase/PocketbaseClient.ts} (64%) rename packages/pockethost.io/src/{pocketbase.ts => pocketbase/index.ts} (77%) create mode 100644 packages/pockethost.io/src/util/database.ts create mode 100644 packages/pockethost.io/src/util/stores.ts create mode 100644 packages/pockethost.io/src/util/utilities.ts create mode 100644 packages/pockethost.io/static/images/logo-square.png diff --git a/development.md b/development.md index a4958530..07cab492 100644 --- a/development.md +++ b/development.md @@ -9,7 +9,7 @@ cp .env-template-frontend-only .env yarn dev ``` -That's it. Youre in business. Your local Svelte build will talk to the pockethost.io mothership and connect to that for all database-related tasks. +That's it. You're in business. Your local Svelte build will talk to the pockethost.io mothership and connect to that for all database-related tasks. # Developing the backend using `docker-compose` @@ -26,7 +26,7 @@ cd pockethost **Edit `/etc/hosts`** -You need at least 3 host entries. One for the main domain, one for the database that tracks everything (the main pockethost.io db), and one (or more) for any instances you want to create an test. Wildcarding is not supported in `/etc/hosts`, so you have to make a manual entry for any PB instance you want to create and test. See `.etc-hosts-sample` for details. +You need at least 3 host entries. One for the main domain, one for the database that tracks everything (the main pockethost.io db), and one (or more) for any instances you want to create a test. Wildcarding is not supported in `/etc/hosts`, so you have to make a manual entry for any PB instance you want to create and test. See `.etc-hosts-sample` for details. ``` 127.0.0.1 pockethost.local # The main domain @@ -38,7 +38,7 @@ You need at least 3 host entries. One for the main domain, one for the database _Any time you change the PocketBase code, you need to rebuild (`yarn build:_`) and restart `docker-compose`\_ -This is to build the binary that runs INSIDE Docker. The Docker container will run using the same architecture as the host machine. If you are running an x86 machine, you'll probably need `build:386`. If you're running on Linux or Mac, then `arm64` is the one you want. You can try them both if you aren't sure. The worst that will appen is the `pocketbase` binary won't execute in Docker and you'll quickly discover that. +This is to build the binary that runs INSIDE Docker. The Docker container will run using the same architecture as the host machine. If you are running an x86 machine, you'll probably need `build:386`. If you're running on Linux or Mac, then `arm64` is the one you want. You can try them both if you aren't sure. The worst that will happen is the `pocketbase` binary won't execute in Docker and you'll quickly discover that. ```bash cd packages/pocketbase diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 37c05fe2..510f3c4e 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,3 +1,3 @@ export * from './assert' -export * from './pocketbase' +export * from './RealtimeSubscriptionManager' export * from './schema' diff --git a/packages/pockethost.io/.prettierignore b/packages/pockethost.io/.prettierignore index 38972655..cb3c1f57 100644 --- a/packages/pockethost.io/.prettierignore +++ b/packages/pockethost.io/.prettierignore @@ -11,3 +11,6 @@ node_modules pnpm-lock.yaml package-lock.json yarn.lock + +# Source files +/src/assets/_bootstrap.css \ No newline at end of file diff --git a/packages/pockethost.io/.prettierrc b/packages/pockethost.io/.prettierrc index bf509a7f..0b157833 100644 --- a/packages/pockethost.io/.prettierrc +++ b/packages/pockethost.io/.prettierrc @@ -1,19 +1,16 @@ { - "useTabs": false, - "singleQuote": true, - "semi": false, - "trailingComma": "none", - "printWidth": 100, - "pluginSearchDirs": [ - ".", - "../.." - ], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ] -} \ No newline at end of file + "useTabs": false, + "singleQuote": true, + "semi": false, + "trailingComma": "none", + "printWidth": 100, + "pluginSearchDirs": [".", "../.."], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/packages/pockethost.io/package.json b/packages/pockethost.io/package.json index 32192874..2470c297 100644 --- a/packages/pockethost.io/package.json +++ b/packages/pockethost.io/package.json @@ -1,41 +1,41 @@ { - "name": "@pockethost/app", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --check .", - "format": "prettier --write .", - "serve": "node dist-server/index.js", - "watch": "chokidar 'src/**' -c 'yarn build' --initial" - }, - "devDependencies": { - "@sveltejs/adapter-auto": "next", - "@sveltejs/kit": "next", - "chokidar-cli": "^3.0.0", - "svelte": "^3.44.0", - "svelte-check": "^2.7.1", - "svelte-preprocess": "^4.10.6", - "tslib": "^2.3.1", - "typescript": "^4.7.4", - "vite": "^3.1.0" - }, - "type": "module", - "dependencies": { - "@fortawesome/free-brands-svg-icons": "^6.2.0", - "@fortawesome/free-solid-svg-icons": "^6.2.0", - "@pockethost/common": "0.0.1", - "@s-libs/micro-dash": "12", - "@sveltejs/adapter-node": "^1.0.0-next.92", - "pocketbase": "^0.7.0", - "random-word-slugs": "^0.1.6", - "sass": "^1.54.9", - "svelte-fa": "^3.0.3", - "svelte-highlight": "^6.2.1", - "sveltestrap": "^5.9.0" - } -} \ No newline at end of file + "name": "@pockethost/app", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check .", + "format": "prettier --write .", + "serve": "node dist-server/index.js", + "watch": "chokidar 'src/**' -c 'yarn build' --initial" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "next", + "@sveltejs/kit": "next", + "chokidar-cli": "^3.0.0", + "svelte": "^3.44.0", + "svelte-check": "^2.7.1", + "svelte-preprocess": "^4.10.6", + "tslib": "^2.3.1", + "typescript": "^4.7.4", + "vite": "^3.1.0" + }, + "type": "module", + "dependencies": { + "@fortawesome/free-brands-svg-icons": "^6.2.0", + "@fortawesome/free-solid-svg-icons": "^6.2.0", + "@pockethost/common": "0.0.1", + "@s-libs/micro-dash": "12", + "@sveltejs/adapter-node": "^1.0.0-next.92", + "pocketbase": "^0.7.0", + "random-word-slugs": "^0.1.6", + "sass": "^1.54.9", + "svelte-fa": "^3.0.3", + "svelte-highlight": "^6.2.1", + "sveltestrap": "^5.9.0" + } +} diff --git a/packages/pockethost.io/src/app.html b/packages/pockethost.io/src/app.html index 68cfb073..7e5d793d 100644 --- a/packages/pockethost.io/src/app.html +++ b/packages/pockethost.io/src/app.html @@ -1,25 +1,40 @@ - - - - + + + + - + - - - + + + - + - + - %sveltekit.head% - - -
%sveltekit.body%
+ %sveltekit.head% + + +
%sveltekit.body%
- - + + diff --git a/packages/pockethost.io/src/components/AlertBar.svelte b/packages/pockethost.io/src/components/AlertBar.svelte new file mode 100644 index 00000000..1c9154d3 --- /dev/null +++ b/packages/pockethost.io/src/components/AlertBar.svelte @@ -0,0 +1,21 @@ + + + diff --git a/packages/pockethost.io/src/components/AuthCheck.svelte b/packages/pockethost.io/src/components/AuthCheck.svelte index c9b74c67..cadb6256 100644 --- a/packages/pockethost.io/src/components/AuthCheck.svelte +++ b/packages/pockethost.io/src/components/AuthCheck.svelte @@ -1,9 +1,9 @@ diff --git a/packages/pockethost.io/src/components/Caption/Caption.svelte b/packages/pockethost.io/src/components/Caption/Caption.svelte index cb1cffb0..ffcd10bd 100644 --- a/packages/pockethost.io/src/components/Caption/Caption.svelte +++ b/packages/pockethost.io/src/components/Caption/Caption.svelte @@ -1,9 +1,9 @@ +
diff --git a/packages/pockethost.io/src/components/CodeSample.svelte b/packages/pockethost.io/src/components/CodeSample.svelte index 4ec85dc6..45b3b570 100644 --- a/packages/pockethost.io/src/components/CodeSample.svelte +++ b/packages/pockethost.io/src/components/CodeSample.svelte @@ -17,7 +17,7 @@ - \ No newline at end of file + .card { + border: 0; + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; + border-radius: 18px; + } + .card-icon { + background-color: #eee; + width: 35px; + height: 35px; + border-radius: 35px; + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + } + diff --git a/packages/pockethost.io/src/components/Gap.svelte b/packages/pockethost.io/src/components/Gap.svelte deleted file mode 100644 index 16fac2d3..00000000 --- a/packages/pockethost.io/src/components/Gap.svelte +++ /dev/null @@ -1,8 +0,0 @@ -
- - diff --git a/packages/pockethost.io/src/components/HomepageHeroAnimation.svelte b/packages/pockethost.io/src/components/HomepageHeroAnimation.svelte index e837d8bf..07fab005 100644 --- a/packages/pockethost.io/src/components/HomepageHeroAnimation.svelte +++ b/packages/pockethost.io/src/components/HomepageHeroAnimation.svelte @@ -1,43 +1,59 @@ -
- {#if !isReady} -
-

Creating Your New Instance...

-
- Loading... -
-
- {/if} + {#if !isReady} +
+

Creating Your New Instance...

+
+ Loading... +
+
+ {/if} - {#if isReady} -
- Screenshot of the Pocketbase Intro UI -
- {/if} + {#if isReady} +
+ Screenshot of the Pocketbase Intro UI +
+ {/if}
- \ No newline at end of file + } + diff --git a/packages/pockethost.io/src/components/InstanceGeneratorWidget.svelte b/packages/pockethost.io/src/components/InstanceGeneratorWidget.svelte new file mode 100644 index 00000000..b805b669 --- /dev/null +++ b/packages/pockethost.io/src/components/InstanceGeneratorWidget.svelte @@ -0,0 +1,154 @@ + + +{#if isProcessing} +
+
+ Loading... +
+ +
+

Creating Your New Instance...

+ +

{processingQuote}

+
+
+{:else} +

Create Your Instance Now

+ +
+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + + + +
+
+ +
+
+ +
+
+ + {#if formError} + + {/if} + +{/if} + + diff --git a/packages/pockethost.io/src/components/MediaQuery.svelte b/packages/pockethost.io/src/components/MediaQuery.svelte new file mode 100644 index 00000000..688b0200 --- /dev/null +++ b/packages/pockethost.io/src/components/MediaQuery.svelte @@ -0,0 +1,42 @@ + + + diff --git a/packages/pockethost.io/src/components/Navbar.svelte b/packages/pockethost.io/src/components/Navbar.svelte index ec789867..a2290d2e 100644 --- a/packages/pockethost.io/src/components/Navbar.svelte +++ b/packages/pockethost.io/src/components/Navbar.svelte @@ -1,80 +1,153 @@ - - - - </NavbarBrand - > - <NavbarToggler on:click={() => (isOpen = !isOpen)} /> - <Collapse {isOpen} navbar expand="md" on:update={handleUpdate}> - <Nav class="ms-auto" navbar> - {#if isLoggedIn()} - <NavItem /> - <NavItem> - <NavLink href="/dashboard">Dashboard</NavLink> - </NavItem> - <NavItem /> - <Dropdown nav inNavbar> - <DropdownToggle nav><Icon name="person-fill" /></DropdownToggle> - <DropdownMenu end> - <DropdownItem> - <NavbarText>{user()?.email}</NavbarText> - </DropdownItem> - <DropdownItem divider /> - <DropdownItem><NavLink on:click={handleLogout}>Logout</NavLink></DropdownItem> - </DropdownMenu> - </Dropdown> - {/if} - {#if !isLoggedIn()} - <NavItem> - <NavLink href="/signup">Sign up</NavLink> - </NavItem> - <NavItem> - <NavLink href="/login">Log in</NavLink> - </NavItem> - {/if} - <NavItem> - <NavLink href="https://github.com/benallfree/pockethost"><Github /></NavLink> - </NavItem> - </Nav> - </Collapse> -</Navbar> +<header class="container-fluid"> + <nav class="navbar navbar-expand-md"> + <a href="/" class="logo text-decoration-none d-flex align-items-center"> + <img src="/images/logo-square.png" alt="PocketHost Logo" class="img-fluid" /> + <h1>Pocket<span>Host</span></h1> + </a> + + <button + class="btn btn-light mobile-nav-button navbar-toggler" + type="button" + data-bs-toggle="collapse" + data-bs-target="#nav-links" + aria-controls="nav-links" + aria-expanded="false" + aria-label="Toggle navigation" + > + <i class="bi bi-list" /> + </button> + + <div class="collapse navbar-collapse" id="nav-links"> + <ul class="navbar-nav ms-auto mb-2 mb-md-0"> + {#if isLoggedIn()} + <li class="nav-item text-md-start text-center"> + <a class="nav-link" href="/dashboard">Dashboard</a> + </li> + + <MediaQuery query="(min-width: 768px)" let:matches> + {#if matches} + <li class="nav-item dropdown"> + <a + class="nav-link" + href="#" + role="button" + data-bs-toggle="dropdown" + aria-expanded="false" + > + <i class="bi bi-person-circle" /> + </a> + + <ul class="dropdown-menu dropdown-menu-end"> + <li><a class="dropdown-item" href="/profile">Profile</a></li> + <li><a class="dropdown-item" href="/settings">Settings</a></li> + <li><hr class="dropdown-divider" /></li> + <li><a class="dropdown-item" href="/" on:click={handleLogout}>Logout</a></li> + </ul> + </li> + {:else} + <li class="nav-item"> + <a class="nav-link text-md-start text-center" href="/profile">Profile</a> + </li> + <li class="nav-item"> + <a class="nav-link text-md-start text-center" href="/settings">Settings</a> + </li> + <li class="nav-item"> + <a class="nav-link text-md-start text-center" href="/" on:click={handleLogout} + >Logout</a + > + </li> + {/if} + </MediaQuery> + {/if} + + {#if !isLoggedIn()} + <li class="nav-item"> + <a class="nav-link text-md-start text-center" href="/signup">Sign up</a> + </li> + + <li class="nav-item"> + <a class="nav-link text-md-start text-center" href="/login">Log in</a> + </li> + {/if} + + <li class="nav-item"> + <a + class="nav-link text-md-start text-center" + href="https://github.com/benallfree/pockethost" + target="_blank" + aria-label="Link to our Github Project" + rel="noopener" + > + <i class="bi bi-github" /><span class="nav-github-link">Github</span> + </a> + </li> + </ul> + </div> + </nav> +</header> <style lang="scss"> + header { + background-color: #fff; + padding: 12px 24px; + border-bottom: 1px solid #eee; + } + + .logo { + img { + max-width: 50px; + margin-right: 16px; + } + + h1 { + font-size: 36px; + font-weight: 300; + margin: 0; + color: #222; + + span { + font-weight: 700; + background-image: linear-gradient( + 83.2deg, + rgba(150, 93, 233, 1) 10.8%, + rgba(99, 88, 238, 1) 94.3% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + } + } + + .mobile-nav-button { + font-size: 20px; + } + + .nav-link { + font-weight: 500; + } + + .nav-github-link { + display: inline-block; + margin-left: 4px; + } + + @media screen and (min-width: 768px) { + .nav-github-link { + display: none; + } + } </style> diff --git a/packages/pockethost.io/src/components/NavbarBrandImage.svelte b/packages/pockethost.io/src/components/NavbarBrandImage.svelte deleted file mode 100644 index 3daadf75..00000000 --- a/packages/pockethost.io/src/components/NavbarBrandImage.svelte +++ /dev/null @@ -1,12 +0,0 @@ -<script lang="ts"> - export let logo: string -</script> - -<img class="logo d-inline-block align-text-top" src={logo} /> - -<style lang="scss"> - img.logo { - height: 30px; - margin-right: 10px; - } -</style> diff --git a/packages/pockethost.io/src/components/NavbarText.svelte b/packages/pockethost.io/src/components/NavbarText.svelte deleted file mode 100644 index f0bb85f2..00000000 --- a/packages/pockethost.io/src/components/NavbarText.svelte +++ /dev/null @@ -1 +0,0 @@ -<div class="navbar-text"><slot /></div> diff --git a/packages/pockethost.io/src/components/ProvisioningStatus/ProvisioningStatus.svelte b/packages/pockethost.io/src/components/ProvisioningStatus/ProvisioningStatus.svelte index c360f122..0e8fe8e8 100644 --- a/packages/pockethost.io/src/components/ProvisioningStatus/ProvisioningStatus.svelte +++ b/packages/pockethost.io/src/components/ProvisioningStatus/ProvisioningStatus.svelte @@ -2,8 +2,11 @@ import { InstanceStatus } from '@pockethost/common/src/schema' import { ProvisioningSize } from './types' - export let status: InstanceStatus = InstanceStatus.Unknown + export let status: InstanceStatus = InstanceStatus.Idle export let size: ProvisioningSize = ProvisioningSize.Normal + if (!status) { + status = InstanceStatus.Idle + } </script> <div class={`status ${status} ${size}`}>{status}</div> diff --git a/packages/pockethost.io/src/components/Title/Title.svelte b/packages/pockethost.io/src/components/Title/Title.svelte index 7170721a..f410f859 100644 --- a/packages/pockethost.io/src/components/Title/Title.svelte +++ b/packages/pockethost.io/src/components/Title/Title.svelte @@ -9,22 +9,24 @@ <h1 class={size}>{first}<span id="host">{second}</span>{third}</h1> -<style type="text/scss"> +<style lang="scss"> h1 { color: #ff3e00; font-size: 30px; - font-weight: 100; margin-left: auto; margin-right: auto; display: block; text-align: center; + font-weight: 300; + #host { - color: blue; - } - &.nav { - font-size: 15px; - font-weight: 400; - display: inline-block; + background-image: linear-gradient( + 83.2deg, + rgba(150, 93, 233, 1) 10.8%, + rgba(99, 88, 238, 1) 94.3% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } } </style> diff --git a/packages/common/src/pocketbase.ts b/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts similarity index 64% rename from packages/common/src/pocketbase.ts rename to packages/pockethost.io/src/pocketbase/PocketbaseClient.ts index 2622e642..590f4b6a 100644 --- a/packages/common/src/pocketbase.ts +++ b/packages/pockethost.io/src/pocketbase/PocketbaseClient.ts @@ -1,12 +1,8 @@ -import { map } from '@s-libs/micro-dash' -import PocketBase, { - BaseAuthStore, - ClientResponseError, - Record, -} from 'pocketbase' +import type { InstanceId, Instance_In, Instance_Out } from '@pockethost/common' +import { createRealtimeSubscriptionManager } from '@pockethost/common' +import { keys, map } from '@s-libs/micro-dash' +import PocketBase, { BaseAuthStore, ClientResponseError, Record } from 'pocketbase' import type { Unsubscriber } from 'svelte/store' -import { createRealtimeSubscriptionManager } from './RealtimeSubscriptionManager' -import type { InstanceId, Instance_In, Instance_Out } from './schema' export const createPocketbaseClient = (url: string) => { const client = new PocketBase(url) @@ -19,8 +15,7 @@ export const createPocketbaseClient = (url: string) => { const isLoggedIn = () => authStore.isValid - const onAuthChange = (cb: (user: BaseAuthStore) => Unsubscriber) => - onChange(() => cb(authStore)) + const onAuthChange = (cb: (user: BaseAuthStore) => Unsubscriber) => onChange(() => cb(authStore)) const logOut = () => authStore.clear() @@ -28,29 +23,22 @@ export const createPocketbaseClient = (url: string) => { client.users.create({ email, password, - passwordConfirm: password, + passwordConfirm: password }) const authViaEmail = (email: string, password: string) => client.users.authViaEmail(email, password) const createInstance = (payload: Instance_In): Promise<Instance_Out> => { - return client.records - .create('instances', payload) - .then((r) => r as unknown as Instance_Out) + return client.records.create('instances', payload).then((r) => r as unknown as Instance_Out) } const getInstanceById = (id: InstanceId): Promise<Instance_Out | undefined> => - client.records - .getOne('instances', id) - .then((r) => r as unknown as Instance_Out) + client.records.getOne('instances', id).then((r) => r as unknown as Instance_Out) const subscribe = createRealtimeSubscriptionManager(client) - const watchInstanceById = ( - id: InstanceId, - cb: (rec: Instance_Out) => void - ): Unsubscriber => { + const watchInstanceById = (id: InstanceId, cb: (rec: Instance_Out) => void): Unsubscriber => { const slug = `instances/${id}` getInstanceById(id).then((v) => { if (!v) return @@ -75,24 +63,16 @@ export const createPocketbaseClient = (url: string) => { console.log(`${instanceId} setting fields`, { fields }) return client.records.update('instances', instanceId, fields).catch((e) => { console.error(`setInstance failed for ${instanceId} with ${e}`, { - fields, + fields }) throw e }) } - const parseError = (e: any): string[] => { - if (e instanceof ClientResponseError) { - const { data } = e - if (!data || !data.data) { - return [`Unknown error ${e.message}`] - } - return map(data.data, (v, k) => (v ? v.message : undefined)).filter( - (v) => !!v - ) - } else { - return [`Unknown error ${e.message}`] - } + const parseError = (e: Error): string[] => { + if (!(e instanceof ClientResponseError)) return [e.message] + if (e.data.message && keys(e.data.data).length === 0) return [e.data.message] + return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter((v) => !!v) } return { @@ -108,6 +88,6 @@ export const createPocketbaseClient = (url: string) => { user, watchInstanceById, getAllInstancesById, - setInstance, + setInstance } } diff --git a/packages/pockethost.io/src/pocketbase.ts b/packages/pockethost.io/src/pocketbase/index.ts similarity index 77% rename from packages/pockethost.io/src/pocketbase.ts rename to packages/pockethost.io/src/pocketbase/index.ts index 13402809..8a15e817 100644 --- a/packages/pockethost.io/src/pocketbase.ts +++ b/packages/pockethost.io/src/pocketbase/index.ts @@ -1,5 +1,5 @@ import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$env/static/public' -import { createPocketbaseClient } from '@pockethost/common' +import { createPocketbaseClient } from './PocketbaseClient' const url = `https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}` const client = createPocketbaseClient(url) diff --git a/packages/pockethost.io/src/routes/+layout.svelte b/packages/pockethost.io/src/routes/+layout.svelte index f7de277e..18425ac8 100644 --- a/packages/pockethost.io/src/routes/+layout.svelte +++ b/packages/pockethost.io/src/routes/+layout.svelte @@ -9,7 +9,4 @@ </main> <style lang="scss"> - main { - margin-top: 20px; - } </style> diff --git a/packages/pockethost.io/src/routes/+page.svelte b/packages/pockethost.io/src/routes/+page.svelte index b9c09698..d77462b3 100644 --- a/packages/pockethost.io/src/routes/+page.svelte +++ b/packages/pockethost.io/src/routes/+page.svelte @@ -1,29 +1,41 @@ <script lang="ts"> - import HomepageHeroAnimation from "$components/HomepageHeroAnimation.svelte"; - import FeatureCard from "$components/FeatureCard.svelte"; + import FeatureCard from '$components/FeatureCard.svelte' + import HomepageHeroAnimation from '$components/HomepageHeroAnimation.svelte' + import InstanceGeneratorWidget from '$components/InstanceGeneratorWidget.svelte' + import { client } from '$src/pocketbase' + + const { isLoggedIn } = client </script> - - <div class="container"> <div class="row align-items-center justify-content-between hero"> - <div class="col-lg-6"> + <div class="col-lg-6 mb-5 mb-lg-0"> <h2>Deploy <span>PocketBase</span> in 30 seconds</h2> - <p>Spend less time on configuring your backend, and more time building new features for your web app.</p> + <p class="mb-5"> + Spend less time on configuring your backend, and more time building new features for your + web app. + </p> - <div> - <a href="/signup" class="btn btn-primary">Get Started for Free</a> - </div> + {#if isLoggedIn()} + <div> + <a href="/dashboard" class="btn btn-primary" + >Go to Your Dashboard <i class="bi bi-arrow-right-short" /></a + > + </div> + {/if} + + {#if !isLoggedIn()} + <InstanceGeneratorWidget /> + {/if} </div> - <div class="col-lg-5"> + <div class="col-lg-5 d-none d-sm-block"> <HomepageHeroAnimation /> </div> </div> </div> - <div class="features"> <div class="container"> <h2 class="mb-5">Features</h2> @@ -31,7 +43,10 @@ <div class="row"> <div class="col-12 col-md-6 col-lg-4 mb-4"> <FeatureCard title="Up in 30 seconds" icon="bi bi-stopwatch" fullHeight={true}> - <p>A backend for your next app is as fast as signing up. No provisioning servers, no Docker fiddling, just B(ad)aaS productivity.</p> + <p> + A backend for your next app is as fast as signing up. No provisioning servers, no Docker + fiddling, just B(ad)aaS productivity. + </p> <ul> <li>Sign up</li> @@ -43,44 +58,73 @@ <div class="col-12 col-md-6 col-lg-4 mb-4"> <FeatureCard title="Zero Config" icon="bi bi-check-lg" fullHeight={true}> - <p>With PocketHost, batteries are included. You get a database, outgoing email, SSL, authentication, cloud functions, and high concurrency all in one stop.</p> + <p> + With PocketHost, batteries are included. You get a database, outgoing email, SSL, + authentication, cloud functions, and high concurrency all in one stop. + </p> </FeatureCard> </div> <div class="col-12 col-md-6 col-lg-4 mb-4"> <FeatureCard title="Database" icon="bi bi-hdd-stack" fullHeight={true}> - <p>Your PocketHost instance is powered by its own internal SQLite instance. SQLite is <a href="https://pocketbase.io/faq/" target="_blank">more performant than mySQL or Postgres</a> and is <a href="https://www.sqlite.org/whentouse.html" target="_blank">perfect for powering your next app</a>.</p> + <p> + Your PocketHost instance is powered by its own internal SQLite instance. SQLite is <a + href="https://pocketbase.io/faq/" + target="_blank">more performant than mySQL or Postgres</a + > + and is + <a href="https://www.sqlite.org/whentouse.html" target="_blank" + >perfect for powering your next app</a + >. + </p> </FeatureCard> </div> <div class="col-12 col-md-6 col-lg-3 mb-4"> <FeatureCard title="Auth" icon="bi bi-shield-lock" fullHeight={true}> - <p>Email and oAuth authentication options work out of the box. Send transactional email to your users from our verified domain and your custom address <code>yoursubdomin@pockethost.local</code>.</p> + <p> + Email and oAuth authentication options work out of the box. Send transactional email to + your users from our verified domain and your custom address <code + >yoursubdomin@pockethost.local</code + >. + </p> </FeatureCard> </div> <div class="col-12 col-md-6 col-lg-3 mb-4"> <FeatureCard title="Storage" icon="bi bi-archive" fullHeight={true}> - <p>PocketHost securely stores your files on Amazon S3, or you can use your own key to manage your own storage.</p> + <p> + PocketHost securely stores your files on Amazon S3, or you can use your own key to + manage your own storage. + </p> </FeatureCard> </div> <div class="col-12 col-md-6 col-lg-3 mb-4"> <FeatureCard title="Room to Grow" icon="bi bi-cloud-arrow-up" fullHeight={true}> <p>PocketHost is perfect for hobbist, low, and medium volume sites and apps.</p> - <p>PocketHost, and the underlying PocketBase, can scale to well over 10,000 simultaneous connections.</p> + <p> + PocketHost, and the underlying PocketBase, can scale to well over 10,000 simultaneous + connections. + </p> </FeatureCard> </div> <div class="col-12 col-md-6 col-lg-3 mb-4"> <FeatureCard title="Self-host" icon="bi bi-house-door" fullHeight={true}> - <p>When you're ready to take your project in-house, we have you covered. You can export your entire PocketHost environment along with a Dockerfile to run it.</p> + <p> + When you're ready to take your project in-house, we have you covered. You can export + your entire PocketHost environment along with a Dockerfile to run it. + </p> </FeatureCard> </div> <div class="col-12 col-md-6 col-lg-6 mb-4"> <FeatureCard title="Open Source Stack" icon="bi bi-code-slash" fullHeight={true}> - <p>PocketHost is powered by Svelte, Vite, Typescript, PocketBase, and SQLite. Because the entire stack is open source, you'll never be locked into the whims of a vendor.</p> + <p> + PocketHost is powered by Svelte, Vite, Typescript, PocketBase, and SQLite. Because the + entire stack is open source, you'll never be locked into the whims of a vendor. + </p> </FeatureCard> </div> @@ -97,28 +141,41 @@ </div> </div> - <style> .hero { - padding: 100px 0; + padding: 50px 0; } .hero h2 { - font-size: 65px; + font-size: 35px; } .hero h2 span { - background-image: linear-gradient( 83.2deg, rgba(150,93,233,1) 10.8%, rgba(99,88,238,1) 94.3% ); + background-image: linear-gradient( + 83.2deg, + rgba(150, 93, 233, 1) 10.8%, + rgba(99, 88, 238, 1) 94.3% + ); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .features { - background-image: linear-gradient( 179.4deg, rgb(252, 239, 233) 2.2%, rgb(211, 242, 185) 96.2% ); + background-image: linear-gradient(179.4deg, rgb(252, 239, 233) 2.2%, rgb(211, 242, 185) 96.2%); padding: 120px 0; } .features h2 { font-size: 56px; } -</style> \ No newline at end of file + + @media screen and (min-width: 768px) { + .hero { + padding: 100px 0; + } + + .hero h2 { + font-size: 65px; + } + } +</style> diff --git a/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte b/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte index 34c494d9..f1b528ee 100644 --- a/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte +++ b/packages/pockethost.io/src/routes/app/instances/[instanceId]/+page.svelte @@ -1,16 +1,14 @@ <script lang="ts"> import { page } from '$app/stores' import Button from '$components/Button/Button.svelte' - import Caption from '$components/Caption/Caption.svelte' import CodeSample from '$components/CodeSample.svelte' import Protected from '$components/Protected.svelte' import ProvisioningStatus from '$components/ProvisioningStatus/ProvisioningStatus.svelte' - import { ProvisioningSize } from '$components/ProvisioningStatus/types' import Title from '$components/Title/Title.svelte' import { PUBLIC_PB_DOMAIN } from '$env/static/public' import { client } from '$src/pocketbase' import { assertExists } from '@pockethost/common/src/assert' - import { InstanceStatus, type Instance_Out } from '@pockethost/common/src/schema' + import type { Instance_Out } from '@pockethost/common/src/schema' import { onDestroy, onMount } from 'svelte' import type { Unsubscriber } from 'svelte/store' @@ -33,31 +31,21 @@ }) }) onDestroy(() => unsub()) - const isRunning = (instance: Instance_Out) => - instance.status === InstanceStatus.Running || instance.status === InstanceStatus.Idle </script> <Protected> <main> <Title /> {#if instance} - {#if isRunning(instance)} - <ProvisioningStatus status={instance.status} /> + <ProvisioningStatus status={instance.status} /> - <div> - Admin URL: <a href={`${url}/_`} target="_blank">{`${url}/_`}</a> - </div> - <div> - JavaScript: - <CodeSample {code} /> - </div> - {/if} - {#if !isRunning} - <Caption>Please stand by, your instance is starting now...</Caption> - <div class="provisioning"> - <ProvisioningStatus status={instance.status} size={ProvisioningSize.Hero} /> - </div> - {/if} + <div> + Admin URL: <a href={`${url}/_`} target="_blank">{`${url}/_`}</a> + </div> + <div> + JavaScript: + <CodeSample {code} /> + </div> {/if} <Button href="/dashboard">< Back to Dashboard</Button> </main> diff --git a/packages/pockethost.io/src/routes/app/new/+page.svelte b/packages/pockethost.io/src/routes/app/new/+page.svelte index 6d5c9c47..9135d9ff 100644 --- a/packages/pockethost.io/src/routes/app/new/+page.svelte +++ b/packages/pockethost.io/src/routes/app/new/+page.svelte @@ -59,6 +59,7 @@ <input class="subdomain" name="instanceName" + id="instanceName" type="text" bind:value={instanceName} />.{PUBLIC_PB_DOMAIN} @@ -69,7 +70,7 @@ </main> </Protected> -<style type="text/scss"> +<style lang="scss"> main { padding: 1em; margin-left: auto; diff --git a/packages/pockethost.io/src/routes/dashboard/+page.svelte b/packages/pockethost.io/src/routes/dashboard/+page.svelte index d6a7dd4d..dffcd526 100644 --- a/packages/pockethost.io/src/routes/dashboard/+page.svelte +++ b/packages/pockethost.io/src/routes/dashboard/+page.svelte @@ -1,17 +1,12 @@ <script lang="ts"> import Button from '$components/Button/Button.svelte' import { ButtonSizes } from '$components/Button/types' - import Gap from '$components/Gap.svelte' import Protected from '$components/Protected.svelte' import ProvisioningStatus from '$components/ProvisioningStatus/ProvisioningStatus.svelte' import Title from '$components/Title/Title.svelte' import { PUBLIC_PB_DOMAIN } from '$env/static/public' import { client } from '$src/pocketbase' - import { - InstanceStatus, - type Instance_Out, - type Instance_Out_ByIdCollection - } from '@pockethost/common/src/schema' + import type { Instance_Out_ByIdCollection } from '@pockethost/common/src/schema' import { forEach, values } from '@s-libs/micro-dash' import { onDestroy, onMount } from 'svelte' import type { Unsubscriber } from 'svelte/store' @@ -19,8 +14,6 @@ const { getAllInstancesById, watchInstanceById } = client let apps: Instance_Out_ByIdCollection = {} - const isRunning = (app: Instance_Out) => - app.status === InstanceStatus.Running || app.status === InstanceStatus.Idle let unsubs: Unsubscriber[] = [] onMount(() => { @@ -56,14 +49,15 @@ <ProvisioningStatus status={app.status} /> </Col> <Col> - {app.subdomain}.{PUBLIC_PB_DOMAIN} + <div class="nowrap"> + {app.subdomain} + </div> </Col> <Col> <Button size={ButtonSizes.Micro} href={`/app/instances/${app.id}`}>Details</Button> <Button - disabled={!isRunning(app)} size={ButtonSizes.Micro} click={() => { window.open(`https://${app.subdomain}.${PUBLIC_PB_DOMAIN}/_`) @@ -73,14 +67,14 @@ </Row> {/each} </Container> - <Gap /> + <div class="newApp"> <Button href="/app/new" size={ButtonSizes.Wide}>+ New App</Button> </div> </main> </Protected> -<style type="text/scss"> +<style lang="scss"> main { margin-top: 10px; margin-right: auto; @@ -91,6 +85,9 @@ h2 { text-align: center; } + .nowrap { + white-space: nowrap; + } .newApp { width: 200px; margin-left: auto; diff --git a/packages/pockethost.io/src/routes/login/+page.svelte b/packages/pockethost.io/src/routes/login/+page.svelte index 48d75330..e5acae04 100644 --- a/packages/pockethost.io/src/routes/login/+page.svelte +++ b/packages/pockethost.io/src/routes/login/+page.svelte @@ -1,54 +1,103 @@ <script lang="ts"> - import Button from '$components/Button/Button.svelte' - import Title from '$components/Title/Title.svelte' - import { client } from '$src/pocketbase' - import { redirect } from '$util/redirect' - import { Form, FormGroup, Input, Label } from 'sveltestrap' + import { handleLogin } from '$util/database' + import AlertBar from '$components/AlertBar.svelte' - let email = '' - let password = '' - let loginError = '' + let email: string = '' + let password: string = '' + let formError: string = '' - const { authViaEmail } = client + let isFormButtonDisabled: boolean = true + $: isFormButtonDisabled = email.length === 0 || password.length === 0 - const handleLogin = () => { - loginError = '' - authViaEmail(email, password) - .then((user) => { - console.log(user) - redirect('/dashboard') - }) - .catch((e) => { - loginError = e.message - }) + const handleSubmit = async (e: SubmitEvent) => { + e.preventDefault() + + isFormButtonDisabled = true + + await handleLogin(email, password, (error) => { + formError = error + }) + + isFormButtonDisabled = false } </script> -<Title first="Log" second="in" /> +<div class="page-bg"> + <div class="card"> + <h2 class="mb-4">Login</h2> -<main> - <error>{loginError}</error> - <Form> - <FormGroup> - <Label for="email">Email</Label> - <Input type="email" id="email" bind:value={email} /> - </FormGroup> - <FormGroup> - <Label for="password">Password</Label> - <Input type="password" id="password" bind:value={password} /> - </FormGroup> - </Form> + <form on:submit={handleSubmit}> + <div class="form-floating mb-3"> + <input + type="email" + class="form-control" + id="email" + placeholder="name@example.com" + bind:value={email} + required + autocomplete="email" + /> + <label for="email">Email address</label> + </div> - <div> - Need to <a href="/signup">create an account</a>? + <div class="form-floating mb-3"> + <input + type="password" + class="form-control" + id="password" + placeholder="Password" + bind:value={password} + required + autocomplete="current-password" + /> + <label for="password">Password</label> + </div> + + {#if formError} + <AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} /> + {/if} + + <button type="submit" class="btn btn-primary w-100" disabled={isFormButtonDisabled}> + Log In <i class="bi bi-arrow-right-short" /> + </button> + </form> + + <div class="py-4"><hr /></div> + + <div class="text-center"> + Need to <a href="/signup">create an account</a>? + </div> </div> - <Button click={handleLogin} disabled={email.length === 0 || password.length === 0}>Log In</Button> -</main> +</div> -<style type="text/scss"> - main { - max-width: 300px; - margin-left: auto; - margin-right: auto; +<style lang="scss"> + .page-bg { + background-color: #222; + background-image: linear-gradient( + 109.6deg, + rgba(125, 89, 252, 1) 11.2%, + rgba(218, 185, 252, 1) 91.1% + ); + display: flex; + align-items: center; + justify-content: center; + height: calc(100vh - 91px); + padding: 0 18px; + } + + .card { + border: 0; + background-color: #fff; + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; + padding: 24px; + max-width: 425px; + width: 100%; + border-radius: 24px; + } + + @media screen and (min-width: 768px) { + .card { + padding: 48px; + } } </style> diff --git a/packages/pockethost.io/src/routes/shh/+page.svelte b/packages/pockethost.io/src/routes/shh/+page.svelte index b0749eab..ac435f4c 100644 --- a/packages/pockethost.io/src/routes/shh/+page.svelte +++ b/packages/pockethost.io/src/routes/shh/+page.svelte @@ -1,25 +1,25 @@ <script lang="ts"> import Title from '$components/Title/Title.svelte' import { TitleSize } from '$components/Title/types' - - </script> <Title first="So" second="Much" third="Quiet" size={TitleSize.Normal} /> <main> - <img src="https://media4.giphy.com/media/V9sdMLcmIFqqk/giphy.gif?cid=790b76118f409453704f5eaabaea1a3dc7380a9daf4fca63&rid=giphy.gif&ct=g"/> + <img + src="https://media4.giphy.com/media/V9sdMLcmIFqqk/giphy.gif?cid=790b76118f409453704f5eaabaea1a3dc7380a9daf4fca63&rid=giphy.gif&ct=g" + /> <h3>The PocketHost instance you are seeking does not exist.</h3> <p>Please check the instance URL and try again.</p> </main> -<style type="text/scss"> +<style lang="scss"> main { max-width: 600px; margin-left: auto; margin-right: auto; img { - width: 100% + width: 100%; } .caption { diff --git a/packages/pockethost.io/src/routes/signup/+page.svelte b/packages/pockethost.io/src/routes/signup/+page.svelte index 84dd5f47..e60e62c4 100644 --- a/packages/pockethost.io/src/routes/signup/+page.svelte +++ b/packages/pockethost.io/src/routes/signup/+page.svelte @@ -1,95 +1,110 @@ <script lang="ts"> - import Button from '$components/Button/Button.svelte' - import Error from '$components/Error/Error.svelte' - import Title from '$components/Title/Title.svelte' - import { TitleSize } from '$components/Title/types' - import { client } from '$src/pocketbase' - import { redirect } from '$util/redirect' - import { Form, FormGroup, Input, Label } from 'sveltestrap' + import { handleRegistration, handleLogin, handleFormError } from '$util/database' + import AlertBar from '$components/AlertBar.svelte' - const { authViaEmail, createUser, parseError } = client - let email = '' - let emailError: string[] = [] - let password = '' - let passwordError = '' + let email: string = '' + let password: string = '' + let formError: string = '' - // client.users - // .authViaEmail('ben@benallfree.com', 'Dhjb2X6C1y0W') - // .then((u) => { - // console.log(`user logged in`, u) - // window.location.href = '/dashboard' - // }) - // .catch((e) => console.error(`user login error`, e)) + let isFormButtonDisabled: boolean = true + $: isFormButtonDisabled = email.length === 0 || password.length === 0 - const handleSignup = () => { - emailError = [] - passwordError = '' - createUser(email, password) - .then((user) => { - console.log({ user }) + const handleSubmit = async (e: SubmitEvent) => { + e.preventDefault() - authViaEmail(email, password) - .then((u) => { - console.log(`user logged in`, u) - redirect('/dashboard') - }) - .catch((e) => console.error(`user login error`, e)) - }) - .catch((e) => { - emailError = parseError(e) - console.error(emailError.join('\n'), e) + isFormButtonDisabled = true + + try { + await handleRegistration(email, password) + + // Go ahead and log the user into the site + await handleLogin(email, password) + } catch (error: any) { + handleFormError(error, (error) => { + formError = error }) + } + + isFormButtonDisabled = false } </script> -<Title first="Sign" second="up" size={TitleSize.Normal} /> +<div class="page-bg"> + <div class="card"> + <h2 class="mb-4">Sign Up</h2> -<main> - <Form> - <FormGroup> - <Label for="email">Email Address</Label> - <Input type="email" bind:value={email} id="email" /> - <Error>{emailError.join('<br/>')}</Error> - </FormGroup> - <FormGroup> - <Label for="password">Password</Label> - <Input type="password" name="password" id="password" bind:value={password} /> - <Error>{passwordError}</Error> - </FormGroup> - </Form> - <Button click={handleSignup} disabled={email.length === 0 || password.length === 0}> - Sign Up - </Button> - <div> - Already have an account? <a href="/login">Log in</a> + <form on:submit={handleSubmit}> + <div class="form-floating mb-3"> + <input + type="email" + class="form-control" + id="email" + placeholder="name@example.com" + bind:value={email} + required + autocomplete="email" + /> + <label for="email">Email address</label> + </div> + + <div class="form-floating mb-3"> + <input + type="password" + class="form-control" + id="password" + placeholder="Password" + bind:value={password} + required + autocomplete="new-password" + /> + <label for="password">Password</label> + </div> + + {#if formError} + <AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} /> + {/if} + + <button type="submit" class="btn btn-primary w-100" disabled={isFormButtonDisabled}> + Sign Up <i class="bi bi-arrow-right-short" /> + </button> + </form> + + <div class="py-4"><hr /></div> + + <div class="text-center"> + Already have an account? <a href="/login">Log in</a> + </div> </div> -</main> +</div> -<style type="text/scss"> - main { - max-width: 300px; - margin-left: auto; - margin-right: auto; - error { - color: red; - display: block; - } +<style lang="scss"> + .page-bg { + background-color: #222; + background-image: linear-gradient( + 109.6deg, + rgba(218, 185, 252, 1) 11.2%, + rgba(125, 89, 252, 1) 91.1% + ); + display: flex; + align-items: center; + justify-content: center; + height: calc(100vh - 91px); + padding: 0 18px; + } - label { - display: block; - font-weight: bold; - width: 200px; - } - main { - padding: 1em; - margin-left: auto; - margin-right: auto; - } + .card { + border: 0; + background-color: #fff; + box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px; + padding: 24px; + max-width: 425px; + width: 100%; + border-radius: 24px; + } - .caption { - font-size: 30px; - margin-top: 20px; - margin-bottom: 20px; + @media screen and (min-width: 768px) { + .card { + padding: 48px; } } </style> diff --git a/packages/pockethost.io/src/util/database.ts b/packages/pockethost.io/src/util/database.ts new file mode 100644 index 00000000..bf91947e --- /dev/null +++ b/packages/pockethost.io/src/util/database.ts @@ -0,0 +1,170 @@ +import { client } from '$src/pocketbase' +import { InstanceStatus } from '@pockethost/common' +import { redirect } from './redirect' +const { authViaEmail, createUser, user, createInstance } = client + +export type FormErrorHandler = (value: string) => void + +export const handleFormError = (error: any, setError?: FormErrorHandler) => { + console.error(`Form error: ${error}`, { error }) + if (setError) { + const message = client.parseError(error)[0] + setError(message) + } else { + throw error + } +} + +/** + * This will log a user into Pocketbase, and includes an optional error handler + * @param email {string} The email of the user + * @param password {string} The password of the user + * @param setError {function} This can be used to show an alert bar if an error occurs during the login process + * @param shouldRedirect {boolean} This will redirect the user to the dashboard when they are logged in + */ +export const handleLogin = async ( + email: string, + password: string, + setError?: FormErrorHandler, + shouldRedirect: boolean = true +) => { + // Reset the form error if the form is submitted + setError?.('') + + try { + await authViaEmail(email, password) + + if (shouldRedirect) { + redirect('/dashboard') + } + } catch (error: any) { + handleFormError(error, setError) + } +} + +/** + * This will register a new user into Pocketbase, and includes an optional error handler + * @param email {string} The email of the user + * @param password {string} The password of the user + * @param setError {function} This can be used to show an alert bar if an error occurs during the login process + */ +export const handleRegistration = async ( + email: string, + password: string, + setError?: FormErrorHandler +) => { + // Reset the form error if the form is submitted + setError?.('') + + try { + await createUser(email, password) + } catch (error: any) { + handleFormError(error, setError) + } +} + +export const handleCreateNewInstance = async ( + instanceName: string, + setError?: FormErrorHandler +) => { + // Get the newly created user id + const { id } = user() || {} + + try { + // Prechecks + if (!instanceName) throw new Error(`Instance name is required`) + if (!id) throw new Error(`Must be logged in to create an instance`) + + // Create a new instance using the generated name + const record = await createInstance({ + subdomain: instanceName, + uid: id, + status: InstanceStatus.Idle + }) + + redirect(`/app/instances/${record.id}`) + } catch (error: any) { + handleFormError(error, setError) + } +} + +export const handleInstanceGeneratorWidget = async ( + email: string, + password: string, + instanceName: string, + setError = (value: string) => {} +) => { + try { + // Handle user creation/signin + // First, attempt to log in using the provided credentials. + // If they have a password manager or anything like that, it will have + // populated the form with their existing login. Try using it. + await handleLogin(email, password, undefined, false) + .then(() => { + console.log(`Account ${email} already exists. Logged in.`) + }) + .catch((e) => { + console.warn(`Login failed, attempting account creation.`) + // This means login has failed. + // Either their credentials were incorrect, or the account + // did not exist, or there is a system issue. + // Try creating the account. This will fail if the email address + // is already in use. + return handleRegistration(email, password) + .then(() => { + console.log(`Account created, proceeding to log in.`) + // This means registration succeeded. That's good. + // Log in using the new credentials + return handleLogin(email, password, undefined, false) + .then(() => { + console.log(`Logged in after account creation`) + }) + .catch((e) => { + console.error(`Panic, auth system down`) + // This should never happen. + // If registration succeeds, login should always succeed. + // If a login fails at this point, the system is broken. + throw new Error( + `Login system is currently down. Please contact us so we can fix this.` + ) + }) + }) + .catch((e) => { + console.warn(`User input error`) + // This is just for clarity + // If registration fails at this point, it means both + // login and account creation failed. + // This means there is something wrong with the user input. + // Bail out to show errors + // Transform the errors so they mention a problem with account creation. + const messages = client.parseError(e) + throw new Error(`Account creation: ${messages[0]}`) + }) + }) + + console.log(`User before instance creation is `, user()) + // We can only get here if we are successfully logged in using the credentials + // provided by the user. + // Instance creation could still fail if the name is taken + await handleCreateNewInstance(instanceName) + .then(() => { + console.log(`Creation of ${instanceName} succeeded`) + }) + .catch((e) => { + console.warn(`Creation of ${instanceName} failed`) + // The instance creation could most likely fail if the name is taken. + // In any case, bail out to show errors. + if (e.data?.data?.subdomain?.code === 'validation_not_unique') { + // Handle this special and common case + throw new Error(`Instance name already taken.`) + } + // The errors remaining errors are kind of generic, so transofrm them into something about + // the instance name. + const messages = client.parseError(e) + throw new Error(`Instance creation: ${messages[0]}`) + }) + } catch (error: any) { + console.error(`Caught widget error`, { error }) + handleFormError(error, setError) + } +} diff --git a/packages/pockethost.io/src/util/stores.ts b/packages/pockethost.io/src/util/stores.ts new file mode 100644 index 00000000..1963ceeb --- /dev/null +++ b/packages/pockethost.io/src/util/stores.ts @@ -0,0 +1,3 @@ +import { writable } from 'svelte/store' + +const instanceCreationWidgetName = writable(0) diff --git a/packages/pockethost.io/src/util/utilities.ts b/packages/pockethost.io/src/util/utilities.ts new file mode 100644 index 00000000..6e5ca4f3 --- /dev/null +++ b/packages/pockethost.io/src/util/utilities.ts @@ -0,0 +1,3 @@ +export const getRandomElementFromArray = (array: string[]) => { + return array[Math.floor(Math.random() * array.length)] +} diff --git a/packages/pockethost.io/static/global.css b/packages/pockethost.io/static/global.css index ffdb57f5..322fa29d 100644 --- a/packages/pockethost.io/static/global.css +++ b/packages/pockethost.io/static/global.css @@ -1,15 +1,41 @@ -body, p, -h1, h2, h3, h4, h5, h6 { - font-family: 'Inter', sans-serif; +body { } -h1, h2, h3, h4, h5, h6 { - font-weight: 700; +body, +p, +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'Inter', sans-serif; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 700; } .btn.btn-primary { - --bs-btn-border-radius: 50px; - --bs-btn-bg: #0072f5; - --bs-btn-padding-x: 24px; - --bs-btn-padding-y: 8px; -} \ No newline at end of file + --bs-btn-border-radius: 50px; + --bs-btn-bg: #965de9; + --bs-btn-padding-x: 24px; + --bs-btn-padding-y: 8px; +} + +.dropdown-menu { + --bs-dropdown-item-padding-y: 12px; +} + +.alert { + --bs-alert-border-radius: 18px; +} + +.form-control { + border-radius: 18px; +} diff --git a/packages/pockethost.io/static/images/logo-square.png b/packages/pockethost.io/static/images/logo-square.png new file mode 100644 index 0000000000000000000000000000000000000000..4d4d1954ca5adc0eeda681b60674e1790713fddf GIT binary patch literal 3044 zcmeAS@N?(olHy`uVBq!ia0y~yVEh8Y9Be?5)7S2I0jXK35uRzjz6@GGHU|S6qYwi# zki`gu42)6?tY9_+Ll~npoE^ug0ae2URAa~hBpF13IITUCfd#6*21tW|2M_~Ig3tk( z6{$J7i6!|(3IRp=3PyS+dgeemrUgteeT-igFeBJNIfna{xgd(Oz$3Dlfq`2Xgc%uT z&5>YWV9v`7i71Ki^|4CM&(%vz$xlkvtH><?DQB>$umUo3Q%e#RDspr3imfVamB1>j zfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0 zuU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3nOBlnp_^B%3^4>|j!SBBa#3bMNoIbY z0?6FNr2NtnTO}osMQ{LdXKF<z!lt}psJDO~)CbAv8|oS8W77uoEZlu4x+)S2aB3>a zOv7OpNCu(}92gKc+JIbO1&+i>m(1MMykek#>`V;|Y`|{AkU{tkNoNF3ok%iBI)NUu zaxO{*C7O`@{2V)MX~;$&T@{)EzQ71iNi0cpNi0dVGco|imVu?Np=F4HnU#^Hm9d$w zfw>jP8z5s))FPP!3KFY`pw#00oKjFk+L;>}0D-<Ch_unikVe;v&>5PSSz>1d)q*CB zt}7BDkIh14X>_grMVYC<<Op`FA=EZxF{pYQeNc)-N}-VC4;BTce>*N<u)y+?9oMz4 z-{lMp>}8%Vjv*Cu-d=aiJ>nqXdT{OnLk4GK9%o~Q&4qo}?kPPrSiD?|<^QFsSLfNm z`H_ai3yIC_AKva+pZ<+4?)bLd6I*xmZv~=1XXF?d{>)#%?9ecZ2RsZ8SiFBd)3)jQ z#<$7e*c$d<=->L|0;7V%C>|7HkZ?xs!1mAk=G~O8kiGrRP3)1~H4t)UZ(wj7#e*RX z8Vc`SH?C$`pM3lH8{R#4jz>XI%?kzrg;6{h!hnV~9K`4iQ{q!+t5(l7j7|Xwc)I$z JtaD0e0sz-LID!BG literal 0 HcmV?d00001 diff --git a/packages/pockethost.io/svelte.config.js b/packages/pockethost.io/svelte.config.js index bcc1a892..c3838a63 100644 --- a/packages/pockethost.io/svelte.config.js +++ b/packages/pockethost.io/svelte.config.js @@ -3,19 +3,19 @@ import preprocess from 'svelte-preprocess' /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://github.com/sveltejs/svelte-preprocess - // for more information about preprocessors - preprocess: preprocess(), + // Consult https://github.com/sveltejs/svelte-preprocess + // for more information about preprocessors + preprocess: preprocess(), - kit: { - adapter: adapter({ out: 'dist-server' }), - alias: { - $components: './src/components', - $util: './src/util', - $src: './src' - } - }, - target: '#svelte' + kit: { + adapter: adapter({ out: 'dist-server' }), + alias: { + $components: './src/components', + $util: './src/util', + $src: './src' + } + }, + target: '#svelte' } export default config diff --git a/packages/pockethost.io/tsconfig.json b/packages/pockethost.io/tsconfig.json index 3446fbd7..64a2aba8 100644 --- a/packages/pockethost.io/tsconfig.json +++ b/packages/pockethost.io/tsconfig.json @@ -1,23 +1,23 @@ { - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "baseUrl": "", - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "paths": { - "$util/*": ["src/util/*"], - "$components/*": ["src/components/*"], - "$src/*": ["src/*"] - } - } - // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "baseUrl": "", + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "paths": { + "$util/*": ["src/util/*"], + "$components/*": ["src/components/*"], + "$src/*": ["src/*"] + } + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in }