mirror of
https://github.com/pockethost/pockethost.git
synced 2025-06-09 23:56:40 +00:00
enh: daisy dashboard
This commit is contained in:
parent
ffe15b07a5
commit
8d296c356c
@ -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
|
@ -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!
|
||||||
|
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
3
packages/dashboard/postcss.config.cjs
Normal file
3
packages/dashboard/postcss.config.cjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: [require('tailwindcss'), require('autoprefixer')],
|
||||||
|
}
|
3
packages/dashboard/src/app.css
Normal file
3
packages/dashboard/src/app.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
2
packages/dashboard/src/app.d.ts
vendored
2
packages/dashboard/src/app.d.ts
vendored
@ -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
|
||||||
|
@ -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
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
17
packages/dashboard/src/components/Logo.svelte
Normal file
17
packages/dashboard/src/components/Logo.svelte
Normal 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>
|
31
packages/dashboard/src/components/MobileNavDrawer.svelte
Normal file
31
packages/dashboard/src/components/MobileNavDrawer.svelte
Normal 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>
|
@ -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>
|
|
||||||
|
@ -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>
|
|
@ -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>
|
||||||
|
15
packages/dashboard/src/components/cards/Card.svelte
Normal file
15
packages/dashboard/src/components/cards/Card.svelte
Normal 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>
|
21
packages/dashboard/src/components/cards/CardHeader.svelte
Normal file
21
packages/dashboard/src/components/cards/CardHeader.svelte
Normal 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}
|
@ -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}
|
||||||
|
@ -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>
|
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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}
|
||||||
|
@ -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>
|
|
||||||
|
@ -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>
|
||||||
|
@ -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: <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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
|
@ -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>
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
|
@ -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>
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 |
@ -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),
|
||||||
|
@ -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>
|
@ -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>
|
|
@ -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>
|
|
||||||
|
@ -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>
|
|
||||||
|
72
packages/dashboard/src/routes/dashboard/InstanceList.svelte
Normal file
72
packages/dashboard/src/routes/dashboard/InstanceList.svelte
Normal 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: <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>
|
@ -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>
|
@ -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>
|
|
||||||
|
@ -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>
|
|
||||||
|
@ -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>
|
|
||||||
|
64
packages/dashboard/src/util/getInstances.ts
Normal file
64
packages/dashboard/src/util/getInstances.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}>({})
|
||||||
|
@ -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;
|
|
||||||
}
|
|
12
packages/dashboard/static/icons/all.min.css
vendored
Normal file
12
packages/dashboard/static/icons/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
packages/dashboard/static/icons/brands.min.css
vendored
Normal file
6
packages/dashboard/static/icons/brands.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
packages/dashboard/static/icons/fontawesome.min.css
vendored
Normal file
9
packages/dashboard/static/icons/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
packages/dashboard/static/images/pockethost-cloud-logo.jpg
Normal file
BIN
packages/dashboard/static/images/pockethost-cloud-logo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
packages/dashboard/static/webfonts/fa-brands-400.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-brands-400.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-duotone-900.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-duotone-900.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-duotone-900.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-duotone-900.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-light-300.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-light-300.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-light-300.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-light-300.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-regular-400.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-regular-400.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-sharp-light-300.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-sharp-light-300.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-sharp-light-300.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-sharp-light-300.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-sharp-regular-400.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-sharp-regular-400.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-sharp-regular-400.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-sharp-regular-400.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-sharp-solid-900.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-sharp-solid-900.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-sharp-solid-900.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-sharp-solid-900.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-solid-900.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-solid-900.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-thin-100.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-thin-100.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-thin-100.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-thin-100.woff2
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-v4compatibility.ttf
Normal file
BIN
packages/dashboard/static/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
packages/dashboard/static/webfonts/fa-v4compatibility.woff2
Normal file
BIN
packages/dashboard/static/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
32
packages/dashboard/tailwind.config.cjs
Normal file
32
packages/dashboard/tailwind.config.cjs
Normal 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')],
|
||||||
|
}
|
28
yarn.lock
28
yarn.lock
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user