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 # Source files
/src/assets/_bootstrap.css /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 ## 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! 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": "*", "@pockethost/common": "*",
"@sveltejs/adapter-static": "^2.0.3", "@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.25.1", "@sveltejs/kit": "^1.25.1",
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"boolean": "^3.2.0", "boolean": "^3.2.0",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"d3-scale-chromatic": "^3.0.0", "d3-scale-chromatic": "^3.0.0",
"daisyui": "^3.8.1",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"highlight.js": "^11.8.0", "highlight.js": "^11.8.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@ -31,6 +33,9 @@
"svelte-check": "^3.5.2", "svelte-check": "^3.5.2",
"svelte-highlight": "^7.3.0", "svelte-highlight": "^7.3.0",
"svelte-preprocess": "^5.0.4", "svelte-preprocess": "^5.0.4",
"svelte-chartjs": "3.1.2",
"chart.js": "4.4.0",
"tailwindcss": "^3.3.3",
"url-pattern": "^1.0.3", "url-pattern": "^1.0.3",
"vite": "^4.4.9" "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 {} = {}> { interface Metadata<TAttributes extends {} = {}> {
attributes: TAttributes attributes: TAttributes

View File

@ -1,9 +1,10 @@
<!DOCTYPE html> <!doctype html>
<html lang="en" data-bs-theme="light"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>PocketHost</title>
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <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 <link
rel="stylesheet" rel="stylesheet"
@ -11,36 +12,15 @@
id="hljs-link" 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 href="/icons/fontawesome.min.css" rel="stylesheet">
<link href="/icons/all.min.css" rel="stylesheet">
<link <link href="/icons/brands.min.css" rel="stylesheet">
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css"
/>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body> <body>
<div>%sveltekit.body%</div> <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> </body>
</html> </html>

File diff suppressed because one or more lines are too long

View File

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

View File

@ -26,12 +26,13 @@
.copy-container { .copy-container {
position: relative; position: relative;
margin: 5px; margin: 5px;
border: 1px solid gray; border-radius: 16px;
overflow: hidden;
.copy-button { .copy-button {
position: absolute; position: absolute;
top: 2px; top: 8px;
right: 2px; right: 8px;
} }
} }
</style> </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"> <script lang="ts">
import { page } from '$app/stores'
import Logo from '$components/Logo.svelte'
import MediaQuery from '$components/MediaQuery.svelte' import MediaQuery from '$components/MediaQuery.svelte'
import ThemeToggle from '$components/ThemeToggle.svelte' import ThemeToggle from '$components/ThemeToggle.svelte'
import { PUBLIC_POCKETHOST_VERSION } from '$src/env'
import { handleLogoutAndRedirect } from '$util/database' import { handleLogoutAndRedirect } from '$util/database'
import { isUserLoggedIn } from '$util/stores' import { getInstances } from '$util/getInstances'
import AuthStateGuard from './helpers/AuthStateGuard.svelte' 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> </script>
<header class="container-fluid"> <aside class="p-4 min-w-[250px] flex flex-col h-screen">
<nav class="navbar navbar-expand-md"> <MediaQuery query="(min-width: 1280px)" let:matches>
<a href="/" class="logo text-decoration-none d-flex align-items-center"> {#if matches}
<img <a href="/dashboard" class="flex gap-2 items-center justify-center">
src="/images/logo-square.png" <Logo hideLogoText={true} logoWidth="w-20" />
alt="PocketHost Logo" </a>
class="img-fluid" {/if}
/> </MediaQuery>
<h1>Pocket<span>Host</span></h1>
<sup class="">{PUBLIC_POCKETHOST_VERSION}</sup>
</a>
<button <div class="flex flex-col gap-2 mb-auto">
class="btn btn-light mobile-nav-button navbar-toggler" <a on:click={handleClick} href="/dashboard" class={linkClasses}
type="button" ><i
data-bs-toggle="collapse" class="fa-regular fa-table-columns {$page.url.pathname ===
data-bs-target="#nav-links" '/dashboard' && 'text-primary'}"
aria-controls="nav-links" ></i> Dashboard</a
aria-expanded="false"
aria-label="Toggle navigation"
> >
<i class="bi bi-list" />
</button>
<div class="collapse navbar-collapse" id="nav-links"> <div class="pl-8 flex flex-col gap-4 mb-4">
<ul class="navbar-nav ms-auto mb-2 mb-md-0"> {#each values($globalInstancesStore) as app}
<AuthStateGuard> <a
{#if $isUserLoggedIn} href={`/app/instances/${app.id}`}
<li class="nav-item text-md-start text-center"> on:click={handleClick}
<a class="nav-link" href="/dashboard">Dashboard</a> class={subLinkClasses}
</li> >
<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> <a href="/app/new" on:click={handleClick} class={addNewAppClasses}>
{#if matches} <i
<li class="nav-item dropdown"> class="fa-regular fa-plus {$page.url.pathname === `/app/new` &&
<button 'text-primary'}"
class="btn border-0 nav-link dropdown-toggle" ></i> Create A New App
type="button" </a>
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>
</div> </div>
</nav>
</header>
<style lang="scss"> <a
header { href="https://github.com/benallfree/pockethost/discussions"
background-color: var(--bs-body-bg); class={linkClasses}
padding: 12px 24px; target="_blank"
border-bottom: 1px solid var(--bs-gray-300); 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 { <a
img { href="https://pockethost.io/docs"
max-width: 50px; class={linkClasses}
margin-right: 16px; 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 { <a
font-size: 36px; href="https://github.com/pockethost/pockethost"
font-weight: 300; class={linkClasses}
margin: 0; target="_blank"
color: var(--bs-body-color); 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 { <button type="button" class={linkClasses} on:click={handleLogoutAndRedirect}
font-weight: 700; ><i class="fa-regular fa-arrow-up-left-from-circle"></i> Logout</button
background-image: linear-gradient( >
83.2deg, </div>
rgba(150, 93, 233, 1) 10.8%,
rgba(99, 88, 238, 1) 94.3%
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
sup { <ThemeToggle />
margin-left: 4px; </aside>
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>

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"> <script lang="ts">
import { browser } from '$app/environment'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { import { ThemeNames, getCurrentTheme, setCurrentTheme } from './helpers/theme'
THEME_ICONS,
ThemeNames,
currentIcon,
getCurrentTheme,
setCurrentTheme,
} from './helpers/theme'
// This can change the CSS a bit depending on where the theme toggle is rendered // This will keep track of the toggle's state
export let navLink: boolean = false let isChecked = true
// Set the default icon to be light mode
let iconClass: string = browser ? currentIcon() : ''
// Wait for the DOM to be available // Wait for the DOM to be available
onMount(() => { 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()) updateTheme(getCurrentTheme())
}) })
// Alternate the theme values on toggle click // Alternate the theme values on toggle click
const handleClick = () => { const handleChange = (e: Event) => {
const newTheme = const target = e.target as HTMLInputElement
getCurrentTheme() === ThemeNames.Light const isChecked = target.checked
? ThemeNames.Dark
: ThemeNames.Light const newTheme = isChecked ? ThemeNames.Dark : ThemeNames.Light
updateTheme(newTheme) updateTheme(newTheme)
} }
const updateTheme = (themeName: ThemeNames) => { const updateTheme = (themeName: ThemeNames) => {
// Update the icon class name to toggle between light and dark mode
iconClass = THEME_ICONS[themeName]
setCurrentTheme(themeName) setCurrentTheme(themeName)
} }
</script> </script>
<button <div class="form-control">
type="button" <label class="label cursor-pointer">
class="{navLink && 'nav-link'} btn border-0 d-inline-block" <span class="label-text">Dark Mode</span>
aria-label="Toggle the site theme" <input
title="Toggle the site theme" type="checkbox"
on:click={handleClick} class="toggle"
> bind:checked={isChecked}
<i class={iconClass} /> on:change={handleChange}
</button> />
</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"> <script lang="ts">
import { isAuthStateInitialized } from '$util/stores' import { isAuthStateInitialized } from '$util/stores'
import { onMount } from 'svelte'
onMount(() => {})
</script> </script>
{#if $isAuthStateInitialized} {#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 <button
type="button" type="button"
class="btn btn-{style}" class="btn btn-{style} btn-sm"
{disabled} {disabled}
style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;" style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
on:click={click}><slot /></button 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', '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 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 STORAGE_NAME: string = 'theme'
export const THEME_ATTRIBUTE: string = 'data-bs-theme' export const THEME_ATTRIBUTE: string = 'data-theme'
export const THEME_ICONS: { [_ in ThemeNames]: string } = {
[ThemeNames.Light]: 'bi bi-moon-stars',
[ThemeNames.Dark]: 'bi bi-brightness-high',
}
export const html = () => { export const html = () => {
const htmlElement = document.querySelector('html') const htmlElement = document.querySelector('html')
@ -35,16 +31,12 @@ export const getCurrentTheme = () => {
return currentTheme return currentTheme
} }
export const currentIcon = () => {
return THEME_ICONS[getCurrentTheme()]
}
export const setCurrentTheme = (themeName: ThemeNames) => { export const setCurrentTheme = (themeName: ThemeNames) => {
html().setAttribute(THEME_ATTRIBUTE, themeName) html().setAttribute(THEME_ATTRIBUTE, themeName)
const theme = document.querySelector<HTMLLinkElement>('#hljs-link') const theme = document.querySelector<HTMLLinkElement>('#hljs-link')
if (theme) { if (theme) {
theme.href = HLJS_THEMES[themeName] theme.href = HLJS_THEMES[themeName]
} }
console.log(theme, themeName)
Cookies.set(STORAGE_NAME, 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> <script>
import InitializeTooltips from '$components/InitializeTooltips.svelte'
import Navbar from '$components/Navbar.svelte' import Navbar from '$components/Navbar.svelte'
import VerifyAccountBar from '$components/VerifyAccountBar.svelte' import VerifyAccountBar from '$components/VerifyAccountBar.svelte'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte' import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import Meta from '$components/helpers/Meta.svelte' import Meta from '$components/helpers/Meta.svelte'
import Protect from '$components/helpers/Protect.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> </script>
<Meta /> <Meta />
<Protect /> <Protect />
<ThemeDetector />
<Navbar /> {#if $isUserLoggedIn}
<AuthStateGuard>
<VerifyAccountBar />
</AuthStateGuard>
<AuthStateGuard> <div class="layout xl:flex">
<VerifyAccountBar /> <MediaQuery query="(min-width: 1280px)" let:matches>
</AuthStateGuard> {#if matches}
<Navbar />
{:else}
<MobileNavDrawer>
<Navbar />
</MobileNavDrawer>
{/if}
</MediaQuery>
<main data-sveltekit-prefetch> <div class="lg:p-4 lg:pt-0 xl:pt-4 min-h-screen grow">
<slot /> <div
</main> 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 /> {#if !$isUserLoggedIn}
<div>
<style lang="scss"> <slot />
</style> </div>
{/if}

View File

@ -1,9 +1,7 @@
<script lang="ts"> <script lang="ts">
import FeatureCard from '$components/FeatureCard.svelte' import Logo from '$components/Logo.svelte'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte' import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import HomepageHeroAnimation from '$components/HomepageHeroAnimation.svelte' import InstanceGeneratorWidget from '$components/login-register/InstanceGeneratorWidget.svelte'
import InstanceGeneratorWidget from '$components/InstanceGeneratorWidget.svelte'
import { PUBLIC_APP_DOMAIN } from '$src/env'
import { isUserLoggedIn } from '$util/stores' import { isUserLoggedIn } from '$util/stores'
</script> </script>
@ -11,210 +9,22 @@
<title>Home - PocketHost</title> <title>Home - PocketHost</title>
</svelte:head> </svelte:head>
<div class="container"> <div class="min-h-screen flex items-center justify-center">
<div class="row align-items-center justify-content-between hero"> <div>
<div class="col-lg-6 mb-5 mb-lg-0"> <AuthStateGuard>
<h2>Deploy <span>PocketBase</span> in 30 seconds</h2> <Logo />
<p class="mb-5"> {#if $isUserLoggedIn}
Spend less time on configuring your backend, and more time building new <div class="">
features for your web app. <a href="/dashboard" class="btn btn-primary"
</p> >Go to Your Dashboard <i class="bi bi-arrow-right-short" /></a
>
</div>
{/if}
<AuthStateGuard> {#if !$isUserLoggedIn}
{#if $isUserLoggedIn} <InstanceGeneratorWidget />
<div> {/if}
<a href="/dashboard" class="btn btn-primary" </AuthStateGuard>
>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>
</div> </div>
</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"> <script lang="ts">
import { page } from '$app/stores' import { page } from '$app/stores'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte' import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env' import { getSingleInstance } from '$util/getInstances'
import { client } from '$src/pocketbase' import { createCleanupManager } from '@pockethost/common'
import { import { onDestroy } from 'svelte'
assertExists,
createCleanupManager,
logger,
} from '@pockethost/common'
import { onDestroy, onMount } from 'svelte'
import { instance } from './store' import { instance } from './store'
const { instanceId } = $page.params
const cm = createCleanupManager() const cm = createCleanupManager()
onMount(async () => {
const { dbg, error } = logger().create(`layout.svelte`) // Run anytime the page params changes
const { watchInstanceById } = client() $: {
watchInstanceById(instanceId, (r) => { getSingleInstance($page.params.instanceId)
dbg(`Handling instance update`, r) }
const { action, record } = r
assertExists(record, `Expected instance here`)
instance.set(record)
})
.then(cm.add)
.catch(error)
})
onDestroy(() => cm.shutdown()) onDestroy(() => cm.shutdown())
</script> </script>
<AuthStateGuard> <AuthStateGuard>
<div class="container"> {#if $instance}
{#if $instance} <slot />
<h2> {/if}
{$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>
</AuthStateGuard> </AuthStateGuard>

View File

@ -1,35 +1,87 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
import { assertExists } from '@pockethost/common' import { assertExists } from '@pockethost/common'
import Code from './Code.svelte' import Code from './Code.svelte'
import Danger from './Danger/Danger.svelte' import UsageChart from './UsageChart.svelte'
import Ftp from './Ftpx.svelte' import Ftp from './Ftpx.svelte'
import Logging from './Logging.svelte' import Logging from './Logging.svelte'
import Overview from './Overview.svelte'
import Secrets from './Secrets/Secrets.svelte' import Secrets from './Secrets/Secrets.svelte'
import { instance } from './store' 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`) assertExists($instance, `Expected instance here`)
const { subdomain, maintenance } = $instance const { subdomain } = $instance
const url = `${PUBLIC_APP_PROTOCOL}://${subdomain}.${PUBLIC_APP_DOMAIN}`
const code = `const url = '${url}'\nconst client = new PocketBase(url)`
</script> </script>
<svelte:head> <svelte:head>
<title>{subdomain} details - PocketHost</title> <title>{subdomain} details - PocketHost</title>
</svelte:head> </svelte:head>
{#if $instance}
{#if $instance.maintenance} <div class="flex md:flex-row flex-col items-center justify-between mb-6 gap-4">
<div class="text-warning"> <div>
This instance is in Maintenance Mode and will not respond to requests. <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> </div>
{/if} </div>
<div class="accordion" id="accordionExample">
<Overview /> <a
<Ftp /> href="https://{$instance.subdomain}.{PUBLIC_APP_DOMAIN}/_"
<Code /> rel="noreferrer"
<Secrets /> target="_blank"
<Logging /> class="btn btn-primary"
<Danger /> >
<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> </div>
{/if} {/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"> <script lang="ts">
import AccordionItem from '$components/AccordionItem.svelte'
import CodeSample from '$components/CodeSample.svelte' import CodeSample from '$components/CodeSample.svelte'
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env' import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
import { instance } from './store' 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}` 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> </script>
<AccordionItem title="Code Samples"> <Card>
JavaScript: <CardHeader>Getting Started</CardHeader>
<CodeSample {code} />
</AccordionItem> <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"> <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 { client } from '$src/pocketbase'
import { instance } from '../store' import { instance } from '../store'
@ -7,19 +8,36 @@
$: ({ id, maintenance } = $instance) $: ({ id, maintenance } = $instance)
const onMaintenance = (maintenance: boolean) => const handleChange = (e: Event) => {
setInstanceMaintenance({ instanceId: id, maintenance }).then(() => 'saved') 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> </script>
<div> <Card>
<h3>Maintenance Mode</h3> <CardHeader
<p class="text-danger"> documentation="https://pockethost.gitbook.io/manual/daily-usage/maintenance"
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
> >
</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"> <script lang="ts">
import CodeSample from '$components/CodeSample.svelte' 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 { PUBLIC_APP_DOMAIN } from '$src/env'
import { client } from '$src/pocketbase' import { client } from '$src/pocketbase'
import { bash } from 'svelte-highlight/languages' import { bash } from 'svelte-highlight/languages'
import AccordionItem from '../../../../components/AccordionItem.svelte'
import { instance } from './store'
const { user } = client() const { user } = client()
$: ({ subdomain } = $instance)
const { email } = user() || {} const { email } = user() || {}
// This will hide the component if the email was not found
if (!email) { if (!email) {
throw new Error(`Email expected here`) throw new Error(`Email expected here`)
} }
@ -17,41 +18,49 @@
)}@ftp.sfo-1.${PUBLIC_APP_DOMAIN}` )}@ftp.sfo-1.${PUBLIC_APP_DOMAIN}`
</script> </script>
<AccordionItem title="FTP Access"> <Card>
<p> <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 Securely access your instance files via FTPS. Use your PocketHost account
login and password. login and password.
</p> </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"> <p>Bash:</p>
table {
margin: 10px; <div class="mb-12">
td, <CodeSample code={`ftp ${ftpUrl}`} language={bash} />
tr, </div>
th {
border: 2px solid rgb(92, 92, 157); <table class="table">
padding: 5px; <thead>
} <tr>
} <th class="border-b-2 border-neutral">Directory</th>
</style> <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 { values } from '@s-libs/micro-dash'
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import AccordionItem from '../../../../components/AccordionItem.svelte'
import { instance } from './store' 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`) const { dbg, trace } = logger().create(`Logging.svelte`)
$: ({ id } = $instance) $: ({ 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 }>({}) const logs = writable<{ [_: RecordId]: InstanceLogFields }>({})
let logsArray: InstanceLogFields[] = [] let logsArray: InstanceLogFields[] = []
const cm = createCleanupManager() const cm = createCleanupManager()
onMount(async () => { onMount(async () => {
dbg(`Watching instance log`) dbg(`Watching instance log`)
const unsub = client().watchInstanceLog(id, (newLog) => { const unsub = client().watchInstanceLog(id, (newLog) => {
trace(`Got new log`, newLog) trace(`Got new log`, newLog)
logs.update((currentLogs) => { logs.update((currentLogs) => {
return { ...currentLogs, [newLog.id]: newLog } return { ...currentLogs, [newLog.id]: newLog }
}) })
logsArray = values($logs) logsArray = values($logs)
.sort((a, b) => (a.created > b.created ? 1 : -1)) .sort((a, b) => (a.created > b.created ? 1 : -1))
.slice(0, 1000) .slice(0, 1000)
.reverse() .reverse()
}) })
cm.add(unsub) cm.add(unsub)
}) })
onDestroy(cm.shutdown) onDestroy(cm.shutdown)
</script> </script>
<AccordionItem title="Instance Logging"> <Card>
<p> <CardHeader>Instance Logging</CardHeader>
<p class="mb-4">
Instance logs appear here in realtime, including <code>console.log</code> from Instance logs appear here in realtime, including <code>console.log</code> from
JavaScript hooks. JavaScript hooks.
</p> </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="mockup-code">
<div class={`message ${log.stream}`}> <div class="h-[450px] flex flex-col-reverse overflow-y-scroll">
{(() => { {#each logsArray as log}
try { <div class="px-4" data-prefix=">">
const parsed = JSON.parse(log.message) <span class="text-xs mr-2"
return `<pre><code>${parsed}</code></pre>` ><i class="fa-regular fa-angle-right"></i></span
} catch (e) { >
return log.message <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>
</div> {/each}
{/each} </div>
</div></AccordionItem </div>
> </Card>
<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>

View File

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

View File

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

View File

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

View File

@ -8,30 +8,46 @@
} from '@pockethost/common' } from '@pockethost/common'
import { forEach, reduce } from '@s-libs/micro-dash' import { forEach, reduce } from '@s-libs/micro-dash'
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
import AccordionItem from '../../../../../components/AccordionItem.svelte'
import { instance } from '../store' import { instance } from '../store'
import Form from './Form.svelte' import Form from './Form.svelte'
import List from './List.svelte' import List from './List.svelte'
import { items } from './stores' 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) $: ({ 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 { dbg } = logger().create(`Secrets.svelte`)
const cm = createCleanupManager() const cm = createCleanupManager()
onMount(() => { onMount(() => {
items.clear() items.clear()
forEach(secrets || {}, (value, name) => { forEach(secrets || {}, (value, name) => {
items.upsert({ name, value }) items.upsert({ name, value })
}) })
let initial = false let initial = false
const unsub = items.subscribe(async (secrets) => { const unsub = items.subscribe(async (secrets) => {
if (!initial) { if (!initial) {
initial = true initial = true
return return
} }
dbg(`Got change`, secrets) dbg(`Got change`, secrets)
await client().saveSecrets({ await client().saveSecrets({
instanceId: id, instanceId: id,
secrets: reduce( secrets: reduce(
@ -45,56 +61,62 @@
), ),
}) })
}) })
cm.add(unsub) cm.add(unsub)
}) })
onDestroy(cm.shutdown) onDestroy(cm.shutdown)
</script> </script>
<AccordionItem title="Secrets"> <Card>
<p> <CardHeader>Secrets</CardHeader>
<p class="mb-4">
These secrets are passed into your <code>pocketbase</code> executable and These secrets are passed into your <code>pocketbase</code> executable and
can be accessed from <code>pb_hooks</code> JS hooks. can be accessed from <code>pb_hooks</code> JS hooks.
</p> </p>
<CodeSample
code={$items
.map((secret) => `const ${secret.name} = process.env.${secret.name}`)
.join('\n')}
/>
<SvgIcons /> <!-- If the user has any secrets, render them in a code block -->
<Form /> {#if $items.length > 0}
<List /> <div class="mb-8">
</AccordionItem> <CodeSample code={`const YOUR_KEY = process.env.YOUR_KEY`} />
</div>
{/if}
<style lang="scss"> {#if $items.length === 0}
.secrets { <div class="alert border-2 border-neutral mb-8">
box-sizing: border-box; <i class="fa-regular fa-shield-keyhole"></i>
padding: 0; <span>No Environment Variables Found</span>
margin: 0; </div>
} {/if}
.secrets {
h2 { <div class="tabs mb-4 border-b-[1px] border-neutral">
position: relative; <button
padding: 0.25rem; on:click={() => handleTabChange(0)}
span { type="button"
position: absolute; class="tab border-b-2 {activeTab === 0
top: 0%; ? 'tab-active font-bold border-base-content'
right: 100%; : 'border-neutral'}"
transform: translateY(-50%); ><i class="fa-regular fa-plus mr-2"></i> Add New</button
display: block; >
width: 1.25em;
height: 1.25em; <button
border-radius: 0.75rem; on:click={() => handleTabChange(1)}
background: hsla(240, 25%, 50%, 0.3); type="button"
} class="tab border-b-2 {activeTab === 1
span, ? 'tab-active font-bold border-base-content'
span svg { : 'border-neutral'}"
display: block; ><i class="fa-regular fa-list mr-2"></i> Current List</button
width: 100%; >
height: 100%; </div>
filter: drop-shadow(0 0 3px hsla(240, 25%, 0%, 0.5));
} <div>
} {#if activeTab === 0}
} <Form />
</style> {/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 // create: add an object for the item at the end of the store's array
upsert: (item: SecretItem) => { upsert: (item: SecretItem) => {
dbg(`Upserting`, item) dbg(`Upserting`, item)
const { name, value } = sanitize(item) const { name, value } = sanitize(item)
return update((n) => { return update((n) => {
return formatInput([ return formatInput([
...n.filter((i) => i.name !== name), ...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"> <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 { PUBLIC_APP_DOMAIN } from '$src/env'
import { handleCreateNewInstance } from '$util/database' import { handleCreateNewInstance } from '$util/database'
import { generateSlug } from 'random-word-slugs' import { generateSlug } from 'random-word-slugs'
import { slide } from 'svelte/transition'
let instanceName: string = generateSlug(2) let instanceName: string = generateSlug(2)
let formError: string = '' let formError: string = ''
@ -36,77 +38,49 @@
<title>New Instance - PocketHost</title> <title>New Instance - PocketHost</title>
</svelte:head> </svelte:head>
<div class="container"> <h2 class="text-4xl text-base-content font-bold capitalize mb-6">
<div class="py-4"> Create A New App
<h1 class="text-center">Choose a name for your PocketBase app.</h1> </h2>
</div>
<div class="row g-3 align-items-center justify-content-center mb-4"> <div class="grid lg:grid-cols-2 grid-cols-1">
<div class="col-auto"> <Card>
<label for="instance-name" class="col-form-label">Instance Name:</label> <form on:submit={handleSubmit}>
</div> <CardHeader>Choose a name for your PocketBase app.</CardHeader>
<div class="col-auto pe-1 position-relative"> <div class="flex rename-instance-form-container-query gap-4">
<input <input
type="text" type="text"
id="instance-name" bind:value={instanceName}
class="form-control" class="input input-bordered w-full"
bind:value={instanceName} />
/>
<button <button
aria-label="Regenerate Instance Name" type="button"
type="button" class="btn btn-outline btn-secondary"
style="transform: rotate({rotationCounter}deg);" aria-label="Regenerate Instance Name"
class="btn btn-light rounded-circle regenerate-instance-name-btn" on:click={handleInstanceNameRegeneration}
on:click={handleInstanceNameRegeneration} ><i class="fa-regular fa-arrows-rotate"></i></button
> >
<i class="bi bi-arrow-repeat" /> </div>
</button>
</div>
<div class="col-auto ps-0"> <h4 class="text-center font-bold py-12">
<span class="form-text">.{PUBLIC_APP_DOMAIN}</span> https://{instanceName}.{PUBLIC_APP_DOMAIN}
</div> </h4>
</div>
{#if formError} {#if formError}
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} /> <div transition:slide class="alert alert-error mb-5">
{/if} <i class="fa-solid fa-circle-exclamation"></i>
<span>{formError}</span>
</div>
{/if}
<div class="text-center"> <div class="flex items-center justify-center gap-4">
<a href="/dashboard" class="btn btn-light" disabled={isFormButtonDisabled} <a href="/dashboard" class="btn">Cancel</a>
>Cancel</a
>
<button <button class="btn btn-primary" disabled={isFormButtonDisabled}>
class="btn btn-primary" Create <i class="bi bi-arrow-right-short" />
disabled={isFormButtonDisabled} </button>
on:click={handleSubmit} </div>
> </form>
Create <i class="bi bi-arrow-right-short" /> </Card>
</button>
</div>
</div> </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"> <script lang="ts">
import { browser } from '$app/environment'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte' import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import ProvisioningStatus from '$components/ProvisioningStatus.svelte' import { globalInstancesStore } from '$util/stores'
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 { values } from '@s-libs/micro-dash' import { values } from '@s-libs/micro-dash'
import { onDestroy, onMount } from 'svelte' import InstanceList from './InstanceList.svelte'
import { writable } from 'svelte/store'
import { fade } from 'svelte/transition'
const { error } = logger() $: isFirstApplication = values($globalInstancesStore).length === 0
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)
}
})
</script> </script>
<svelte:head> <svelte:head>
@ -54,112 +12,17 @@
</svelte:head> </svelte:head>
<AuthStateGuard> <AuthStateGuard>
<div class="container" in:fade={{ duration: 30 }}> {#if !isFirstApplication}
{#if !isFirstApplication} <div class="flex items-center justify-between mb-6">
<div class="py-4"> <h2 class="text-4xl text-base-content font-bold capitalize">Dashboard</h2>
<h1 class="text-center">Your Apps</h1>
</div>
<div class="row justify-content-center"> <a href="/app/new" class="m-3 btn btn-primary"
{#each values($instancesStore) as app} ><i class="fa-solid fa-plus"></i> New App</a
<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>
</div> </div>
</div>
<!--<UsageChartForAllInstances />-->
<InstanceList />
{/if}
</AuthStateGuard> </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"> <script lang="ts">
import { handleLogin } from '$util/database'
import AlertBar from '$components/AlertBar.svelte' import AlertBar from '$components/AlertBar.svelte'
import { handleLogin } from '$util/database'
let email: string = '' let email: string = ''
let password: string = '' let password: string = ''
@ -26,90 +26,59 @@
<title>Sign In - PocketHost</title> <title>Sign In - PocketHost</title>
</svelte:head> </svelte:head>
<div class="page-bg"> <div class="flex justify-center">
<div class="card"> <div class="card w-96 bg-base-100 shadow-xl">
<h2 class="mb-4">Login</h2> <div class="card-body">
<h2 class="card-title mb-4">Login</h2>
<form on:submit={handleSubmit}> <form on:submit={handleSubmit}>
<div class="form-floating mb-3"> <div class="form-control w-full max-w-xs">
<input <label class="label" for="email">Email address</label>
type="email" <input
class="form-control" type="email"
id="email" class="input input-bordered w-full max-w-xs"
placeholder="name@example.com" id="email"
bind:value={email} placeholder="name@example.com"
required bind:value={email}
autocomplete="email" required
/> autocomplete="email"
<label for="email">Email address</label> />
</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>
<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> </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"> <script lang="ts">
import { handleUnauthenticatedPasswordReset } from '$util/database'
import AlertBar from '$components/AlertBar.svelte' import AlertBar from '$components/AlertBar.svelte'
import { handleUnauthenticatedPasswordReset } from '$util/database'
let email: string = '' let email: string = ''
let formError: string = '' let formError: string = ''
@ -28,81 +28,52 @@
<title>Password Reset - PocketHost</title> <title>Password Reset - PocketHost</title>
</svelte:head> </svelte:head>
<div class="page-bg"> <div class="flex justify-center">
<div class="card"> <div class="card w-96 bg-base-100 shadow-xl">
{#if userShouldCheckTheirEmail} <div class="card-body">
<div class="text-center"> {#if userShouldCheckTheirEmail}
<h2 class="mb-4">Check Your Email</h2> <div class="text-center">
<p> <h2 class="mb-4">Check Your Email</h2>
A verification link has been sent to <br /><strong>{email}</strong> <p>
</p> A verification link has been sent to <br /><strong>{email}</strong>
</p>
<div class="display-1"> <div class="display-1">
<i class="bi bi-envelope-check" /> <i class="bi bi-envelope-check" />
</div>
</div> </div>
</div> {:else}
{:else} <h2 class="card-title mb-4">Password Reset</h2>
<h2 class="mb-4">Password Reset</h2>
<form on:submit={handleSubmit}> <form on:submit={handleSubmit}>
<div class="form-floating mb-3"> <div class="form-control w-full max-w-xs">
<input <label class="label" for="email">Email address</label>
type="email" <input
class="form-control" type="email"
id="email" class="input input-bordered w-full max-w-xs"
placeholder="name@example.com" id="email"
bind:value={email} placeholder="name@example.com"
required bind:value={email}
autocomplete="email" required
/> autocomplete="email"
<label for="email">Email address</label> />
</div> </div>
{#if formError} {#if formError}
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} /> <AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
{/if} {/if}
<button <div class="mt-4 card-actions justify-end">
type="submit" <button
class="btn btn-primary w-100" type="submit"
disabled={isFormButtonDisabled} class="btn btn-primary w-100"
> disabled={isFormButtonDisabled}
Send Verification Email <i class="bi bi-arrow-right-short" /> >
</button> Send Verification Email <i class="bi bi-arrow-right-short" />
</form> </button>
{/if} </div>
</form>
<div class="py-4"><hr /></div> {/if}
<div class="text-center">
Need to <a href="/signup">create an account</a>?
</div> </div>
</div> </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> <title>Sign Up - PocketHost</title>
</svelte:head> </svelte:head>
<div class="page-bg"> <div class="flex justify-center">
<div class="card"> <div class="card w-96 bg-base-100 shadow-xl">
<h2 class="mb-4">Sign Up</h2> <div class="card-body">
<h2 class="card-title mb-4">Sign Up</h2>
<form on:submit={handleSubmit}> <form on:submit={handleSubmit}>
<div class="form-floating mb-3"> <div class="form-control w-full max-w-xs">
<input <label class="label" for="email">Email address</label>
type="email" <input
class="form-control" type="email"
id="email" class="input input-bordered w-full max-w-xs"
placeholder="name@example.com" id="email"
bind:value={email} placeholder="name@example.com"
required bind:value={email}
autocomplete="email" required
/> autocomplete="email"
<label for="email">Email address</label> />
</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>
<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> </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 { browser } from '$app/environment'
import { client } from '$src/pocketbase' import { client } from '$src/pocketbase'
import type { AuthStoreProps } from '$src/pocketbase/PocketbaseClient' 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' import { writable } from 'svelte/store'
export const authStoreState = writable<AuthStoreProps>({ export const authStoreState = writable<AuthStoreProps>({
@ -9,6 +13,7 @@ export const authStoreState = writable<AuthStoreProps>({
model: null, model: null,
token: '', token: '',
}) })
export const isUserLoggedIn = writable(false) export const isUserLoggedIn = writable(false)
export const isUserVerified = writable(false) export const isUserVerified = writable(false)
export const isAuthStateInitialized = 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. * Listen for auth change events. When we get at least one, the auth state is initialized.
*/ */
onAuthChange((authStoreProps) => { onAuthChange((authStoreProps) => {
const { dbg, error, warn } = logger() const { dbg } = logger()
dbg(`onAuthChange in store`, { ...authStoreProps }) dbg(`onAuthChange in store`, { ...authStoreProps })
authStoreState.set(authStoreProps) authStoreState.set(authStoreProps)
isAuthStateInitialized.set(true) isAuthStateInitialized.set(true)
@ -28,9 +33,14 @@ if (browser) {
// Update derived stores when authStore changes // Update derived stores when authStore changes
authStoreState.subscribe((authStoreProps) => { authStoreState.subscribe((authStoreProps) => {
const { dbg, error, warn } = logger() const { dbg } = logger()
dbg(`subscriber change`, authStoreProps) dbg(`subscriber change`, authStoreProps)
isUserLoggedIn.set(authStoreProps.isValid) isUserLoggedIn.set(authStoreProps.isValid)
isUserVerified.set(!!authStoreProps.model?.verified) 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/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@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": "@mapbox/node-pre-gyp@^1.0.0":
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" 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: dependencies:
is-regex "^1.0.3" 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: chokidar-cli@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/chokidar-cli/-/chokidar-cli-3.0.0.tgz#29283666063b9e167559d30f247ff8fc48794eb7" 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" postcss-js "^4"
tailwindcss "^3" 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: data-uri-to-buffer@^4.0.0:
version "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" 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" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 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: svelte-check@^3.5.2:
version "3.5.2" version "3.5.2"
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-3.5.2.tgz#d6e650996afbe80f5e5b9b02d3fb9489f7d6fb8a" resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-3.5.2.tgz#d6e650996afbe80f5e5b9b02d3fb9489f7d6fb8a"