mirror of
https://github.com/pockethost/pockethost.git
synced 2026-03-18 06:08:52 +00:00
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 <zach@brewhouse.digital> Co-authored-by: Ben Allfree <ben@benallfree.com>
This commit is contained in:
committed by
GitHub
parent
bee037b494
commit
fa283d70cb
@@ -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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './assert'
|
||||
export * from './pocketbase'
|
||||
export * from './RealtimeSubscriptionManager'
|
||||
export * from './schema'
|
||||
|
||||
@@ -11,3 +11,6 @@ node_modules
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Source files
|
||||
/src/assets/_bootstrap.css
|
||||
@@ -1,19 +1,16 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"pluginSearchDirs": [
|
||||
".",
|
||||
"../.."
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"pluginSearchDirs": [".", "../.."],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="/global.css" />
|
||||
<link rel="stylesheet" href="/global.css" />
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css"
|
||||
/>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div>%sveltekit.body%</div>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div>%sveltekit.body%</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
21
packages/pockethost.io/src/components/AlertBar.svelte
Normal file
21
packages/pockethost.io/src/components/AlertBar.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
export let title: string = ''
|
||||
export let text: string = ''
|
||||
export let icon: string = ''
|
||||
</script>
|
||||
|
||||
<div class="alert alert-warning d-flex gap-3 align-items-center" role="alert">
|
||||
{#if icon}
|
||||
<i class={icon} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
{#if title}<p class="fw-bold mb-0">{title}</p>{/if}
|
||||
|
||||
{#if text}
|
||||
{text}
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { client } from '$src/pocketbase'
|
||||
import { redirect } from '$util/redirect'
|
||||
import { isLoggedIn } from '@pockethost/common/src/pocketbase'
|
||||
|
||||
if (browser && isLoggedIn()) {
|
||||
if (browser && client.isLoggedIn()) {
|
||||
redirect(`/dashboard`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { CaptionSize } from "./types"
|
||||
import { CaptionSize } from './types'
|
||||
|
||||
|
||||
export let size:CaptionSize=CaptionSize.Normal
|
||||
export let size: CaptionSize = CaptionSize.Normal
|
||||
</script>
|
||||
|
||||
<div class="caption {size}"><slot /></div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -12,19 +12,15 @@
|
||||
margin-bottom: 20px;
|
||||
&.small {
|
||||
font-size: 15px;
|
||||
|
||||
}
|
||||
&.normal {
|
||||
font-size: 25px;
|
||||
|
||||
}
|
||||
&.large {
|
||||
font-size: 45px;
|
||||
|
||||
}
|
||||
&.hero {
|
||||
font-size: 65px;
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style type="text/scss">
|
||||
<style lang="scss">
|
||||
.copy-container {
|
||||
position: relative;
|
||||
margin: 5px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<error><slot /></error>
|
||||
|
||||
<style type="text/scss">
|
||||
<style lang="scss">
|
||||
error {
|
||||
color: red;
|
||||
display: block;
|
||||
|
||||
@@ -1,49 +1,46 @@
|
||||
<script lang="ts">
|
||||
export let title: string;
|
||||
export let subtitle: string;
|
||||
export let icon: string;
|
||||
export let fullHeight: boolean;
|
||||
export let title: string = ''
|
||||
export let subtitle: string = ''
|
||||
export let icon: string = ''
|
||||
export let fullHeight: boolean = false
|
||||
</script>
|
||||
|
||||
|
||||
<div class="card {fullHeight && 'h-100'}">
|
||||
<div class="card-body">
|
||||
{#if icon}
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<div class="card-icon">
|
||||
<i class={icon}></i>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{#if icon}
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<div class="card-icon">
|
||||
<i class={icon} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if title}<h5 class="card-title {!subtitle && 'mb-0'}">{title}</h5>{/if}
|
||||
{#if subtitle}<h6 class="card-subtitle mb-0 text-muted">{subtitle}</h6>{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if title}<h5 class="card-title">{title}</h5>{/if}
|
||||
{#if subtitle}<h6 class="card-subtitle mb-2 text-muted">{subtitle}</h6>{/if}
|
||||
{/if}
|
||||
<div>
|
||||
{#if title}<h5 class="card-title {!subtitle && 'mb-0'}">{title}</h5>{/if}
|
||||
{#if subtitle}<h6 class="card-subtitle mb-0 text-muted">{subtitle}</h6>{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if title}<h5 class="card-title">{title}</h5>{/if}
|
||||
{#if subtitle}<h6 class="card-subtitle mb-2 text-muted">{subtitle}</h6>{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<div class="gap" />
|
||||
|
||||
<style lang="scss">
|
||||
.gap {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +1,59 @@
|
||||
<script lang="ts">
|
||||
import {fade} from 'svelte/transition';
|
||||
import {onMount} from "svelte";
|
||||
import { fade } from 'svelte/transition'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let isReady: boolean = false;
|
||||
let isReady: boolean = false
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
isReady = true;
|
||||
}, 3000);
|
||||
})
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
isReady = true
|
||||
}, 3000)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<div class="homepage-hero-animation">
|
||||
{#if !isReady}
|
||||
<div class="text-center">
|
||||
<p>Creating Your New Instance...</p>
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !isReady}
|
||||
<div class="text-center">
|
||||
<p>Creating Your New Instance...</p>
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isReady}
|
||||
<div in:fade="{{duration: 1000}}">
|
||||
<img src="/images/pocketbase-intro-screen.jpg" alt="Screenshot of the Pocketbase Intro UI" class="img-fluid" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if isReady}
|
||||
<div in:fade={{ duration: 1000 }}>
|
||||
<img
|
||||
src="/images/pocketbase-intro-screen.jpg"
|
||||
alt="Screenshot of the Pocketbase Intro UI"
|
||||
class="img-fluid"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.homepage-hero-animation {
|
||||
background-color: #fff;
|
||||
box-shadow: blue 0px 0px 0px 2px inset, rgb(255, 255, 255) 10px -10px 0px -3px,
|
||||
rgb(31, 193, 27) 10px -10px, rgb(255, 255, 255) 20px -20px 0px -3px,
|
||||
rgb(255, 217, 19) 20px -20px, rgb(255, 255, 255) 30px -30px 0px -3px,
|
||||
rgb(255, 156, 85) 30px -30px, rgb(255, 255, 255) 40px -40px 0px -3px,
|
||||
rgb(255, 85, 85) 40px -40px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 25px;
|
||||
padding: 30px;
|
||||
margin-right: 45px;
|
||||
margin-top: 25px;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.homepage-hero-animation {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 25px;
|
||||
padding: 30px;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import AlertBar from '$components/AlertBar.svelte'
|
||||
import { handleInstanceGeneratorWidget } from '$util/database'
|
||||
import { getRandomElementFromArray } from '$util/utilities'
|
||||
import { generateSlug } from 'random-word-slugs'
|
||||
|
||||
// Controls the spin animation of the instance regeneration button
|
||||
let rotationCounter: number = 0
|
||||
|
||||
let email: string = ''
|
||||
let password: string = ''
|
||||
let instanceName = generateSlug(2)
|
||||
let formError: string = ''
|
||||
|
||||
let isFormButtonDisabled: boolean = true
|
||||
$: isFormButtonDisabled = email.length === 0 || password.length === 0 || instanceName.length === 0
|
||||
|
||||
let isProcessing: boolean = false
|
||||
|
||||
// Fun quotes when waiting for the instance to load. This could take up to 10 seconds
|
||||
let processingQuotesArray = [
|
||||
'Did you know it takes fourteen sentient robots to create each instance on PocketHost?'
|
||||
]
|
||||
|
||||
let processingQuote = getRandomElementFromArray(processingQuotesArray)
|
||||
|
||||
const handleInstanceNameRegeneration = () => {
|
||||
rotationCounter = rotationCounter + 180
|
||||
instanceName = generateSlug(2)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
isFormButtonDisabled = true
|
||||
isProcessing = true
|
||||
|
||||
await handleInstanceGeneratorWidget(email, password, instanceName, (error) => {
|
||||
formError = error
|
||||
})
|
||||
|
||||
isFormButtonDisabled = false
|
||||
|
||||
isProcessing = false
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isProcessing}
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="mb-1">Creating Your New Instance...</h3>
|
||||
|
||||
<p class="small text-muted mb-0">{processingQuote}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<h3 class="mb-3">Create Your Instance Now</h3>
|
||||
|
||||
<form class="row align-items-center" on:submit={handleSubmit}>
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="form-floating mb-3 mb-lg-3">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
autocomplete="email"
|
||||
bind:value={email}
|
||||
required
|
||||
/>
|
||||
<label for="email">Email</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="form-floating mb-3 mb-lg-3">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
autocomplete="new-password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
<label for="password">Password</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="form-floating mb-3 mb-lg-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="instance"
|
||||
placeholder="Instance"
|
||||
bind:value={instanceName}
|
||||
required
|
||||
/>
|
||||
<label for="instance">Instance Name</label>
|
||||
|
||||
<button
|
||||
aria-label="Regenerate Instance Name"
|
||||
type="button"
|
||||
style="transform: rotate({rotationCounter}deg);"
|
||||
class="btn btn-light rounded-circle regenerate-instance-name-btn"
|
||||
on:click={handleInstanceNameRegeneration}
|
||||
>
|
||||
<i class="bi bi-arrow-repeat" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="mb-3 mb-lg-3 text-lg-start text-center">
|
||||
<button type="submit" class="btn btn-primary" disabled={isFormButtonDisabled}>
|
||||
Create <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if formError}
|
||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
form {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.row {
|
||||
--bs-gutter-x: 0.5rem;
|
||||
}
|
||||
|
||||
.btn.btn-primary {
|
||||
--bs-btn-padding-y: 12px;
|
||||
}
|
||||
|
||||
.regenerate-instance-name-btn {
|
||||
padding: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
z-index: 500;
|
||||
top: 10px;
|
||||
right: 7px;
|
||||
transition: all 200ms;
|
||||
}
|
||||
</style>
|
||||
42
packages/pockethost.io/src/components/MediaQuery.svelte
Normal file
42
packages/pockethost.io/src/components/MediaQuery.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
// Documentation Source
|
||||
// https://svelte.dev/repl/26eb44932920421da01e2e21539494cd?version=3.51.0
|
||||
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let query
|
||||
|
||||
let mql
|
||||
let mqlListener
|
||||
let wasMounted = false
|
||||
let matches = false
|
||||
|
||||
onMount(() => {
|
||||
wasMounted = true
|
||||
return () => {
|
||||
removeActiveListener()
|
||||
}
|
||||
})
|
||||
|
||||
$: {
|
||||
if (wasMounted) {
|
||||
removeActiveListener()
|
||||
addNewListener(query)
|
||||
}
|
||||
}
|
||||
|
||||
function addNewListener(query) {
|
||||
mql = window.matchMedia(query)
|
||||
mqlListener = (v) => (matches = v.matches)
|
||||
mql.addListener(mqlListener)
|
||||
matches = mql.matches
|
||||
}
|
||||
|
||||
function removeActiveListener() {
|
||||
if (mql && mqlListener) {
|
||||
mql.removeListener(mqlListener)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot {matches} />
|
||||
@@ -1,80 +1,153 @@
|
||||
<script lang="ts">
|
||||
import { client } from '$src/pocketbase'
|
||||
import { redirect } from '$util/redirect'
|
||||
import {
|
||||
Collapse,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
Icon,
|
||||
Nav,
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarToggler,
|
||||
NavItem,
|
||||
NavLink
|
||||
} from 'sveltestrap'
|
||||
import logo from '../assets/logo-square.png'
|
||||
import Github from './Icons/Github.svelte'
|
||||
import NavbarBrandImage from './NavbarBrandImage.svelte'
|
||||
import NavbarText from './NavbarText.svelte'
|
||||
import Title from './Title/Title.svelte'
|
||||
import { TitleSize } from './Title/types'
|
||||
import MediaQuery from '$components/MediaQuery.svelte'
|
||||
|
||||
let isOpen = false
|
||||
const { isLoggedIn, logOut } = client
|
||||
|
||||
function handleUpdate(event: CustomEvent<boolean>) {
|
||||
isOpen = event.detail.valueOf()
|
||||
}
|
||||
|
||||
const { isLoggedIn, logOut, user } = client
|
||||
|
||||
const handleLogout = () => {
|
||||
const handleLogout = (e: Event) => {
|
||||
e.preventDefault()
|
||||
logOut()
|
||||
redirect(`/`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Navbar color="light" light expand="md">
|
||||
<NavbarBrand href="/">
|
||||
<NavbarBrandImage {logo} />
|
||||
<Title size={TitleSize.Nav} first="pocket" second="host" third=".io" /></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>
|
||||
|
||||
@@ -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>
|
||||
@@ -1 +0,0 @@
|
||||
<div class="navbar-text"><slot /></div>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -9,7 +9,4 @@
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
main {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.hero {
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 65px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
170
packages/pockethost.io/src/util/database.ts
Normal file
170
packages/pockethost.io/src/util/database.ts
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
3
packages/pockethost.io/src/util/stores.ts
Normal file
3
packages/pockethost.io/src/util/stores.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
const instanceCreationWidgetName = writable(0)
|
||||
3
packages/pockethost.io/src/util/utilities.ts
Normal file
3
packages/pockethost.io/src/util/utilities.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const getRandomElementFromArray = (array: string[]) => {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
--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;
|
||||
}
|
||||
|
||||
BIN
packages/pockethost.io/static/images/logo-square.png
Normal file
BIN
packages/pockethost.io/static/images/logo-square.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user