enh: daisy dashboard

This commit is contained in:
Ben Allfree 2023-10-03 10:48:53 -07:00
parent ffe15b07a5
commit 8d296c356c
87 changed files with 1985 additions and 2124 deletions

View File

@ -14,4 +14,7 @@ yarn.lock
# Source files
/src/assets/_bootstrap.css
dist-server
dist-server
# Ignore the static files
static

View File

@ -6,7 +6,7 @@ Description about PocketHost goes here!
## Developing Locally
To run this project, navigate to the `/packages/pockethost.io` folder and run `vite dev`.
To run this project, navigate to the `/packages/dashboard` folder and run `yarn dev`.
It will start up the server here: [http://127.0.0.1:5173/](http://127.0.0.1:5173/) and now you're ready to code!

View File

@ -16,10 +16,12 @@
"@pockethost/common": "*",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.25.1",
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.16",
"boolean": "^3.2.0",
"d3-scale": "^4.0.2",
"d3-scale-chromatic": "^3.0.0",
"daisyui": "^3.8.1",
"date-fns": "^2.30.0",
"highlight.js": "^11.8.0",
"js-cookie": "^3.0.5",
@ -31,6 +33,9 @@
"svelte-check": "^3.5.2",
"svelte-highlight": "^7.3.0",
"svelte-preprocess": "^5.0.4",
"svelte-chartjs": "3.1.2",
"chart.js": "4.4.0",
"tailwindcss": "^3.3.3",
"url-pattern": "^1.0.3",
"vite": "^4.4.9"
},

View File

@ -0,0 +1,3 @@
module.exports = {
plugins: [require('tailwindcss'), require('autoprefixer')],
}

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -9,7 +9,7 @@ declare namespace App {
}
/**
* Taken from markdown plugin. For some reason importing here causes ts errors
* Taken from Markdown plugin. For some reason importing here causes ts errors
*/
interface Metadata<TAttributes extends {} = {}> {
attributes: TAttributes

View File

@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<title>PocketHost</title>
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link
rel="stylesheet"
@ -11,36 +12,15 @@
id="hljs-link"
/>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
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="stylesheet" href="/global.css" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css"
/>
<link href="/icons/fontawesome.min.css" rel="stylesheet">
<link href="/icons/all.min.css" rel="stylesheet">
<link href="/icons/brands.min.css" rel="stylesheet">
%sveltekit.head%
</head>
<body>
<div>%sveltekit.body%</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"
></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -9,27 +9,12 @@
const bodyId = `body${uid}`
</script>
<div class="accordion-item">
<h2 class="accordion-header" id={headerId}>
<button
class="accordion-button {show ? '' : 'collapsed'} text-bg-{header} "
type="button"
data-bs-toggle="collapse"
data-bs-target="#{bodyId}"
aria-expanded="true"
aria-controls={bodyId}
>
{title}
</button>
</h2>
<div
id={bodyId}
class="accordion-collapse collapse {show ? 'show' : ''} "
aria-labelledby={bodyId}
data-bs-parent="#accordionExample"
>
<div class="accordion-body">
<slot />
</div>
<div class="collapse collapse-arrow bg-base-200">
<input type="radio" name="my-accordion-1" checked={show} />
<div class="collapse-title text-xl font-medium">
{title}
</div>
<div class="collapse-content">
<slot />
</div>
</div>

View File

@ -26,12 +26,13 @@
.copy-container {
position: relative;
margin: 5px;
border: 1px solid gray;
border-radius: 16px;
overflow: hidden;
.copy-button {
position: absolute;
top: 2px;
right: 2px;
top: 8px;
right: 8px;
}
}
</style>

View File

@ -1,52 +0,0 @@
<script lang="ts">
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} />
</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}
<slot />
</div>
</div>
<style>
.card {
border: 0;
box-shadow: var(--soft-box-shadow);
border-radius: 18px;
}
.card-icon {
background-color: var(--bs-gray-200);
width: 35px;
height: 35px;
border-radius: 35px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -1,48 +0,0 @@
<script lang="ts">
import { faq } from '$src/docs'
// ts interface for the Question type
interface Question {
title: string
body: string
collapsed: boolean
}
interface rawQ {
title: string
body: string
}
// Array of Questions, if the boolean <collapsed> is set to false, the answer is displayed
const questions: Question[] = faq.outline.map((q: rawQ) => ({
...q,
collapsed: true,
}))
</script>
<div class="accordion w-100">
{#each questions as question}
<div class="accordion-item shadow">
<h5 class="accordion-header">
<button
class="accordion-button title {question.collapsed ? 'collapsed' : ''}"
type="button"
on:click={() => (question.collapsed = !question.collapsed)}
>
{question.title}
</button>
</h5>
<div class="accordion-collapse {question.collapsed ? 'collapse' : ''}">
<div class="accordion-body">
{@html question.body}
</div>
</div>
</div>
{/each}
</div>
<style>
.title {
font-size: 20px;
font-weight: bold;
}
</style>

View File

@ -1,41 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition'
import { onMount } from 'svelte'
import RetroBoxContainer from '$components/RetroBoxContainer.svelte'
let isReady: boolean = false
onMount(() => {
setTimeout(() => {
isReady = true
}, 3000)
})
</script>
<RetroBoxContainer minHeight={500} bgColor="#fff">
{#if !isReady}
<div class="hero-animation-content 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}
</RetroBoxContainer>
<style>
.hero-animation-content {
color: #222;
}
</style>

View File

@ -1,17 +0,0 @@
<script>
import { afterNavigate } from '$app/navigation'
import { browser } from '$app/environment'
// Tooltips must be manually initialized
// https://getbootstrap.com/docs/5.2/components/tooltips/#enable-tooltips
afterNavigate(() => {
if (browser) {
const tooltipTriggerList = document.querySelectorAll(
'[data-bs-toggle="tooltip"]',
)
const tooltipList = [...tooltipTriggerList].map(
(tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl),
)
}
})
</script>

View File

@ -1,164 +0,0 @@
<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: string = 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>

View File

@ -0,0 +1,17 @@
<script>
export let hideLogoText = false
export let logoWidth = 'w-24'
</script>
<div class="flex items-center justify-center gap-4">
<img
src="/images/pockethost-cloud-logo.jpg"
width="450"
height="450"
class="mix-blend-lighten {logoWidth}"
alt="PocketHost Logo"
/>
<h1 class="text-white font-bold text-2xl {hideLogoText && 'sr-only'}">
PocketHost
</h1>
</div>

View File

@ -0,0 +1,31 @@
<script>
import Logo from '$components/Logo.svelte'
</script>
<div class="drawer drawer-end">
<input id="mobile-nav-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<div class="flex items-center justify-between px-8 pt-1">
<a href="/dashboard" class="flex gap-2 items-center justify-center">
<Logo hideLogoText={true} logoWidth="w-16" />
</a>
<label for="mobile-nav-drawer" class="btn drawer-button">
<i class="fa-regular fa-bars text-2xl"></i>
</label>
</div>
</div>
<div class="drawer-side z-50">
<label
for="mobile-nav-drawer"
aria-label="close sidebar"
class="drawer-overlay"
></label>
<div class="bg-base-100 w-80 min-h-full">
<slot />
</div>
</div>
</div>

View File

@ -1,206 +1,107 @@
<script lang="ts">
import { page } from '$app/stores'
import Logo from '$components/Logo.svelte'
import MediaQuery from '$components/MediaQuery.svelte'
import ThemeToggle from '$components/ThemeToggle.svelte'
import { PUBLIC_POCKETHOST_VERSION } from '$src/env'
import { handleLogoutAndRedirect } from '$util/database'
import { isUserLoggedIn } from '$util/stores'
import AuthStateGuard from './helpers/AuthStateGuard.svelte'
import { getInstances } from '$util/getInstances'
import { globalInstancesStore } from '$util/stores'
import { values } from '@s-libs/micro-dash'
// This will query the database for all instances and then update the global state
getInstances()
const linkClasses =
'font-medium text-xl text-base-content btn btn-ghost capitalize justify-start'
const subLinkClasses =
'font-medium text-base-content btn btn-ghost btn-sm capitalize justify-start'
const addNewAppClasses =
'font-medium text-base-content btn btn-outline btn-primary btn-sm capitalize justify-start'
const handleClick = () => {
document.querySelector('.drawer-overlay')?.click()
}
</script>
<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>
<sup class="">{PUBLIC_POCKETHOST_VERSION}</sup>
</a>
<aside class="p-4 min-w-[250px] flex flex-col h-screen">
<MediaQuery query="(min-width: 1280px)" let:matches>
{#if matches}
<a href="/dashboard" class="flex gap-2 items-center justify-center">
<Logo hideLogoText={true} logoWidth="w-20" />
</a>
{/if}
</MediaQuery>
<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"
<div class="flex flex-col gap-2 mb-auto">
<a on:click={handleClick} href="/dashboard" class={linkClasses}
><i
class="fa-regular fa-table-columns {$page.url.pathname ===
'/dashboard' && 'text-primary'}"
></i> Dashboard</a
>
<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">
<AuthStateGuard>
{#if $isUserLoggedIn}
<li class="nav-item text-md-start text-center">
<a class="nav-link" href="/dashboard">Dashboard</a>
</li>
<div class="pl-8 flex flex-col gap-4 mb-4">
{#each values($globalInstancesStore) as app}
<a
href={`/app/instances/${app.id}`}
on:click={handleClick}
class={subLinkClasses}
>
<i
class="fa-regular fa-server {$page.url.pathname ===
`/app/instances/${app.id}` && 'text-primary'}"
></i>
{app.subdomain}
</a>
{/each}
<MediaQuery query="(min-width: 768px)" let:matches>
{#if matches}
<li class="nav-item dropdown">
<button
class="btn border-0 nav-link dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-label="Click to expand the Account Dropdown"
title="Account Dropdown"
aria-expanded="false"
>
Account
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button
class="dropdown-item"
type="button"
on:click={handleLogoutAndRedirect}>Logout</button
>
</li>
</ul>
</li>
{:else}
<li class="nav-item">
<a
class="nav-link text-md-start text-center"
href="/"
on:click={handleLogoutAndRedirect}>Logout</a
>
</li>
{/if}
</MediaQuery>
{/if}
{#if !$isUserLoggedIn}
<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}
</AuthStateGuard>
<li class="nav-item text-center">
<a
href="https://pockethost.io/docs/overview/faq"
class="nav-link btn btn-outline-dark rounded-1 d-inline-block px-3"
rel="noreferrer">FAQ</a
>
</li>
<li class="nav-item text-center">
<a
href="https://github.com/benallfree/pockethost/discussions"
class="nav-link btn btn-outline-dark rounded-1 d-inline-block px-3"
target="_blank"
rel="noreferrer">Support</a
>
</li>
<li class="nav-item text-center">
<a
href="https://pockethost.io/docs/"
class="nav-link btn btn-outline-dark rounded-1 d-inline-block px-3"
rel="noreferrer">Docs</a
>
</li>
<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"
title="Link to our Github Project"
rel="noopener"
>
<i class="bi bi-github" /><span class="nav-github-link">Github</span
>
</a>
</li>
<li class="nav-item text-center">
<ThemeToggle navLink={true} />
</li>
</ul>
<a href="/app/new" on:click={handleClick} class={addNewAppClasses}>
<i
class="fa-regular fa-plus {$page.url.pathname === `/app/new` &&
'text-primary'}"
></i> Create A New App
</a>
</div>
</nav>
</header>
<style lang="scss">
header {
background-color: var(--bs-body-bg);
padding: 12px 24px;
border-bottom: 1px solid var(--bs-gray-300);
}
<a
href="https://github.com/benallfree/pockethost/discussions"
class={linkClasses}
target="_blank"
rel="noreferrer"
><i class="fa-regular fa-comment-code"></i> Discussion
<i
class="fa-regular fa-arrow-up-right-from-square ml-auto opacity-50 text-sm"
></i></a
>
.logo {
img {
max-width: 50px;
margin-right: 16px;
}
<a
href="https://pockethost.io/docs"
class={linkClasses}
target="_blank"
rel="noreferrer"
>
<i class="fa-regular fa-webhook"></i> Docs
<i
class="fa-regular fa-arrow-up-right-from-square ml-auto opacity-50 text-sm"
></i></a
>
h1 {
font-size: 36px;
font-weight: 300;
margin: 0;
color: var(--bs-body-color);
<a
href="https://github.com/pockethost/pockethost"
class={linkClasses}
target="_blank"
rel="noreferrer"
>
<i class="fa-brands fa-github"></i> GitHub
<i
class="fa-regular fa-arrow-up-right-from-square ml-auto opacity-50 text-sm"
></i></a
>
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;
}
}
<button type="button" class={linkClasses} on:click={handleLogoutAndRedirect}
><i class="fa-regular fa-arrow-up-left-from-circle"></i> Logout</button
>
</div>
sup {
margin-left: 4px;
font-size: 12px;
font-weight: 700;
color: var(--bs-gray-600);
}
}
.mobile-nav-button {
font-size: 20px;
}
.nav-item {
margin: 8px 0;
}
.nav-link {
font-weight: 500;
margin: 0 5px;
}
.nav-github-link {
display: inline-block;
margin-left: 4px;
}
@media screen and (min-width: 768px) {
.nav-github-link {
display: none;
}
.nav-item {
margin: 0;
}
}
</style>
<ThemeToggle />
</aside>

View File

@ -1,40 +0,0 @@
<script lang="ts">
export let minHeight: number = 0
export let bgColor: string = 'var(--bs-body-bg)'
// Construct the CSS styles since Svelte doesn't support CSS injection yet
let cssStyles = `min-height: ${minHeight}px; background-color: ${bgColor};`
</script>
<div class="homepage-hero-animation" style={cssStyles}>
<slot />
</div>
<style>
.homepage-hero-animation {
box-shadow:
var(--bs-primary) 0 0 0 3px inset,
var(--bs-body-bg) 10px -10px 0px -3px,
var(--bs-success) 10px -10px,
var(--bs-body-bg) 20px -20px 0px -3px,
var(--bs-warning) 20px -20px,
var(--bs-body-bg) 30px -30px 0px -3px,
var(--bs-orange) 30px -30px,
var(--bs-body-bg) 40px -40px 0px -3px,
var(--bs-danger) 40px -40px;
border: 0;
border-radius: 25px;
padding: 30px;
margin-right: 45px;
margin-top: 25px;
display: flex;
align-items: center;
justify-content: center;
}
@media screen and (min-width: 768px) {
.homepage-hero-animation {
margin: 0;
}
}
</style>

View File

@ -1,48 +1,45 @@
<script lang="ts">
import { browser } from '$app/environment'
import { onMount } from 'svelte'
import {
THEME_ICONS,
ThemeNames,
currentIcon,
getCurrentTheme,
setCurrentTheme,
} from './helpers/theme'
import { ThemeNames, getCurrentTheme, setCurrentTheme } from './helpers/theme'
// This can change the CSS a bit depending on where the theme toggle is rendered
export let navLink: boolean = false
// Set the default icon to be light mode
let iconClass: string = browser ? currentIcon() : ''
// This will keep track of the toggle's state
let isChecked = true
// Wait for the DOM to be available
onMount(() => {
// Check what theme cookie is set
const currentTheme = getCurrentTheme()
// Set the toggle's state
isChecked = currentTheme === ThemeNames.Dark
// Update the site's theme to match what the cookie has
updateTheme(getCurrentTheme())
})
// Alternate the theme values on toggle click
const handleClick = () => {
const newTheme =
getCurrentTheme() === ThemeNames.Light
? ThemeNames.Dark
: ThemeNames.Light
const handleChange = (e: Event) => {
const target = e.target as HTMLInputElement
const isChecked = target.checked
const newTheme = isChecked ? ThemeNames.Dark : ThemeNames.Light
updateTheme(newTheme)
}
const updateTheme = (themeName: ThemeNames) => {
// Update the icon class name to toggle between light and dark mode
iconClass = THEME_ICONS[themeName]
setCurrentTheme(themeName)
}
</script>
<button
type="button"
class="{navLink && 'nav-link'} btn border-0 d-inline-block"
aria-label="Toggle the site theme"
title="Toggle the site theme"
on:click={handleClick}
>
<i class={iconClass} />
</button>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Dark Mode</span>
<input
type="checkbox"
class="toggle"
bind:checked={isChecked}
on:change={handleChange}
/>
</label>
</div>

View File

@ -0,0 +1,15 @@
<script lang="ts">
export let block = true
export let height: string = 'h-full'
export let marginBottom: string = ''
</script>
<!-- Setting the `container-type` allows us to use Container Queries -->
<div
class="card card-body bg-base-200 {block
? 'block'
: ''} {height} {marginBottom}"
style="container-type: inline-size"
>
<slot />
</div>

View File

@ -0,0 +1,21 @@
<script lang="ts">
export let documentation: string = ''
</script>
{#if documentation}
<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
<h3 class="text-xl font-bold"><slot /></h3>
<a href={documentation} class="btn btn-sm btn-outline btn-primary"
>Full documentation <i
class="fa-regular fa-arrow-up-right-from-square opacity-50 text-sm"
></i></a
>
</div>
{/if}
{#if !documentation}
<h3 class="text-xl font-bold mb-4">
<slot />
</h3>
{/if}

View File

@ -1,8 +1,5 @@
<script lang="ts">
import { isAuthStateInitialized } from '$util/stores'
import { onMount } from 'svelte'
onMount(() => {})
</script>
{#if $isAuthStateInitialized}

View File

@ -1,24 +0,0 @@
<script>
</script>
<svelte:head>
<script>
{
const THEME_ATTRIBUTE = 'data-bs-theme'
const currentTheme =
document.cookie
.split('; ')
.find((row) => row.startsWith('theme='))
?.split('=')?.[1] || 'light'
document
.querySelector('html')
?.setAttribute(THEME_ATTRIBUTE, currentTheme)
const theme = document.querySelector('#hljs-link')
if (currentTheme === 'light') {
theme.href =
'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css'
}
}
</script>
</svelte:head>

View File

@ -6,7 +6,7 @@
<button
type="button"
class="btn btn-{style}"
class="btn btn-{style} btn-sm"
{disabled}
style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
on:click={click}><slot /></button

View File

@ -14,13 +14,9 @@ export const HLJS_THEMES = {
'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-dark.min.css',
}
export const ALLOWED_THEMES: ThemeNames[] = [ThemeNames.Light, ThemeNames.Dark]
export const DEFAULT_THEME: ThemeNames = ThemeNames.Light
export const DEFAULT_THEME: ThemeNames = ThemeNames.Dark
export const STORAGE_NAME: string = 'theme'
export const THEME_ATTRIBUTE: string = 'data-bs-theme'
export const THEME_ICONS: { [_ in ThemeNames]: string } = {
[ThemeNames.Light]: 'bi bi-moon-stars',
[ThemeNames.Dark]: 'bi bi-brightness-high',
}
export const THEME_ATTRIBUTE: string = 'data-theme'
export const html = () => {
const htmlElement = document.querySelector('html')
@ -35,16 +31,12 @@ export const getCurrentTheme = () => {
return currentTheme
}
export const currentIcon = () => {
return THEME_ICONS[getCurrentTheme()]
}
export const setCurrentTheme = (themeName: ThemeNames) => {
html().setAttribute(THEME_ATTRIBUTE, themeName)
const theme = document.querySelector<HTMLLinkElement>('#hljs-link')
if (theme) {
theme.href = HLJS_THEMES[themeName]
}
console.log(theme, themeName)
Cookies.set(STORAGE_NAME, themeName)
}

View File

@ -0,0 +1,34 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import NewInstanceProcessingBlock from '$components/login-register/NewInstanceProcessingBlock.svelte'
import RegisterForm from '$components/login-register/RegisterForm.svelte'
import LoginForm from '$components/login-register/LoginForm.svelte'
// Create a toggle to hold the Sign-Up view or the Register view
let isSignUpView: boolean = true
// Disable the form button while the instance is being created
let isProcessing: boolean = false
</script>
<div class="card w-96 bg-zinc-900 mx-auto shadow-xl overflow-hidden">
{#if isSignUpView}
<div in:slide={{ delay: 400 }} out:slide>
{#if isProcessing}
<div in:slide={{ delay: 400 }} out:slide>
<NewInstanceProcessingBlock />
</div>
{:else}
<div in:slide={{ delay: 400 }} out:slide>
<RegisterForm bind:isProcessing bind:isSignUpView />
</div>
{/if}
</div>
{/if}
{#if !isSignUpView}
<div in:slide={{ delay: 400 }} out:slide>
<LoginForm bind:isProcessing bind:isSignUpView />
</div>
{/if}
</div>

View File

@ -0,0 +1,101 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import { handleLogin } from '$util/database'
import { boolean } from 'boolean'
export let isSignUpView: boolean = true
// Set up the variables to hold the form information
let email: string = ''
let password: string = ''
let formError: string = ''
// Disable the form button until all fields are filled out
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled = email.length === 0 || password.length === 0
let isButtonLoading: boolean = false
// Toggle between registration and login forms
const handleLoginClick = () => {
isSignUpView = !isSignUpView
}
// Handle the form submission
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault()
isFormButtonDisabled = true
isButtonLoading = true
await handleLogin(email, password, (error) => {
formError = error
})
isFormButtonDisabled = false
isButtonLoading = false
}
</script>
<form class="card-body" on:submit={handleSubmit}>
<h2 class="font-bold text-white mb-3 text-center text-2xl">Log In</h2>
<div class="mb-3">
<label class="label" for="id">
<span class="label-text">Email</span>
</label>
<input
type="email"
class="input input-bordered w-full"
id="email"
placeholder="name@example.com"
autocomplete="email"
bind:value={email}
required
/>
</div>
<div class="mb-3">
<label class="label" for="password">
<span class="label-text">Password</span>
</label>
<input
type="password"
class="input input-bordered w-full"
id="password"
placeholder="Password"
autocomplete="new-password"
bind:value={password}
required
/>
</div>
{#if formError}
<div transition:slide class="alert alert-error mb-5">
<i class="fa-solid fa-circle-exclamation"></i>
<span>{formError}</span>
</div>
{/if}
<div class="card-actions justify-end">
<button
type="submit"
class="btn btn-primary"
disabled={isFormButtonDisabled}
>
{#if isButtonLoading}
<span class="loading loading-spinner"></span>
{:else}
Log In <i class="fa-solid fa-arrow-right"></i>
{/if}
</button>
</div>
</form>
<div class="p-4 bg-zinc-800 text-center">
Need to Register? <button
type="button"
class="link font-bold"
on:click={handleLoginClick}>Create A New Account</button
>
</div>

View File

@ -0,0 +1,24 @@
<script lang="ts">
import { getRandomElementFromArray } from '$util/utilities'
// 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)
</script>
<div class="card-body">
<div class="text-center">
<span class="loading loading-spinner loading-lg mb-4"></span>
<div>
<h2 class="mb-12 font-bold text-white text-2xl">
Creating Your New Instance...
</h2>
<p class="italic">{processingQuote}</p>
</div>
</div>
</div>

View File

@ -0,0 +1,141 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import { handleInstanceGeneratorWidget } from '$util/database'
import { generateSlug } from 'random-word-slugs'
export let isProcessing: boolean = false
export let isSignUpView: boolean = false
// Controls the spin animation of the instance regeneration button
let rotationCounter: number = 0
// Set up the variables to hold the form information
let email: string = ''
let password: string = ''
let instanceName: string = generateSlug(2)
let formError: string = ''
// Disable the form button until all fields are filled out
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled =
email.length === 0 || password.length === 0 || instanceName.length === 0
// Generate a unique name for the PocketHost instance
const handleInstanceNameRegeneration = () => {
rotationCounter = rotationCounter + 180
instanceName = generateSlug(2)
}
// Toggle between registration and login forms
const handleLoginClick = () => {
isSignUpView = !isSignUpView
}
// Handle the form submission
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault()
isFormButtonDisabled = true
isProcessing = true
/*await handleInstanceGeneratorWidget(
email,
password,
instanceName,
(error) => {
formError = error
},
)*/
setTimeout(() => {
isFormButtonDisabled = false
isProcessing = false
}, 5000)
}
</script>
<form class="card-body" on:submit={handleSubmit}>
<h2 class="font-bold text-white mb-3 text-center text-2xl">
Register and Create Your <br />First Instance
</h2>
<div class="mb-3">
<label class="label" for="id">
<span class="label-text">Email</span>
</label>
<input
type="email"
class="input input-bordered w-full"
id="email"
placeholder="name@example.com"
autocomplete="email"
bind:value={email}
required
/>
</div>
<div class="mb-3">
<label class="label" for="password">
<span class="label-text">Password</span>
</label>
<input
type="password"
class="input input-bordered w-full"
id="password"
placeholder="Password"
autocomplete="new-password"
bind:value={password}
required
/>
</div>
<div class="mb-12">
<label class="label" for="instance">
<span class="label-text">Instance Name</span>
</label>
<div class="input-group">
<input
type="text"
placeholder="instance-name"
bind:value={instanceName}
id="instance"
class="input input-bordered w-full"
/>
<button
type="button"
class="btn btn-square"
on:click={handleInstanceNameRegeneration}
>
<i class="fa-solid fa-rotate"></i>
</button>
</div>
</div>
{#if formError}
<div transition:slide class="alert alert-error mb-5">
<i class="fa-solid fa-circle-exclamation"></i>
<span>{formError}</span>
</div>
{/if}
<div class="card-actions justify-end">
<button
type="submit"
class="btn btn-primary"
disabled={isFormButtonDisabled}
>
Create <i class="fa-solid fa-arrow-right"></i>
</button>
</div>
</form>
<div class="p-4 bg-zinc-800 text-center">
Already have an account? <button
type="button"
class="link font-bold"
on:click={handleLoginClick}>Login</button
>
</div>

View File

@ -1,28 +1,48 @@
<script>
import InitializeTooltips from '$components/InitializeTooltips.svelte'
import Navbar from '$components/Navbar.svelte'
import VerifyAccountBar from '$components/VerifyAccountBar.svelte'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import Meta from '$components/helpers/Meta.svelte'
import Protect from '$components/helpers/Protect.svelte'
import ThemeDetector from '$components/helpers/ThemeDetector.svelte'
import MediaQuery from '$components/MediaQuery.svelte'
import MobileNavDrawer from '$components/MobileNavDrawer.svelte'
import '../app.css'
import { isUserLoggedIn } from '$util/stores'
import Logo from '$components/Logo.svelte'
</script>
<Meta />
<Protect />
<ThemeDetector />
<Navbar />
{#if $isUserLoggedIn}
<AuthStateGuard>
<VerifyAccountBar />
</AuthStateGuard>
<AuthStateGuard>
<VerifyAccountBar />
</AuthStateGuard>
<div class="layout xl:flex">
<MediaQuery query="(min-width: 1280px)" let:matches>
{#if matches}
<Navbar />
{:else}
<MobileNavDrawer>
<Navbar />
</MobileNavDrawer>
{/if}
</MediaQuery>
<main data-sveltekit-prefetch>
<slot />
</main>
<div class="lg:p-4 lg:pt-0 xl:pt-4 min-h-screen grow">
<div
class="bg-base-300 border-base-300 border-[16px] xl:h-[calc(100vh-32px)] lg:p-4 rounded-2xl xl:overflow-hidden xl:overflow-y-auto"
>
<slot />
</div>
</div>
</div>
{/if}
<InitializeTooltips />
<style lang="scss">
</style>
{#if !$isUserLoggedIn}
<div>
<slot />
</div>
{/if}

View File

@ -1,9 +1,7 @@
<script lang="ts">
import FeatureCard from '$components/FeatureCard.svelte'
import Logo from '$components/Logo.svelte'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import HomepageHeroAnimation from '$components/HomepageHeroAnimation.svelte'
import InstanceGeneratorWidget from '$components/InstanceGeneratorWidget.svelte'
import { PUBLIC_APP_DOMAIN } from '$src/env'
import InstanceGeneratorWidget from '$components/login-register/InstanceGeneratorWidget.svelte'
import { isUserLoggedIn } from '$util/stores'
</script>
@ -11,210 +9,22 @@
<title>Home - PocketHost</title>
</svelte:head>
<div class="container">
<div class="row align-items-center justify-content-between hero">
<div class="col-lg-6 mb-5 mb-lg-0">
<h2>Deploy <span>PocketBase</span> in 30 seconds</h2>
<div class="min-h-screen flex items-center justify-center">
<div>
<AuthStateGuard>
<Logo />
<p class="mb-5">
Spend less time on configuring your backend, and more time building new
features for your web app.
</p>
{#if $isUserLoggedIn}
<div class="">
<a href="/dashboard" class="btn btn-primary"
>Go to Your Dashboard <i class="bi bi-arrow-right-short" /></a
>
</div>
{/if}
<AuthStateGuard>
{#if $isUserLoggedIn}
<div>
<a href="/dashboard" class="btn btn-primary"
>Go to Your Dashboard <i class="bi bi-arrow-right-short" /></a
>
</div>
{/if}
{#if !$isUserLoggedIn}
<InstanceGeneratorWidget />
{/if}
</AuthStateGuard>
</div>
<div class="col-lg-5 d-none d-sm-block">
<HomepageHeroAnimation />
</div>
{#if !$isUserLoggedIn}
<InstanceGeneratorWidget />
{/if}
</AuthStateGuard>
</div>
</div>
<div class="section features">
<div class="container">
<h2 class="mb-5">Features</h2>
<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>
<ul>
<li>Sign up</li>
<li>Pick a unique project name</li>
<li>Connect with our JS client</li>
</ul>
</FeatureCard>
</div>
<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>
</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>
</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>yoursubdomain@{PUBLIC_APP_DOMAIN}</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>
</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>
</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>
</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>
</FeatureCard>
</div>
<div class="col-12 col-md-6 col-lg-6 mb-4">
<FeatureCard
title="Coming Soon"
icon="bi bi-card-checklist"
fullHeight={true}
>
<ul>
<li>JS/TS cloud functions</li>
<li>Deploy to Fly.io</li>
<li>Litestream</li>
</ul>
</FeatureCard>
</div>
</div>
</div>
</div>
<style>
.hero {
padding: 50px 0;
}
.hero h2 {
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%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.features {
background-image: var(--gradient-white-lime);
}
.section {
padding: 120px 0;
}
.section h2 {
font-size: 56px;
}
@media screen and (min-width: 768px) {
.hero {
padding: 100px 0;
}
.hero h2 {
font-size: 65px;
}
}
</style>

View File

@ -1,51 +1,23 @@
<script lang="ts">
import { page } from '$app/stores'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
import { client } from '$src/pocketbase'
import {
assertExists,
createCleanupManager,
logger,
} from '@pockethost/common'
import { onDestroy, onMount } from 'svelte'
import { getSingleInstance } from '$util/getInstances'
import { createCleanupManager } from '@pockethost/common'
import { onDestroy } from 'svelte'
import { instance } from './store'
const { instanceId } = $page.params
const cm = createCleanupManager()
onMount(async () => {
const { dbg, error } = logger().create(`layout.svelte`)
const { watchInstanceById } = client()
watchInstanceById(instanceId, (r) => {
dbg(`Handling instance update`, r)
const { action, record } = r
assertExists(record, `Expected instance here`)
instance.set(record)
})
.then(cm.add)
.catch(error)
})
// Run anytime the page params changes
$: {
getSingleInstance($page.params.instanceId)
}
onDestroy(() => cm.shutdown())
</script>
<AuthStateGuard>
<div class="container">
{#if $instance}
<h2>
{$instance.subdomain}
<a
href="{PUBLIC_APP_PROTOCOL}://{$instance.subdomain}.{PUBLIC_APP_DOMAIN}/_"
target="_blank"
rel="noreferrer"><i class="bi bi-box-arrow-up-right" /></a
>
</h2>
<slot />
<div class="text-center py-5">
<a href="/dashboard" class="btn btn-light"
><i class="bi bi-arrow-left-short" /> Back to Dashboard</a
>
</div>
{/if}
</div>
{#if $instance}
<slot />
{/if}
</AuthStateGuard>

View File

@ -1,35 +1,87 @@
<script lang="ts">
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
import { assertExists } from '@pockethost/common'
import Code from './Code.svelte'
import Danger from './Danger/Danger.svelte'
import UsageChart from './UsageChart.svelte'
import Ftp from './Ftpx.svelte'
import Logging from './Logging.svelte'
import Overview from './Overview.svelte'
import Secrets from './Secrets/Secrets.svelte'
import { instance } from './store'
import { PUBLIC_APP_DOMAIN } from '$src/env'
import DangerZoneTitle from './Danger/DangerZoneTitle.svelte'
import RenameInstance from './Danger/RenameInstance.svelte'
import Maintenance from './Danger/Maintenance.svelte'
import VersionChange from './Danger/VersionChange.svelte'
import { slide } from 'svelte/transition'
$: ({ status, version, secondsThisMonth } = $instance)
assertExists($instance, `Expected instance here`)
const { subdomain, maintenance } = $instance
const url = `${PUBLIC_APP_PROTOCOL}://${subdomain}.${PUBLIC_APP_DOMAIN}`
const code = `const url = '${url}'\nconst client = new PocketBase(url)`
const { subdomain } = $instance
</script>
<svelte:head>
<title>{subdomain} details - PocketHost</title>
</svelte:head>
{#if $instance}
{#if $instance.maintenance}
<div class="text-warning">
This instance is in Maintenance Mode and will not respond to requests.
<div class="flex md:flex-row flex-col items-center justify-between mb-6 gap-4">
<div>
<h2
class="text-4xl md:text-left text-center text-base-content font-bold capitalize mb-3 break-words"
>
{$instance.subdomain}
</h2>
<div class="flex flex-wrap md:justify-start justify-center gap-2">
<div class="badge badge-accent badge-outline">
Status: &nbsp;<span class="capitalize">{status}</span>
</div>
<div class="badge badge-accent badge-outline">
Usage: {Math.ceil(secondsThisMonth / 60)} mins
</div>
<div class="badge badge-accent badge-outline">Version: {version}</div>
</div>
{/if}
<div class="accordion" id="accordionExample">
<Overview />
<Ftp />
<Code />
<Secrets />
<Logging />
<Danger />
</div>
<a
href="https://{$instance.subdomain}.{PUBLIC_APP_DOMAIN}/_"
rel="noreferrer"
target="_blank"
class="btn btn-primary"
>
<img src="/images/pocketbase-logo.svg" alt="PocketBase Logo" class="w-6" />
Admin
</a>
</div>
{#if $instance.maintenance}
<div transition:slide class="alert alert-warning mb-6">
<i class="fa-regular fa-triangle-person-digging"></i>
<span
>This instance is in Maintenance Mode and will not respond to requests</span
>
</div>
{/if}
<div class="grid lg:grid-cols-2 grid-cols-1 gap-4 mb-4">
<UsageChart />
<Code />
</div>
<div class="grid lg:grid-cols-3 grid-cols-1 gap-4 mb-16">
<Ftp />
<Logging />
<Secrets />
</div>
<DangerZoneTitle />
<div class="grid lg:grid-cols-3 gap-4 mb-4">
<RenameInstance />
<Maintenance />
<VersionChange />
</div>

View File

@ -1,17 +1,56 @@
<script lang="ts">
import AccordionItem from '$components/AccordionItem.svelte'
import CodeSample from '$components/CodeSample.svelte'
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
import { instance } from './store'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
let code = ''
let installSnippet = `npm i pocketbase`
let connectionSnippet = ''
$: {
const url = `${PUBLIC_APP_PROTOCOL}://${$instance.subdomain}.${PUBLIC_APP_DOMAIN}`
code = `const url = '${url}'\nconst client = new PocketBase(url)`
connectionSnippet = `import PocketBase from 'pocketbase';\n\nconst url = '${url}'\nconst client = new PocketBase(url)`
}
let firstQuerySnippet = `const records = await client.collection('posts').getFullList({
sort: '-created',
});`
</script>
<AccordionItem title="Code Samples">
JavaScript:
<CodeSample {code} />
</AccordionItem>
<Card>
<CardHeader>Getting Started</CardHeader>
<div class="mb-4">
<p>Installing PocketBase</p>
<CodeSample code={installSnippet} />
</div>
<div class="mb-4">
<p>Connecting to Your Instance</p>
<CodeSample code={connectionSnippet} />
</div>
<div class="mb-4">
<p>Making Your First Query</p>
<CodeSample code={firstQuerySnippet} />
</div>
<p>Additional Resources:</p>
<ul class="list-disc pl-4">
<li>
<a
href="https://pocketbase.io/docs/api-records/"
target="_blank"
class="link">PocketBase Web APIs</a
>
</li>
<li>
<a
href="https://www.npmjs.com/package/pocketbase"
target="_blank"
class="link">PocketBase NPM Package</a
>
</li>
</ul>
</Card>

View File

@ -1,12 +0,0 @@
<script lang="ts">
import AccordionItem from '../../../../../components/AccordionItem.svelte'
import Version from '../Version.svelte'
import Maintenance from './Maintenance.svelte'
import Rename from './Rename.svelte'
</script>
<AccordionItem title="Danger Zone" header="danger">
<Rename />
<Maintenance />
<Version />
</AccordionItem>

View File

@ -0,0 +1,9 @@
<div class="block py-8 mb-4">
<div class="flex items-center justify-center gap-4 w-full">
<i class="fa-solid fa-siren-on text-red-600"></i>
<h2 class="text-4xl font-bold text-red-600">Danger Zone</h2>
<i class="fa-solid fa-siren-on text-red-600"></i>
</div>
</div>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import MiniToggle from '$components/MiniToggle.svelte'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { client } from '$src/pocketbase'
import { instance } from '../store'
@ -7,19 +8,36 @@
$: ({ id, maintenance } = $instance)
const onMaintenance = (maintenance: boolean) =>
setInstanceMaintenance({ instanceId: id, maintenance }).then(() => 'saved')
const handleChange = (e: Event) => {
const target = e.target as HTMLInputElement
const isChecked = target.checked
// Update the database with the new value
setInstanceMaintenance({ instanceId: id, maintenance: isChecked }).then(
() => 'saved',
)
}
</script>
<div>
<h3>Maintenance Mode</h3>
<p class="text-danger">
Your PocketHost instance will not be accessible while in maintenance mode.
Use this when you are upgrading, downgrading, or backing up your data. See <a
href="https://pockethost.io/docs/usage/maintenance">Maintenance Mode</a
> for more information.
</p>
<MiniToggle value={maintenance} save={onMaintenance}
>Maintenance Mode</MiniToggle
<Card>
<CardHeader
documentation="https://pockethost.gitbook.io/manual/daily-usage/maintenance"
>
</div>
Maintenance Mode
</CardHeader>
<p class="mb-8">
Your PocketHost instance will not be accessible while in maintenance mode.
Use this when you are upgrading, downgrading, or backing up your data.
</p>
<label class="label cursor-pointer justify-center gap-4">
<span class="label-text">Maintenance Mode</span>
<input
type="checkbox"
class="toggle toggle-warning"
checked={!!maintenance}
on:change={handleChange}
/>
</label>
</Card>

View File

@ -1,24 +0,0 @@
<script lang="ts">
import { client } from '$src/pocketbase'
import MiniEdit from '../../../../../components/MiniEdit.svelte'
import { instance } from '../store'
const { renameInstance, setInstanceMaintenance } = client()
$: ({ subdomain, id, maintenance } = $instance)
const onRename = (subdomain: string) =>
renameInstance({ instanceId: id, subdomain }).then(() => 'saved')
</script>
<div>
<h3>Rename Instance</h3>
<p class="text-danger">
Warning - renaming your instance will cause it to become inaccessible by the
old instance name. You also may not be able to change it back if someone
else choose it. See <a
href="https://pockethost.io/docs/usage/rename-instance">renaming</a
> for more information.
</p>
<MiniEdit value={subdomain} save={onRename} />
</div>

View File

@ -0,0 +1,77 @@
<script lang="ts">
import { client } from '$src/pocketbase'
import { instance } from '../store'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
const { renameInstance } = client()
$: ({ subdomain, id } = $instance)
// Create a copy of the subdomain
let formSubdomain = subdomain
$: {
formSubdomain = subdomain
}
// Controls the disabled state of the button
let isButtonDisabled = false
// TODO: What are the limits for this?
const onRename = (e: Event) => {
e.preventDefault()
// Disable the button to prevent double submissions
isButtonDisabled = true
// TODO: Set up error handling for when the name is wrong
// TODO: Do validations like trim and removing numbers
renameInstance({ instanceId: id, subdomain: formSubdomain }).then(
() => 'saved',
)
// Set the button back to normal
isButtonDisabled = false
}
</script>
<Card>
<CardHeader
documentation="https://pockethost.gitbook.io/manual/daily-usage/rename-instance"
>
Rename Instance
</CardHeader>
<p class="mb-8">
Renaming your instance will cause it to become <strong class="text-error"
>inaccessible</strong
> by the old instance name. You also may not be able to change it back if someone
else choose it.
</p>
<form
class="flex rename-instance-form-container-query gap-4"
on:submit={onRename}
>
<input
type="text"
bind:value={formSubdomain}
class="input input-bordered w-full"
/>
<button type="submit" class="btn btn-error" disabled={isButtonDisabled}
>Rename Instance</button
>
</form>
</Card>
<style>
.rename-instance-form-container-query {
flex-direction: column;
}
@container (min-width: 400px) {
.rename-instance-form-container-query {
flex-direction: row;
}
}
</style>

View File

@ -0,0 +1,78 @@
<script lang="ts">
import { client } from '$src/pocketbase'
import MiniEdit from '$components/MiniEdit.svelte'
import { instance } from '../store'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
$: ({ id, maintenance } = $instance)
let version = $instance.version
// Controls the disabled state of the button
let isButtonDisabled = false
// Update the version number
const handleSave = async (e: Event) => {
e.preventDefault()
// Disable the button to prevent double submissions
isButtonDisabled = true
// Save to the database
client()
.saveVersion({ instanceId: id, version: version })
.then(() => {
return 'saved'
})
// Set the button back to normal
isButtonDisabled = false
}
</script>
<Card>
<CardHeader
documentation="https://pockethost.gitbook.io/manual/daily-usage/upgrading"
>
Version Change
</CardHeader>
<p class="mb-8">
Changing your version can only be done when the instance is in maintenance
mode. We recommend you <strong>do a full backup</strong> before making a
change. The version number uses the semver syntax and any
<a
href="https://www.npmjs.com/package/pocketbase?activeTab=versions"
class="link">supported PocketBase version</a
> should work.
</p>
<form
class="flex change-version-form-container-query gap-4"
on:submit={handleSave}
>
<input
type="text"
bind:value={version}
class="input input-bordered w-full"
/>
<button
type="submit"
class="btn btn-error"
disabled={!maintenance || isButtonDisabled}>Change Version</button
>
</form>
</Card>
<style>
.change-version-form-container-query {
flex-direction: column;
}
@container (min-width: 400px) {
.change-version-form-container-query {
flex-direction: row;
}
}
</style>

View File

@ -1,14 +1,15 @@
<script lang="ts">
import CodeSample from '$components/CodeSample.svelte'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { PUBLIC_APP_DOMAIN } from '$src/env'
import { client } from '$src/pocketbase'
import { bash } from 'svelte-highlight/languages'
import AccordionItem from '../../../../components/AccordionItem.svelte'
import { instance } from './store'
const { user } = client()
$: ({ subdomain } = $instance)
const { email } = user() || {}
// This will hide the component if the email was not found
if (!email) {
throw new Error(`Email expected here`)
}
@ -17,41 +18,49 @@
)}@ftp.sfo-1.${PUBLIC_APP_DOMAIN}`
</script>
<AccordionItem title="FTP Access">
<p>
<Card>
<CardHeader
documentation="https://pockethost.gitbook.io/manual/daily-usage/ftp"
>
FTP Access
</CardHeader>
<p class="mb-8">
Securely access your instance files via FTPS. Use your PocketHost account
login and password.
</p>
<p>
<a href="https://pockethost.io/docs/usage/ftp">Full documentation</a>
</p>
<p>Bash:</p>
<CodeSample code={`ftp ${ftpUrl}`} language={bash} />
<table>
<thead><tr><th>Directory</th><th>Description</th></tr></thead>
<tr>
<th>pb_data</th><td>The PocketBase data directory</td>
</tr>
<tr>
<th>pb_public</th><td>Public files, such as a web frontend</td>
</tr>
<tr>
<th>pb_migrations</th><td>The PocketBase migrations directory</td>
</tr>
<tr>
<th>pb_hooks</th><td>The PocketBase JS hooks directory</td>
</tr>
</table>
</AccordionItem>
<style lang="scss">
table {
margin: 10px;
td,
tr,
th {
border: 2px solid rgb(92, 92, 157);
padding: 5px;
}
}
</style>
<p>Bash:</p>
<div class="mb-12">
<CodeSample code={`ftp ${ftpUrl}`} language={bash} />
</div>
<table class="table">
<thead>
<tr>
<th class="border-b-2 border-neutral">Directory</th>
<th class="border-b-2 border-neutral">Description</th>
</tr>
</thead>
<tbody>
<tr>
<th>pb_data</th>
<td>The PocketBase data directory</td>
</tr>
<tr>
<th>pb_public</th>
<td>Public files, such as a web frontend</td>
</tr>
<tr>
<th>pb_migrations</th>
<td>The PocketBase migrations directory</td>
</tr>
<tr>
<th>pb_hooks</th>
<td>The PocketBase JS hooks directory</td>
</tr>
</tbody>
</table>
</Card>

View File

@ -9,93 +9,81 @@
import { values } from '@s-libs/micro-dash'
import { onDestroy, onMount } from 'svelte'
import { writable } from 'svelte/store'
import AccordionItem from '../../../../components/AccordionItem.svelte'
import { instance } from './store'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
const { dbg, trace } = logger().create(`Logging.svelte`)
$: ({ id } = $instance)
// This takes in a log type and returns a specific text color
const logColor = (type: string) => {
if (type === 'system') return 'text-success'
if (type === 'info') return 'text-info'
if (type === 'error') return 'text-error'
return 'text-info'
}
// This will take in the log message and return either the message or a string
const logText = (log: any) => {
try {
return JSON.parse(log.message)
} catch (e) {
return log.message
}
}
const logs = writable<{ [_: RecordId]: InstanceLogFields }>({})
let logsArray: InstanceLogFields[] = []
const cm = createCleanupManager()
onMount(async () => {
dbg(`Watching instance log`)
const unsub = client().watchInstanceLog(id, (newLog) => {
trace(`Got new log`, newLog)
logs.update((currentLogs) => {
return { ...currentLogs, [newLog.id]: newLog }
})
logsArray = values($logs)
.sort((a, b) => (a.created > b.created ? 1 : -1))
.slice(0, 1000)
.reverse()
})
cm.add(unsub)
})
onDestroy(cm.shutdown)
</script>
<AccordionItem title="Instance Logging">
<p>
<Card>
<CardHeader>Instance Logging</CardHeader>
<p class="mb-4">
Instance logs appear here in realtime, including <code>console.log</code> from
JavaScript hooks.
</p>
<div class="log-window">
{#each logsArray as log}
<div class="log">
<div class="time">{log.created}</div>
<div class={`stream ${log.stream}`}>{log.stream}</div>
<div class={`message ${log.stream}`}>
{(() => {
try {
const parsed = JSON.parse(log.message)
return `<pre><code>${parsed}</code></pre>`
} catch (e) {
return log.message
}
})()}
<div class="mockup-code">
<div class="h-[450px] flex flex-col-reverse overflow-y-scroll">
{#each logsArray as log}
<div class="px-4" data-prefix=">">
<span class="text-xs mr-2"
><i class="fa-regular fa-angle-right"></i></span
>
<span class="text-xs mr-1">{log.created}</span>
<span class={`text-xs mr-1 font-bold ${logColor(log.stream)}`}
>{log.stream}</span
>
<span class="text-xs mr-1 text-base-content">{logText(log)}</span>
</div>
</div>
{/each}
</div></AccordionItem
>
<style lang="scss">
.log-window {
border: 1px solid gray;
padding: 5px;
height: 500px;
overflow: auto;
display: flex;
flex-direction: column-reverse;
white-space: nowrap;
.log {
position: relative;
font-family: monospace;
.time {
color: gray;
display: inline-block;
}
.stream {
color: gray;
display: inline-block;
&.system {
color: orange;
}
&.info {
color: blue;
}
&.error {
color: red;
}
}
.message {
display: inline-block;
}
}
}
</style>
{/each}
</div>
</div>
</Card>

View File

@ -1,22 +1,23 @@
<script lang="ts">
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
import AccordionItem from '../../../../components/AccordionItem.svelte'
import { instance } from './store'
$: ({ subdomain, status, version, secondsThisMonth } = $instance)
const url = `${PUBLIC_APP_PROTOCOL}://${subdomain}.${PUBLIC_APP_DOMAIN}`
$: ({ status, version, secondsThisMonth } = $instance)
</script>
<AccordionItem title="Overview" show={true}>
<div class="card card-body bg-base-200">
<h3 class="font-bold text-2xl">Overview</h3>
<div>
Status: <ProvisioningStatus {status} />
</div>
<div>
Usage: {Math.ceil(secondsThisMonth / 60)} mins
</div>
<div>
Version: {version} (change in Danger Zone)
Version: {version}
</div>
</AccordionItem>
</div>

View File

@ -1,110 +1,126 @@
<script lang="ts">
import { SECRET_KEY_REGEX } from '@pockethost/common'
// import the items as described in the store
import { items } from './stores.js'
import { slide } from 'svelte/transition'
// variables bound to the input elements
let name: string = ''
let value: string = ''
// Keep track of the new key and value to be added
let secretKey: string = ''
let secretValue: string = ''
// These will validate the key and value before being submitted
let isKeyValid = false
let isValueValid = false
let isFormValid = false
// following the submit event, proceed to update the store by adding the item or updating its value (if existing)
const handleSubmit = () => {
// findIndex returns -1 if a match is not found
const index = $items.findIndex((item) => item.name === name)
items.upsert({ name, value })
name = ''
value = ''
// This will animate a success message when the key is saved
let successfulSave = false
// Keep track of any error message
let errorMessage: string = ''
// Watch for changes in real time and update the key and value as the user types them
$: {
secretKey = secretKey.toUpperCase()
secretValue = secretValue.trim()
isKeyValid = !!secretKey.match(SECRET_KEY_REGEX)
isValueValid = secretValue.length > 0
isFormValid = isKeyValid && isValueValid
}
$: {
name = name.toUpperCase()
value = value.trim()
isKeyValid = !!name.match(SECRET_KEY_REGEX)
isValueValid = value.length > 0
isFormValid = isKeyValid && isValueValid
console.log({ isFormValid })
// Submit the form to create the new environment variable
const handleSubmit = async (e: Event) => {
e.preventDefault()
// Reset any messaging
errorMessage = ''
try {
// Block the button from submitting more than once
isFormValid = false
// Save to the database
items.upsert({ name: secretKey, value: secretValue })
// Reset the values when the POST is done
secretKey = ''
secretValue = ''
// Enable the submit button
isFormValid = true
// Show the success message
successfulSave = true
// Remove the success toast after a few seconds
setTimeout(() => {
successfulSave = false
}, 5000)
} catch (error: any) {
errorMessage = error.message
}
}
</script>
<!-- form component
introduce the component with a heading
describe the form with input elements of type text and number
-->
<div class="container">
<h2>Add an Environment Variable</h2>
<section>
<!-- display a form with 2 fields
- input[type="text"], describing the name
- input[type="number"], describing the value (price, cost, currently undecided)
<div class="mb-8">
<h4 class="flex items-center font-bold h-9 text-lg mb-3">
Add an Environment Variable
</h4>
-->
<!-- wrap each input in a label -->
<label>
<span>Name</span>
<input class="form-control" required type="text" bind:value={name} />
</label>
<label>
<span>Value</span>
<input class="form-control" bind:value placeholder="" type="password" />
</label>
<form on:submit={handleSubmit} class="mb-4">
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="label" for="secret-key">
<span class="label-text">Key</span>
</label>
<!-- describe the action of the icon button through aria attributes -->
<button
class="btn btn-primary"
aria-label="Create entry"
aria-describedby="description"
disabled={!isFormValid}
on:click={() => handleSubmit()}
>Add
</button>
{#if !isKeyValid && name.length > 0}
<div class="text-danger">
All key names must be upper case, alphanumeric, and may include
underscore (_).
<input
id="secret-key"
type="text"
bind:value={secretKey}
class="input input-bordered w-full max-w-xs"
/>
</div>
<div>
<label class="label" for="secret-value">
<span class="label-text">Value</span>
</label>
<input
id="secret-value"
type="text"
bind:value={secretValue}
class="input input-bordered w-full max-w-xs"
/>
</div>
</div>
{#if !isKeyValid && secretKey.length > 0}
<div in:slide class="alert alert-error mb-4">
<i class="fa-regular fa-circle-exclamation"></i>
All key names must be upper case, alphanumeric, and may include underscore
(_).
</div>
{/if}
</section>
</div>
<style lang="scss">
.container {
border: 1px solid black;
margin: 20px;
padding: 20px;
width: 300px;
h2 {
font-size: 13pt;
}
/* display the input in a wrapping row
flip the hue for the color and background
*/
section {
display: flex;
flex-wrap: wrap;
align-items: center;
color: var(--bs-gray-600);
background: var(--bs-gray-100);
padding: 0.75rem 1rem;
border-radius: 5px;
margin-bottom: 20px;
label {
flex-grow: 1;
display: flex;
flex-direction: column;
font-size: 1rem;
line-height: 2;
margin: 10px;
input {
font-size: 1.1rem;
color: inherit;
font-family: inherit;
background: none;
padding: 0.5rem 0.75rem;
}
}
}
}
</style>
<div class="text-right">
<button type="submit" class="btn btn-primary" disabled={!isFormValid}
>Add <i class="fa-regular fa-floppy-disk"></i></button
>
</div>
</form>
{#if successfulSave}
<div in:slide class="alert alert-success">
<i class="fa-regular fa-shield-check"></i>
Your new environment variable has been saved.
</div>
{/if}
{#if errorMessage}
<div in:slide class="alert alert-error mb-4">
<i class="fa-regular fa-circle-exclamation"></i>
{errorMessage}
</div>
{/if}
</div>

View File

@ -1,87 +1,54 @@
<script type="ts">
// import the items as described in the store
import { fade } from 'svelte/transition'
import { items } from './stores'
let showSecretKeys = false
</script>
{#if $items.length > 0}
<!-- introduce the section with a heading and describe the items in a main element -->
<section>
<!-- display the articles in a grid, specifying the name and numerical values in a column -->
<main>
{#each $items as item}
<article style="border-color: {item.color}">
<h2>{item.name}</h2>
<div class="value">
{item.value.slice(0, 2)}{item.value.slice(2).replaceAll(/./g, '*')}
</div>
<!-- following a click on the button update the store with the delete operation -->
<button on:click={() => items.delete(item.name)} aria-label="Delete">
<svg viewBox="0 0 100 100" width="30" height="30">
<use href="#delete" />
</svg>
</button>
</article>
{/each}
</main>
</section>
{/if}
<div class="flex items-center justify-between mb-3 h-9">
<h4 class="font-bold text-lg">Current Environment Variables</h4>
<style lang="scss">
/* display the items as squares in a grid */
main {
display: grid;
justify-content: center;
grid-template-columns: repeat(auto-fill, 150px);
grid-auto-rows: 150px;
grid-gap: 2rem;
}
/* display the text elements in a column */
article {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: hsla(240, 25%, 50%, 0.1);
border: 5px solid currentColor;
border-radius: 25px;
position: relative;
h2 {
font-weight: 400;
font-size: 10pt;
}
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text text-accent mr-2">Show Secrets</span>
<input
type="checkbox"
class="toggle toggle-sm"
bind:checked={showSecretKeys}
/>
</label>
</div>
</div>
.value {
font-size: 10pt;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700;
overflow: hidden;
width: 100%;
padding-left: 5px;
padding-right: 5px;
}
<table class="table">
<thead>
<tr>
<th class="w-2/5 border-b-2 border-neutral">Key</th>
<th class="w-2/5 border-b-2 border-neutral">Value</th>
<th class="w-1/5 border-b-2 border-neutral text-right">Actions</th>
</tr>
</thead>
button {
position: absolute;
top: 0%;
right: 0%;
transform: translate(50%, -50%);
background: none;
border: none;
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
color: inherit;
background: currentColor;
/* use the same hue as the background to fake a clip on the border underneath */
box-shadow: 0 0 0 0.5rem hsl(240, 25%, 20%);
svg {
display: block;
width: 100%;
height: 100%;
color: hsl(240, 25%, 20%);
}
}
}
/* absolute position the button in the top right corner */
</style>
<tbody>
{#each $items as item}
<tr transition:fade>
<th>{item.name}</th>
<td
>{showSecretKeys
? item.value
: item.value.slice(0, 2) +
item.value.slice(2).replaceAll(/./g, '*')}</td
>
<td class="text-right">
<button
aria-label="Delete"
on:click={() => items.delete(item.name)}
type="button"
class="btn btn-sm btn-square btn-outline btn-warning"
><i class="fa-regular fa-trash"></i></button
>
</td>
</tr>
{/each}
</tbody>
</table>

View File

@ -8,30 +8,46 @@
} from '@pockethost/common'
import { forEach, reduce } from '@s-libs/micro-dash'
import { onDestroy, onMount } from 'svelte'
import AccordionItem from '../../../../../components/AccordionItem.svelte'
import { instance } from '../store'
import Form from './Form.svelte'
import List from './List.svelte'
import { items } from './stores'
import SvgIcons from './SvgIcons.svelte'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
// TODO: Hot Reload is causing an infinite loop in the network tab for some reason. Wasn't able to figure out why
$: ({ id, secrets } = $instance)
// Keep track of which tab the user has selected
let activeTab = 0
// Toggle between the tabs on click
const handleTabChange = (id: number) => {
activeTab = id
}
const { dbg } = logger().create(`Secrets.svelte`)
const cm = createCleanupManager()
onMount(() => {
items.clear()
forEach(secrets || {}, (value, name) => {
items.upsert({ name, value })
})
let initial = false
const unsub = items.subscribe(async (secrets) => {
if (!initial) {
initial = true
return
}
dbg(`Got change`, secrets)
await client().saveSecrets({
instanceId: id,
secrets: reduce(
@ -45,56 +61,62 @@
),
})
})
cm.add(unsub)
})
onDestroy(cm.shutdown)
</script>
<AccordionItem title="Secrets">
<p>
<Card>
<CardHeader>Secrets</CardHeader>
<p class="mb-4">
These secrets are passed into your <code>pocketbase</code> executable and
can be accessed from <code>pb_hooks</code> JS hooks.
</p>
<CodeSample
code={$items
.map((secret) => `const ${secret.name} = process.env.${secret.name}`)
.join('\n')}
/>
<SvgIcons />
<Form />
<List />
</AccordionItem>
<!-- If the user has any secrets, render them in a code block -->
{#if $items.length > 0}
<div class="mb-8">
<CodeSample code={`const YOUR_KEY = process.env.YOUR_KEY`} />
</div>
{/if}
<style lang="scss">
.secrets {
box-sizing: border-box;
padding: 0;
margin: 0;
}
.secrets {
h2 {
position: relative;
padding: 0.25rem;
span {
position: absolute;
top: 0%;
right: 100%;
transform: translateY(-50%);
display: block;
width: 1.25em;
height: 1.25em;
border-radius: 0.75rem;
background: hsla(240, 25%, 50%, 0.3);
}
span,
span svg {
display: block;
width: 100%;
height: 100%;
filter: drop-shadow(0 0 3px hsla(240, 25%, 0%, 0.5));
}
}
}
</style>
{#if $items.length === 0}
<div class="alert border-2 border-neutral mb-8">
<i class="fa-regular fa-shield-keyhole"></i>
<span>No Environment Variables Found</span>
</div>
{/if}
<div class="tabs mb-4 border-b-[1px] border-neutral">
<button
on:click={() => handleTabChange(0)}
type="button"
class="tab border-b-2 {activeTab === 0
? 'tab-active font-bold border-base-content'
: 'border-neutral'}"
><i class="fa-regular fa-plus mr-2"></i> Add New</button
>
<button
on:click={() => handleTabChange(1)}
type="button"
class="tab border-b-2 {activeTab === 1
? 'tab-active font-bold border-base-content'
: 'border-neutral'}"
><i class="fa-regular fa-list mr-2"></i> Current List</button
>
</div>
<div>
{#if activeTab === 0}
<Form />
{/if}
{#if activeTab === 1}
<List />
{/if}
</div>
</Card>

View File

@ -1,67 +0,0 @@
<!-- describe the graphics included throughout the project -->
<svg viewBox="0 0 100 100" width="40" height="40" style="display: none;">
<symbol id="add">
<g
fill="none"
stroke="currentColor"
stroke-width="7"
stroke-linecap="round"
>
<path d="M 50 35 v 30 m -15 -15 h 30"></path>
</g>
</symbol>
<symbol id="create">
<g
fill="none"
stroke="currentColor"
stroke-width="7"
stroke-linecap="round"
>
<g transform="translate(76 24)">
<path
d="M -20 0 h -37.5 a 15 15 0 0 0 -15 15 v 42.5 a 15 15 0 0 0 15 15 h 42.5 a 15 15 0 0 0 15 -15 v -37.5"
></path>
<circle cx="0" cy="0" r="20"></circle>
<path stroke-width="5" d="M 0 -7 v 14 m -7 -7 h 14"></path>
</g>
</g>
</symbol>
<symbol id="list">
<g
fill="none"
stroke="currentColor"
stroke-width="7"
stroke-linecap="round"
>
<path d="M 50 35 h 20"></path>
<path d="M 30 50 h 40"></path>
<path d="M 30 65 h 20"></path>
</g>
</symbol>
<symbol id="delete">
<g transform="translate(50 50)">
<g transform="rotate(45)">
<g
fill="none"
stroke="currentColor"
stroke-width="10"
stroke-linecap="round"
>
<path d="M 0 -20 v 40 m -20 -20 h 40"></path>
</g>
</g>
</g>
</symbol>
<symbol id="highlight">
<g
fill="none"
stroke="currentColor"
stroke-width="7"
stroke-linecap="round"
>
<path d="M 35 65 v -7.5"></path>
<path d="M 50 65 v -15"></path>
<path d="M 65 65 v -30"></path>
</g>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -48,7 +48,9 @@ function createItems(initialItems: SecretsArray) {
// create: add an object for the item at the end of the store's array
upsert: (item: SecretItem) => {
dbg(`Upserting`, item)
const { name, value } = sanitize(item)
return update((n) => {
return formatInput([
...n.filter((i) => i.name !== name),

View File

@ -0,0 +1,133 @@
<script lang="ts">
import { Line } from 'svelte-chartjs'
import { subMonths, format } from 'date-fns'
import { instance } from './store'
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale,
} from 'chart.js'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale,
)
$: ({ secondsThisMonth } = $instance)
// Calculate the last six months
const getLastSixMonths = () => {
let currentDate = new Date()
let months = Array.from({ length: 6 }, (_, index) => {
let date = subMonths(currentDate, index)
return format(date, 'MMM') // format as you need, e.g. 'yyyy-MM' will be '2023-09'
})
return months.reverse() // to have them in ascending order
}
// Set the default data object for the chart
let data = {}
// This will watch for changes in the `secondsThisMonth` variable and update the chart data
$: {
data = {
labels: getLastSixMonths(),
datasets: [
{
label: 'Usage',
fill: true,
lineTension: 0.3,
backgroundColor: '#1eb854',
borderColor: '#1eb854',
borderCapStyle: 'round',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: '#fff',
pointBackgroundColor: '#fff',
pointBorderWidth: 5,
pointHoverRadius: 5,
pointHoverBackgroundColor: '#1eb854',
pointHoverBorderColor: 'rgba(220, 220, 220,1)',
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 25,
data: [24, 3, 16, 56, 55, Math.ceil(secondsThisMonth / 60)],
},
],
}
}
let options = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
title: {
text: 'Minutes',
display: true,
},
},
},
plugins: {
legend: {
display: false,
},
},
}
</script>
<Card block={false}>
<CardHeader>Usage</CardHeader>
<div class="h-full relative">
<div class="h-full blur">
<Line {data} {options} />
</div>
<div
class="inset-center z-10 border-info border-2 rounded-2xl mx-auto w-full"
>
<div class="alert">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-info shrink-0 w-6 h-6"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path></svg
>
<span>Usage Charts Coming Soon</span>
</div>
</div>
</div>
</Card>
<style>
.inset-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>

View File

@ -1,31 +0,0 @@
<script lang="ts">
import { client } from '$src/pocketbase'
import MiniEdit from '../../../../components/MiniEdit.svelte'
import { instance } from './store'
$: ({ id, maintenance } = $instance)
let _version = $instance.version
const saveEdit = async (newValue: string) =>
client()
.saveVersion({ instanceId: id, version: newValue })
.then(() => {
_version = newValue
return 'saved'
})
</script>
<div>
<h3>Version Lock</h3>
<p class="text-danger">
Warning - changing your version number should only be done when the instance
is in maintenance mode and you have already done a fresh backup. Depending
on the upgrade/downgrade you are performing, your instance may become
inoperable. If that happens, you may need to manually upgrade your database
locally. See <a href="https://pockethost.io/docs/usage/upgrading"
>upgrading</a
> for more information. name.
</p>
Version <MiniEdit value={_version} save={saveEdit} disabled={!maintenance} />
</div>

View File

@ -1,8 +1,10 @@
<script lang="ts">
import AlertBar from '$components/AlertBar.svelte'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { PUBLIC_APP_DOMAIN } from '$src/env'
import { handleCreateNewInstance } from '$util/database'
import { generateSlug } from 'random-word-slugs'
import { slide } from 'svelte/transition'
let instanceName: string = generateSlug(2)
let formError: string = ''
@ -36,77 +38,49 @@
<title>New Instance - PocketHost</title>
</svelte:head>
<div class="container">
<div class="py-4">
<h1 class="text-center">Choose a name for your PocketBase app.</h1>
</div>
<h2 class="text-4xl text-base-content font-bold capitalize mb-6">
Create A New App
</h2>
<div class="row g-3 align-items-center justify-content-center mb-4">
<div class="col-auto">
<label for="instance-name" class="col-form-label">Instance Name:</label>
</div>
<div class="grid lg:grid-cols-2 grid-cols-1">
<Card>
<form on:submit={handleSubmit}>
<CardHeader>Choose a name for your PocketBase app.</CardHeader>
<div class="col-auto pe-1 position-relative">
<input
type="text"
id="instance-name"
class="form-control"
bind:value={instanceName}
/>
<div class="flex rename-instance-form-container-query gap-4">
<input
type="text"
bind:value={instanceName}
class="input input-bordered w-full"
/>
<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>
<button
type="button"
class="btn btn-outline btn-secondary"
aria-label="Regenerate Instance Name"
on:click={handleInstanceNameRegeneration}
><i class="fa-regular fa-arrows-rotate"></i></button
>
</div>
<div class="col-auto ps-0">
<span class="form-text">.{PUBLIC_APP_DOMAIN}</span>
</div>
</div>
<h4 class="text-center font-bold py-12">
https://{instanceName}.{PUBLIC_APP_DOMAIN}
</h4>
{#if formError}
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
{/if}
{#if formError}
<div transition:slide class="alert alert-error mb-5">
<i class="fa-solid fa-circle-exclamation"></i>
<span>{formError}</span>
</div>
{/if}
<div class="text-center">
<a href="/dashboard" class="btn btn-light" disabled={isFormButtonDisabled}
>Cancel</a
>
<div class="flex items-center justify-center gap-4">
<a href="/dashboard" class="btn">Cancel</a>
<button
class="btn btn-primary"
disabled={isFormButtonDisabled}
on:click={handleSubmit}
>
Create <i class="bi bi-arrow-right-short" />
</button>
</div>
<button class="btn btn-primary" disabled={isFormButtonDisabled}>
Create <i class="bi bi-arrow-right-short" />
</button>
</div>
</form>
</Card>
</div>
<style lang="scss">
.container {
max-width: 600px;
min-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.regenerate-instance-name-btn {
padding: 0;
width: 34px;
height: 34px;
position: absolute;
z-index: 500;
top: 2px;
right: 6px;
transition: all 200ms;
}
</style>

View File

@ -1,52 +1,10 @@
<script lang="ts">
import { browser } from '$app/environment'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
import RetroBoxContainer from '$components/RetroBoxContainer.svelte'
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
import { client } from '$src/pocketbase'
import {
logger,
type InstanceFields,
type InstanceId,
type InstanceRecordsById,
} from '@pockethost/common'
import { globalInstancesStore } from '$util/stores'
import { values } from '@s-libs/micro-dash'
import { onDestroy, onMount } from 'svelte'
import { writable } from 'svelte/store'
import { fade } from 'svelte/transition'
import InstanceList from './InstanceList.svelte'
const { error } = logger()
let apps: InstanceRecordsById = {}
const instancesStore = writable<{ [_: InstanceId]: InstanceFields }>({})
$: isFirstApplication = values($instancesStore).length === 0
onMount(() => {
if (browser) {
;(async () => {
const { getAllInstancesById } = client()
const instances = await getAllInstancesById()
instancesStore.set(instances)
console.log({ instances })
client()
.client.collection('instances')
.subscribe<InstanceFields>('*', (data) => {
instancesStore.update((instances) => {
instances[data.record.id] = data.record
return instances
})
})
})().catch(error)
}
})
onDestroy(() => {
if (browser) {
client().client.collection('instances').unsubscribe('*').catch(error)
}
})
$: isFirstApplication = values($globalInstancesStore).length === 0
</script>
<svelte:head>
@ -54,112 +12,17 @@
</svelte:head>
<AuthStateGuard>
<div class="container" in:fade={{ duration: 30 }}>
{#if !isFirstApplication}
<div class="py-4">
<h1 class="text-center">Your Apps</h1>
</div>
{#if !isFirstApplication}
<div class="flex items-center justify-between mb-6">
<h2 class="text-4xl text-base-content font-bold capitalize">Dashboard</h2>
<div class="row justify-content-center">
{#each values($instancesStore) as app}
<div class="col-xl-4 col-md-6 col-12 mb-5">
<div class="card">
<div
class="server-status d-flex align-items-center justify-content-between"
>
<div class="server-status-minutes">
Usage: {Math.ceil(app.secondsThisMonth / 60)} mins
{#if app.maintenance}
<span class="text-warning">Maintenance Mode</span>
{/if}
</div>
<div
class="d-flex align-items-center gap-3 server-status-minutes"
>
{app.version}
<ProvisioningStatus status={app.status} />
</div>
</div>
<h2 class="mb-4 font-monospace">{app.subdomain}</h2>
<div class="d-flex justify-content-around">
<a href={`/app/instances/${app.id}`} class="btn btn-light">
<i class="bi bi-gear-fill" />
<span>Details</span>
</a>
<a
class="btn btn-light pocketbase-button"
href={`${PUBLIC_APP_PROTOCOL}://${app.subdomain}.${PUBLIC_APP_DOMAIN}/_`}
target="_blank"
>
<img
src="/images/pocketbase-logo.svg"
alt="PocketBase Logo"
class="img-fluid"
/>
<span>Admin</span>
</a>
</div>
</div>
</div>
{/each}
</div>
{/if}
<div class="first-app-screen">
<RetroBoxContainer minHeight={isFirstApplication ? 500 : 0}>
<div class="px-lg-5">
<h2 class="mb-4">
Create Your {isFirstApplication ? 'First' : 'Next'} App
</h2>
<a href="/app/new" class="btn btn-primary btn-lg"
><i class="bi bi-plus" /> New App</a
>
</div>
</RetroBoxContainer>
<a href="/app/new" class="m-3 btn btn-primary"
><i class="fa-solid fa-plus"></i> New App</a
>
</div>
</div>
<!--<UsageChartForAllInstances />-->
<InstanceList />
{/if}
</AuthStateGuard>
<style lang="scss">
.first-app-screen {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
padding: 85px 0;
}
.card {
border: 0;
padding: 42px 24px 24px 24px;
box-shadow: var(--soft-box-shadow);
}
.server-status {
position: absolute;
top: 8px;
right: 16px;
width: calc(100% - 32px);
}
.server-status-minutes {
font-size: 13px;
}
.server-status-icons {
}
.pocketbase-button img {
max-width: 25px;
}
@media screen and (min-width: 768px) {
.first-app-screen {
min-height: 70vh;
}
}
</style>

View File

@ -0,0 +1,72 @@
<script>
import { globalInstancesStore } from '$util/stores'
import { fly } from 'svelte/transition'
import { backOut } from 'svelte/easing'
import { PUBLIC_APP_DOMAIN } from '$src/env'
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
// Convert the object of objects into an array of objects
const allInstancesArray = Object.values($globalInstancesStore)
</script>
<Card height="h-auto">
<CardHeader>Active Instances</CardHeader>
<div class="grid">
{#each allInstancesArray as instance, index}
<div
class="lg:flex items-center justify-between transition-all duration-500 lg:py-8 py-16 px-4 rounded-2xl {index %
2 ===
0
? ''
: 'bg-base-100'}"
>
<div class="lg:text-left text-center mb-6 lg:mb-0">
<h4 class="font-bold capitalize mb-2">{instance.subdomain}</h4>
<div class="flex items-center flex-wrap justify-center gap-2">
<div class="badge badge-accent badge-outline">
Status: &nbsp;<span class="capitalize">{instance.status}</span>
</div>
<div class="badge badge-accent badge-outline">
Usage: {Math.ceil(instance.secondsThisMonth / 60)} mins
</div>
<div class="badge badge-accent badge-outline">
Version: {instance.version}
</div>
{#if instance.maintenance}
<div class="badge badge-outline border-warning gap-2">
<i class="fa-regular fa-triangle-person-digging text-warning"
></i>
<span class="text-warning">Maintenance Mode</span>
</div>
{/if}
</div>
</div>
<div class="flex items-center justify-center gap-2">
<a href={`/app/instances/${instance.id}`} class="btn btn-primary">
<i class="fa-regular fa-circle-info"></i>
<span>Details</span>
</a>
<a
class="btn btn-secondary"
href={`https://${instance.subdomain}.${PUBLIC_APP_DOMAIN}/_`}
target="_blank"
>
<img
src="/images/pocketbase-logo.svg"
alt="PocketBase Logo"
class="w-6"
/>
<span>Admin</span>
</a>
</div>
</div>
{/each}
</div>
</Card>

View File

@ -0,0 +1,181 @@
<script lang="ts">
import { Line } from 'svelte-chartjs'
import { subMonths, format } from 'date-fns'
import { globalInstancesStore } from '$util/stores'
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale,
} from 'chart.js'
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale,
)
// Calculate the last six months
const getLastSixMonths = () => {
let currentDate = new Date()
let months = Array.from({ length: 6 }, (_, index) => {
let date = subMonths(currentDate, index)
return format(date, 'MMM') // format as you need, e.g. 'yyyy-MM' will be '2023-09'
})
return months.reverse() // to have them in ascending order
}
// Temporary function to generate a random number from 1 to 60 to use in the charts
function getRandomNumber() {
return Math.floor(Math.random() * 60) + 1
}
// Temporary function to generate a random number from 60 to 250 to use in the charts
function getRandomNumberLarge() {
return Math.floor(Math.random() * 250) + 60
}
// This will generate unique colors for each of the user's instances
const lineGraphColorArray = [
'#1EB854',
'#00A473',
'#008E83',
'#007783',
'#005F74',
'#2F4858',
'#1C6E7D',
'#039590',
'#4BBC8E',
'#9BDE7E',
'#9BDE7E',
'#4BBC8E',
'#039590',
'#1C6E7D',
'#2F4858',
]
// Convert the object of objects into an array of objects
const allInstancesArray = Object.values($globalInstancesStore)
// Loop through the instance list and build a ChartJS object for each one
const individualInstanceUsageData = allInstancesArray.map(
(instance, index) => {
return {
label: instance.subdomain,
fill: true,
lineTension: 0.3,
backgroundColor: lineGraphColorArray?.[index] ?? '#fff',
borderColor: lineGraphColorArray?.[index] ?? '#fff',
borderCapStyle: 'round',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: '#fff',
pointBackgroundColor: '#fff',
pointBorderWidth: 5,
pointHoverRadius: 5,
pointHoverBackgroundColor: lineGraphColorArray?.[index] ?? '#fff',
pointHoverBorderColor: 'rgba(220, 220, 220,1)',
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 25,
data: [
getRandomNumber(),
getRandomNumber(),
getRandomNumber(),
getRandomNumber(),
getRandomNumber(),
Math.ceil(instance.secondsThisMonth / 60),
],
}
},
)
// Loop through the instance list again, and create a "total usage" entry
const totalUsageAmount = allInstancesArray.reduce(
(total, instance) => total + instance.secondsThisMonth,
0,
)
// Add up the individual instance usages and the total usage
const allChartData = [
...individualInstanceUsageData,
{
label: 'All Instances',
fill: true,
lineTension: 0.3,
backgroundColor: '#fff',
borderColor: '#fff',
borderCapStyle: 'round',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: '#fff',
pointBackgroundColor: '#fff',
pointBorderWidth: 5,
pointHoverRadius: 5,
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: 'rgba(220, 220, 220,1)',
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 25,
data: [
getRandomNumberLarge(),
getRandomNumberLarge(),
getRandomNumberLarge(),
getRandomNumberLarge(),
getRandomNumberLarge(),
Math.ceil(totalUsageAmount / 60),
],
},
]
// Set the default data object for the chart
let data = {
labels: getLastSixMonths(),
datasets: allChartData,
}
let options = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
title: {
text: 'Minutes',
display: true,
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
padding: 24,
cornerRadius: 16,
titleSpacing: 12,
},
},
}
</script>
<div class="card card-body bg-base-200 h-[600px] mb-4">
<h3 class="text-xl font-bold mb-16">Usage for All Instances</h3>
<div class="h-full">
<Line {data} {options} />
</div>
</div>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { handleLogin } from '$util/database'
import AlertBar from '$components/AlertBar.svelte'
import { handleLogin } from '$util/database'
let email: string = ''
let password: string = ''
@ -26,90 +26,59 @@
<title>Sign In - PocketHost</title>
</svelte:head>
<div class="page-bg">
<div class="card">
<h2 class="mb-4">Login</h2>
<div class="flex justify-center">
<div class="card w-96 bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">Login</h2>
<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>
<form on:submit={handleSubmit}>
<div class="form-control w-full max-w-xs">
<label class="label" for="email">Email address</label>
<input
type="email"
class="input input-bordered w-full max-w-xs"
id="email"
placeholder="name@example.com"
bind:value={email}
required
autocomplete="email"
/>
</div>
<div class="form-control w-full max-w-xs">
<label class="label" for="password">Password</label>
<input
type="password"
class="input input-bordered w-full max-w-xs"
id="password"
placeholder="Password"
bind:value={password}
required
autocomplete="current-password"
/>
</div>
<p class="m-3"></p>
{#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">
<a class="link" href="/signup">sign up</a> |
<a class="link" href="/login/password-reset">forgot password</a>
</div>
<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>
<p class="password-reset-container">
<a href="/login/password-reset">Forgot Your Password?</a>
</p>
{#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>
</div>
<style lang="scss">
.page-bg {
background-color: #222;
background-image: var(--gradient-light-soft-blue);
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - 91px);
padding: 0 18px;
}
.card {
border: 0;
box-shadow: var(--soft-box-shadow);
padding: 24px;
max-width: 425px;
width: 100%;
border-radius: 24px;
}
.password-reset-container {
font-size: 13px;
text-align: right;
}
@media screen and (min-width: 768px) {
.card {
padding: 48px;
}
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { handleUnauthenticatedPasswordReset } from '$util/database'
import AlertBar from '$components/AlertBar.svelte'
import { handleUnauthenticatedPasswordReset } from '$util/database'
let email: string = ''
let formError: string = ''
@ -28,81 +28,52 @@
<title>Password Reset - PocketHost</title>
</svelte:head>
<div class="page-bg">
<div class="card">
{#if userShouldCheckTheirEmail}
<div class="text-center">
<h2 class="mb-4">Check Your Email</h2>
<p>
A verification link has been sent to <br /><strong>{email}</strong>
</p>
<div class="flex justify-center">
<div class="card w-96 bg-base-100 shadow-xl">
<div class="card-body">
{#if userShouldCheckTheirEmail}
<div class="text-center">
<h2 class="mb-4">Check Your Email</h2>
<p>
A verification link has been sent to <br /><strong>{email}</strong>
</p>
<div class="display-1">
<i class="bi bi-envelope-check" />
<div class="display-1">
<i class="bi bi-envelope-check" />
</div>
</div>
</div>
{:else}
<h2 class="mb-4">Password Reset</h2>
{:else}
<h2 class="card-title mb-4">Password Reset</h2>
<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>
<form on:submit={handleSubmit}>
<div class="form-control w-full max-w-xs">
<label class="label" for="email">Email address</label>
<input
type="email"
class="input input-bordered w-full max-w-xs"
id="email"
placeholder="name@example.com"
bind:value={email}
required
autocomplete="email"
/>
</div>
{#if formError}
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
{/if}
{#if formError}
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
{/if}
<button
type="submit"
class="btn btn-primary w-100"
disabled={isFormButtonDisabled}
>
Send Verification Email <i class="bi bi-arrow-right-short" />
</button>
</form>
{/if}
<div class="py-4"><hr /></div>
<div class="text-center">
Need to <a href="/signup">create an account</a>?
<div class="mt-4 card-actions justify-end">
<button
type="submit"
class="btn btn-primary w-100"
disabled={isFormButtonDisabled}
>
Send Verification Email <i class="bi bi-arrow-right-short" />
</button>
</div>
</form>
{/if}
</div>
</div>
</div>
<style lang="scss">
.page-bg {
background-color: #222;
background-image: var(--gradient-light-soft-blue-vertical);
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - 91px);
padding: 0 18px;
}
.card {
border: 0;
box-shadow: var(--soft-box-shadow);
padding: 24px;
max-width: 425px;
width: 100%;
border-radius: 24px;
}
@media screen and (min-width: 768px) {
.card {
padding: 48px;
}
}
</style>

View File

@ -37,81 +37,56 @@
<title>Sign Up - PocketHost</title>
</svelte:head>
<div class="page-bg">
<div class="card">
<h2 class="mb-4">Sign Up</h2>
<div class="flex justify-center">
<div class="card w-96 bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4">Sign Up</h2>
<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>
<form on:submit={handleSubmit}>
<div class="form-control w-full max-w-xs">
<label class="label" for="email">Email address</label>
<input
type="email"
class="input input-bordered w-full max-w-xs"
id="email"
placeholder="name@example.com"
bind:value={email}
required
autocomplete="email"
/>
</div>
<div class="form-control w-full max-w-xs">
<label class="label" for="password">Password</label>
<input
type="password"
class="input input-bordered w-full max-w-xs"
id="password"
placeholder="Password"
bind:value={password}
required
autocomplete="new-password"
/>
</div>
{#if formError}
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
{/if}
<button
type="submit"
class="m-3 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">
<a class="link" href="/login">log in</a>
</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>
</div>
<style lang="scss">
.page-bg {
background-color: #222;
background-image: var(--gradient-dark-soft-blue);
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - 91px);
padding: 0 18px;
}
.card {
border: 0;
box-shadow: var(--soft-box-shadow);
padding: 24px;
max-width: 425px;
width: 100%;
border-radius: 24px;
}
@media screen and (min-width: 768px) {
.card {
padding: 48px;
}
}
</style>

View File

@ -0,0 +1,64 @@
import { browser } from '$app/environment'
import { client } from '$src/pocketbase'
import { instance } from '$src/routes/app/instances/[instanceId]/store'
import { globalInstancesStore } from '$util/stores'
import {
assertExists,
createCleanupManager,
logger,
type InstanceFields,
} from '@pockethost/common'
import { onDestroy, onMount } from 'svelte'
const { error } = logger()
const cm = createCleanupManager()
export const getInstances = async () => {
onMount(() => {
if (browser) {
;(async () => {
const { getAllInstancesById } = client()
const instances = await getAllInstancesById()
globalInstancesStore.set(instances)
client()
.client.collection('instances')
.subscribe<InstanceFields>('*', (data) => {
globalInstancesStore.update((instances) => {
instances[data.record.id] = data.record
return instances
})
})
})().catch(error)
}
})
// Stop listening to the db if this component unmounts
onDestroy(() => {
if (browser) {
client().client.collection('instances').unsubscribe('*').catch(error)
}
})
}
export const getSingleInstance = async (instanceId: string) => {
// Only run this on the browser
if (browser) {
const { dbg, error } = logger().create(`layout.svelte`)
const { watchInstanceById } = client()
watchInstanceById(instanceId, (r) => {
dbg(`Handling instance update`, r)
const { action, record } = r
assertExists(record, `Expected instance here`)
// Update the page state with the instance information
instance.set(record)
})
.then(cm.add)
.catch(error)
}
}

View File

@ -1,7 +1,11 @@
import { browser } from '$app/environment'
import { client } from '$src/pocketbase'
import type { AuthStoreProps } from '$src/pocketbase/PocketbaseClient'
import { logger } from '@pockethost/common'
import {
logger,
type InstanceFields,
type InstanceId,
} from '@pockethost/common'
import { writable } from 'svelte/store'
export const authStoreState = writable<AuthStoreProps>({
@ -9,6 +13,7 @@ export const authStoreState = writable<AuthStoreProps>({
model: null,
token: '',
})
export const isUserLoggedIn = writable(false)
export const isUserVerified = writable(false)
export const isAuthStateInitialized = writable(false)
@ -20,7 +25,7 @@ if (browser) {
* Listen for auth change events. When we get at least one, the auth state is initialized.
*/
onAuthChange((authStoreProps) => {
const { dbg, error, warn } = logger()
const { dbg } = logger()
dbg(`onAuthChange in store`, { ...authStoreProps })
authStoreState.set(authStoreProps)
isAuthStateInitialized.set(true)
@ -28,9 +33,14 @@ if (browser) {
// Update derived stores when authStore changes
authStoreState.subscribe((authStoreProps) => {
const { dbg, error, warn } = logger()
const { dbg } = logger()
dbg(`subscriber change`, authStoreProps)
isUserLoggedIn.set(authStoreProps.isValid)
isUserVerified.set(!!authStoreProps.model?.verified)
})
}
// This holds an array of all the user's instances and their data
export const globalInstancesStore = writable<{
[_: InstanceId]: InstanceFields
}>({})

View File

@ -1,239 +0,0 @@
body {
min-height: 101vh;
}
body,
p,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: 'Inter', sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 700;
}
:root {
--bs-primary-rgb: 52, 152, 219;
--bs-secondary-rgb: 127, 140, 141;
--bs-success-rgb: 46, 204, 113;
--bs-info-rgb: 236, 240, 241;
--bs-warning-rgb: 241, 196, 15;
--bs-danger-rgb: 192, 57, 43;
--bs-primary: #3498db;
--bs-secondary: #7f8c8d;
--bs-success: #2ecc71;
--bs-info: #ecf0f1;
--bs-warning: #f1c40f;
--bs-danger: #c0392b;
/* Custom Colors */
--bs-orange: #e67e22;
--gradient-white-lime: linear-gradient(
179.4deg,
rgb(252, 239, 233) 2.2%,
rgb(211, 242, 185) 96.2%
);
--gradient-light-soft-blue: linear-gradient(
109.6deg,
rgba(125, 89, 252, 1) 11.2%,
rgba(218, 185, 252, 1) 91.1%
);
--gradient-dark-soft-blue: linear-gradient(
109.6deg,
rgba(218, 185, 252, 1) 11.2%,
rgba(125, 89, 252, 1) 91.1%
);
--gradient-light-soft-blue-vertical: linear-gradient(
0deg,
rgba(125, 89, 252, 1) 11.2%,
rgba(218, 185, 252, 1) 91.1%
);
--soft-box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
}
.btn.btn-primary,
.btn.btn-outline-primary,
.btn.btn-light {
--bs-btn-padding-x: 24px;
--bs-btn-padding-y: 8px;
}
.btn.btn-primary {
/* --bs-btn-bg: #965de9; */
}
.btn.btn-outline-primary {
--bs-btn-color: #965de9;
--bs-btn-border-color: #965de9;
--bs-btn-hover-bg: #965de9;
--bs-btn-hover-border-color: #965de9;
--bs-btn-active-bg: #965de9;
--bs-btn-active-border-color: #965de9;
}
.btn-light {
--bs-btn-color: var(--bs-body-color);
--bs-btn-bg: var(--bs-gray-200);
--bs-btn-border-color: var(--bs-gray-100);
--bs-btn-hover-color: var(--bs-body-color);
--bs-btn-hover-bg: var(--bs-gray-300);
--bs-btn-hover-border-color: var(--bs-gray-300);
--bs-btn-focus-shadow-rgb: 211, 212, 213;
--bs-btn-active-color: var(--bs-body-color);
--bs-btn-active-bg: var(--bs-gray-300);
--bs-btn-active-border-color: var(--bs-gray-300);
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: var(--bs-body-color);
--bs-btn-disabled-bg: var(--bs-gray-500);
--bs-btn-disabled-border-color: var(--bs-gray-500);
}
.btn-dark {
--bs-btn-color: var(--bs-body-bg);
--bs-btn-bg: var(--bs-body-color);
--bs-btn-border-color: var(--bs-body-color);
--bs-btn-hover-color: var(--bs-body-bg);
--bs-btn-hover-bg: var(--bs-gray-700);
--bs-btn-hover-border-color: var(--bs-gray-700);
--bs-btn-focus-shadow-rgb: 66, 70, 73;
--bs-btn-active-color: var(--bs-body-bg);
--bs-btn-active-bg: var(--bs-gray-700);
--bs-btn-active-border-color: var(--bs-gray-700);
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: var(--bs-body-bg);
--bs-btn-disabled-bg: var(--bs-gray-500);
--bs-btn-disabled-border-color: var(--bs-gray-500);
}
.btn-outline-dark {
--bs-btn-color: var(--bs-gray-900);
--bs-btn-border-color: var(--bs-gray-900);
--bs-btn-hover-color: var(--bs-body-color);
--bs-btn-hover-bg: var(--bs-gray-200);
--bs-btn-hover-border-color: var(--bs-gray-900);
--bs-btn-focus-shadow-rgb: 33, 37, 41;
--bs-btn-active-color: var(--bs-body-color);
--bs-btn-active-bg: var(--bs-gray-200);
--bs-btn-active-border-color: var(--bs-gray-900);
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: var(--bs-gray-900);
--bs-btn-disabled-bg: transparent;
--bs-btn-disabled-border-color: var(--bs-gray-900);
--bs-gradient: none;
}
.btn-outline-dark.nav-link:hover {
--bs-nav-link-hover-color: var(--bs-black);
}
.dropdown-menu {
--bs-dropdown-item-padding-y: 12px;
}
.alert {
--bs-alert-border-radius: 18px;
}
.form-control,
.form-control:focus {
border-radius: 18px;
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
}
.card {
--bs-card-bg: var(--bs-gray-100);
}
.navbar {
--bs-navbar-color: var(--bs-body-color);
--bs-navbar-hover-color: var(--bs-gray-500);
}
/* Dark Mode */
[data-bs-theme='dark']:root {
--bs-black: #fff;
--bs-white: #000;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #212529;
--bs-gray-200: #343a40;
--bs-gray-300: #495057;
--bs-gray-400: #6c757d;
--bs-gray-500: #adb5bd;
--bs-gray-600: #ced4da;
--bs-gray-700: #dee2e6;
--bs-gray-800: #e9ecef;
--bs-gray-900: #f8f9fa;
--bs-light: #212529;
--bs-dark: #f8f9fa;
--bs-light-rgb: 33, 37, 41;
--bs-dark-rgb: 248, 249, 250;
--bs-white-rgb: 0, 0, 0;
--bs-black-rgb: 255, 255, 255;
--bs-body-color-rgb: 255, 255, 255;
--bs-body-bg-rgb: 33, 37, 41;
--bs-body-color: #fff;
--bs-body-bg: #222;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-link-color: #0d6efd;
--bs-link-hover-color: #0a58ca;
--bs-code-color: #d63384;
--bs-highlight-bg: #fff3cd;
--gradient-white-lime: linear-gradient(179.4deg, rgb(3, 16, 22) 2.2%, rgb(44, 13, 67) 96.2%);
--gradient-light-soft-blue: linear-gradient(
109.6deg,
rgba(125, 89, 252, 1) 11.2%,
rgba(25, 25, 25, 1) 91.1%
);
--gradient-dark-soft-blue: linear-gradient(
109.6deg,
rgba(25, 25, 25, 1) 11.2%,
rgba(125, 89, 252, 1) 91.1%
);
--gradient-light-soft-blue-vertical: linear-gradient(
0deg,
rgba(125, 89, 252, 1) 11.2%,
rgba(25, 25, 25, 1) 91.1%
);
--soft-box-shadow: rgba(0, 0, 0, 0.3) 0px 7px 29px 0px;
--bs-primary-bg-subtle: #292f37;
--bs-primary-text-emphasis: #a2c8ff;
}
.copy-container {
min-height: 60px;
}
.copy-container pre {
padding-bottom: 0px;
margin-bottom: 0px;
}
.copy-container code {
min-height: 60px;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,32 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{svelte,js,ts,md}'],
theme: {
extend: {},
},
daisyui: {
themes: [
'light',
'dark',
{
// Custom theme definitions
dark: {
...require('daisyui/src/theming/themes')['[data-theme=dark]'],
primary: '#1eb854',
secondary: '#1db990',
'base-content': '#ffffff',
},
},
{
// Custom theme definitions
light: {
...require('daisyui/src/theming/themes')['[data-theme=light]'],
primary: '#1eb854',
secondary: '#1db990',
'base-content': '#222',
},
},
],
},
plugins: [require('@tailwindcss/typography'), require('daisyui')],
}

View File

@ -371,6 +371,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@kurkle/color@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.10"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"
@ -1324,6 +1329,13 @@ character-parser@^2.2.0:
dependencies:
is-regex "^1.0.3"
chart.js@4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.0.tgz#df843fdd9ec6bd88d7f07e2b95348d221bd2698c"
integrity sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==
dependencies:
"@kurkle/color" "^0.3.0"
chokidar-cli@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chokidar-cli/-/chokidar-cli-3.0.0.tgz#29283666063b9e167559d30f247ff8fc48794eb7"
@ -1685,6 +1697,17 @@ daisyui@^3.7.7:
postcss-js "^4"
tailwindcss "^3"
daisyui@^3.8.1:
version "3.8.3"
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-3.8.3.tgz#75ff35d544d2ff3c745093be84e6bd267d560ed9"
integrity sha512-64QuR6niTv58E5YyAVBkoXQGngp04jt6XOcKGxnq1C/7r5ZE3bDuOSc4ktT7CPacTRjl5cCeNYY13VxLpZP+7A==
dependencies:
colord "^2.9"
css-selector-tokenizer "^0.8"
postcss "^8"
postcss-js "^4"
tailwindcss "^3"
data-uri-to-buffer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
@ -5121,6 +5144,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svelte-chartjs@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/svelte-chartjs/-/svelte-chartjs-3.1.2.tgz#741114fcf8168615535de33b360a6bafcee2344e"
integrity sha512-3+6gY2IJ9Ua8R9pk3iS1ypa7Z9OoXCJb9oPwIfTp7caJM+X+RrWnH2CTkGAq7FeSxc2nnmW08tYN88Q8Y+5M+w==
svelte-check@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-3.5.2.tgz#d6e650996afbe80f5e5b9b02d3fb9489f7d6fb8a"