mirror of
https://github.com/pockethost/pockethost.git
synced 2025-06-06 06:06:45 +00:00
enh: daisy dashboard
This commit is contained in:
parent
ffe15b07a5
commit
8d296c356c
@ -15,3 +15,6 @@ yarn.lock
|
||||
# Source files
|
||||
/src/assets/_bootstrap.css
|
||||
dist-server
|
||||
|
||||
# Ignore the static files
|
||||
static
|
@ -6,7 +6,7 @@ Description about PocketHost goes here!
|
||||
|
||||
## Developing Locally
|
||||
|
||||
To run this project, navigate to the `/packages/pockethost.io` folder and run `vite dev`.
|
||||
To run this project, navigate to the `/packages/dashboard` folder and run `yarn dev`.
|
||||
|
||||
It will start up the server here: [http://127.0.0.1:5173/](http://127.0.0.1:5173/) and now you're ready to code!
|
||||
|
||||
|
@ -16,10 +16,12 @@
|
||||
"@pockethost/common": "*",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/kit": "^1.25.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"boolean": "^3.2.0",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-scale-chromatic": "^3.0.0",
|
||||
"daisyui": "^3.8.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
@ -31,6 +33,9 @@
|
||||
"svelte-check": "^3.5.2",
|
||||
"svelte-highlight": "^7.3.0",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"svelte-chartjs": "3.1.2",
|
||||
"chart.js": "4.4.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"url-pattern": "^1.0.3",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
|
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 {} = {}> {
|
||||
attributes: TAttributes
|
||||
|
@ -1,9 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>PocketHost</title>
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
@ -11,36 +12,15 @@
|
||||
id="hljs-link"
|
||||
/>
|
||||
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="/global.css" />
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css"
|
||||
/>
|
||||
<link href="/icons/fontawesome.min.css" rel="stylesheet">
|
||||
<link href="/icons/all.min.css" rel="stylesheet">
|
||||
<link href="/icons/brands.min.css" rel="stylesheet">
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body>
|
||||
<div>%sveltekit.body%</div>
|
||||
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
File diff suppressed because one or more lines are too long
@ -9,27 +9,12 @@
|
||||
const bodyId = `body${uid}`
|
||||
</script>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id={headerId}>
|
||||
<button
|
||||
class="accordion-button {show ? '' : 'collapsed'} text-bg-{header} "
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{bodyId}"
|
||||
aria-expanded="true"
|
||||
aria-controls={bodyId}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
</h2>
|
||||
<div
|
||||
id={bodyId}
|
||||
class="accordion-collapse collapse {show ? 'show' : ''} "
|
||||
aria-labelledby={bodyId}
|
||||
data-bs-parent="#accordionExample"
|
||||
>
|
||||
<div class="accordion-body">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="collapse collapse-arrow bg-base-200">
|
||||
<input type="radio" name="my-accordion-1" checked={show} />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
{title}
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,12 +26,13 @@
|
||||
.copy-container {
|
||||
position: relative;
|
||||
margin: 5px;
|
||||
border: 1px solid gray;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -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">
|
||||
import { page } from '$app/stores'
|
||||
import Logo from '$components/Logo.svelte'
|
||||
import MediaQuery from '$components/MediaQuery.svelte'
|
||||
import ThemeToggle from '$components/ThemeToggle.svelte'
|
||||
import { PUBLIC_POCKETHOST_VERSION } from '$src/env'
|
||||
import { handleLogoutAndRedirect } from '$util/database'
|
||||
import { isUserLoggedIn } from '$util/stores'
|
||||
import AuthStateGuard from './helpers/AuthStateGuard.svelte'
|
||||
import { getInstances } from '$util/getInstances'
|
||||
import { globalInstancesStore } from '$util/stores'
|
||||
import { values } from '@s-libs/micro-dash'
|
||||
|
||||
// This will query the database for all instances and then update the global state
|
||||
getInstances()
|
||||
|
||||
const linkClasses =
|
||||
'font-medium text-xl text-base-content btn btn-ghost capitalize justify-start'
|
||||
const subLinkClasses =
|
||||
'font-medium text-base-content btn btn-ghost btn-sm capitalize justify-start'
|
||||
const addNewAppClasses =
|
||||
'font-medium text-base-content btn btn-outline btn-primary btn-sm capitalize justify-start'
|
||||
|
||||
const handleClick = () => {
|
||||
document.querySelector('.drawer-overlay')?.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="container-fluid">
|
||||
<nav class="navbar navbar-expand-md">
|
||||
<a href="/" class="logo text-decoration-none d-flex align-items-center">
|
||||
<img
|
||||
src="/images/logo-square.png"
|
||||
alt="PocketHost Logo"
|
||||
class="img-fluid"
|
||||
/>
|
||||
<h1>Pocket<span>Host</span></h1>
|
||||
<sup class="">{PUBLIC_POCKETHOST_VERSION}</sup>
|
||||
</a>
|
||||
<aside class="p-4 min-w-[250px] flex flex-col h-screen">
|
||||
<MediaQuery query="(min-width: 1280px)" let:matches>
|
||||
{#if matches}
|
||||
<a href="/dashboard" class="flex gap-2 items-center justify-center">
|
||||
<Logo hideLogoText={true} logoWidth="w-20" />
|
||||
</a>
|
||||
{/if}
|
||||
</MediaQuery>
|
||||
|
||||
<button
|
||||
class="btn btn-light mobile-nav-button navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#nav-links"
|
||||
aria-controls="nav-links"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
<div class="flex flex-col gap-2 mb-auto">
|
||||
<a on:click={handleClick} href="/dashboard" class={linkClasses}
|
||||
><i
|
||||
class="fa-regular fa-table-columns {$page.url.pathname ===
|
||||
'/dashboard' && 'text-primary'}"
|
||||
></i> Dashboard</a
|
||||
>
|
||||
<i class="bi bi-list" />
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="nav-links">
|
||||
<ul class="navbar-nav ms-auto mb-2 mb-md-0">
|
||||
<AuthStateGuard>
|
||||
{#if $isUserLoggedIn}
|
||||
<li class="nav-item text-md-start text-center">
|
||||
<a class="nav-link" href="/dashboard">Dashboard</a>
|
||||
</li>
|
||||
<div class="pl-8 flex flex-col gap-4 mb-4">
|
||||
{#each values($globalInstancesStore) as app}
|
||||
<a
|
||||
href={`/app/instances/${app.id}`}
|
||||
on:click={handleClick}
|
||||
class={subLinkClasses}
|
||||
>
|
||||
<i
|
||||
class="fa-regular fa-server {$page.url.pathname ===
|
||||
`/app/instances/${app.id}` && 'text-primary'}"
|
||||
></i>
|
||||
{app.subdomain}
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
<MediaQuery query="(min-width: 768px)" let:matches>
|
||||
{#if matches}
|
||||
<li class="nav-item dropdown">
|
||||
<button
|
||||
class="btn border-0 nav-link dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-label="Click to expand the Account Dropdown"
|
||||
title="Account Dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Account
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
type="button"
|
||||
on:click={handleLogoutAndRedirect}>Logout</button
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link text-md-start text-center"
|
||||
href="/"
|
||||
on:click={handleLogoutAndRedirect}>Logout</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
</MediaQuery>
|
||||
{/if}
|
||||
|
||||
{#if !$isUserLoggedIn}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-md-start text-center" href="/signup"
|
||||
>Sign up</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-md-start text-center" href="/login"
|
||||
>Log in</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
</AuthStateGuard>
|
||||
|
||||
<li class="nav-item text-center">
|
||||
<a
|
||||
href="https://pockethost.io/docs/overview/faq"
|
||||
class="nav-link btn btn-outline-dark rounded-1 d-inline-block px-3"
|
||||
rel="noreferrer">FAQ</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="nav-item text-center">
|
||||
<a
|
||||
href="https://github.com/benallfree/pockethost/discussions"
|
||||
class="nav-link btn btn-outline-dark rounded-1 d-inline-block px-3"
|
||||
target="_blank"
|
||||
rel="noreferrer">Support</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="nav-item text-center">
|
||||
<a
|
||||
href="https://pockethost.io/docs/"
|
||||
class="nav-link btn btn-outline-dark rounded-1 d-inline-block px-3"
|
||||
rel="noreferrer">Docs</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link text-md-start text-center"
|
||||
href="https://github.com/benallfree/pockethost"
|
||||
target="_blank"
|
||||
aria-label="Link to our Github Project"
|
||||
title="Link to our Github Project"
|
||||
rel="noopener"
|
||||
>
|
||||
<i class="bi bi-github" /><span class="nav-github-link">Github</span
|
||||
>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item text-center">
|
||||
<ThemeToggle navLink={true} />
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/app/new" on:click={handleClick} class={addNewAppClasses}>
|
||||
<i
|
||||
class="fa-regular fa-plus {$page.url.pathname === `/app/new` &&
|
||||
'text-primary'}"
|
||||
></i> Create A New App
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<style lang="scss">
|
||||
header {
|
||||
background-color: var(--bs-body-bg);
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--bs-gray-300);
|
||||
}
|
||||
<a
|
||||
href="https://github.com/benallfree/pockethost/discussions"
|
||||
class={linkClasses}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
><i class="fa-regular fa-comment-code"></i> Discussion
|
||||
<i
|
||||
class="fa-regular fa-arrow-up-right-from-square ml-auto opacity-50 text-sm"
|
||||
></i></a
|
||||
>
|
||||
|
||||
.logo {
|
||||
img {
|
||||
max-width: 50px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
<a
|
||||
href="https://pockethost.io/docs"
|
||||
class={linkClasses}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i class="fa-regular fa-webhook"></i> Docs
|
||||
<i
|
||||
class="fa-regular fa-arrow-up-right-from-square ml-auto opacity-50 text-sm"
|
||||
></i></a
|
||||
>
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
color: var(--bs-body-color);
|
||||
<a
|
||||
href="https://github.com/pockethost/pockethost"
|
||||
class={linkClasses}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i class="fa-brands fa-github"></i> GitHub
|
||||
<i
|
||||
class="fa-regular fa-arrow-up-right-from-square ml-auto opacity-50 text-sm"
|
||||
></i></a
|
||||
>
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
background-image: linear-gradient(
|
||||
83.2deg,
|
||||
rgba(150, 93, 233, 1) 10.8%,
|
||||
rgba(99, 88, 238, 1) 94.3%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
<button type="button" class={linkClasses} on:click={handleLogoutAndRedirect}
|
||||
><i class="fa-regular fa-arrow-up-left-from-circle"></i> Logout</button
|
||||
>
|
||||
</div>
|
||||
|
||||
sup {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--bs-gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-nav-button {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-weight: 500;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.nav-github-link {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.nav-github-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<ThemeToggle />
|
||||
</aside>
|
||||
|
@ -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">
|
||||
import { browser } from '$app/environment'
|
||||
import { onMount } from 'svelte'
|
||||
import {
|
||||
THEME_ICONS,
|
||||
ThemeNames,
|
||||
currentIcon,
|
||||
getCurrentTheme,
|
||||
setCurrentTheme,
|
||||
} from './helpers/theme'
|
||||
import { ThemeNames, getCurrentTheme, setCurrentTheme } from './helpers/theme'
|
||||
|
||||
// This can change the CSS a bit depending on where the theme toggle is rendered
|
||||
export let navLink: boolean = false
|
||||
|
||||
// Set the default icon to be light mode
|
||||
let iconClass: string = browser ? currentIcon() : ''
|
||||
// This will keep track of the toggle's state
|
||||
let isChecked = true
|
||||
|
||||
// Wait for the DOM to be available
|
||||
onMount(() => {
|
||||
// Check what theme cookie is set
|
||||
const currentTheme = getCurrentTheme()
|
||||
|
||||
// Set the toggle's state
|
||||
isChecked = currentTheme === ThemeNames.Dark
|
||||
|
||||
// Update the site's theme to match what the cookie has
|
||||
updateTheme(getCurrentTheme())
|
||||
})
|
||||
|
||||
// Alternate the theme values on toggle click
|
||||
const handleClick = () => {
|
||||
const newTheme =
|
||||
getCurrentTheme() === ThemeNames.Light
|
||||
? ThemeNames.Dark
|
||||
: ThemeNames.Light
|
||||
const handleChange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const isChecked = target.checked
|
||||
|
||||
const newTheme = isChecked ? ThemeNames.Dark : ThemeNames.Light
|
||||
|
||||
updateTheme(newTheme)
|
||||
}
|
||||
|
||||
const updateTheme = (themeName: ThemeNames) => {
|
||||
// Update the icon class name to toggle between light and dark mode
|
||||
iconClass = THEME_ICONS[themeName]
|
||||
|
||||
setCurrentTheme(themeName)
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="{navLink && 'nav-link'} btn border-0 d-inline-block"
|
||||
aria-label="Toggle the site theme"
|
||||
title="Toggle the site theme"
|
||||
on:click={handleClick}
|
||||
>
|
||||
<i class={iconClass} />
|
||||
</button>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Dark Mode</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
bind:checked={isChecked}
|
||||
on:change={handleChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
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">
|
||||
import { isAuthStateInitialized } from '$util/stores'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
onMount(() => {})
|
||||
</script>
|
||||
|
||||
{#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
|
||||
type="button"
|
||||
class="btn btn-{style}"
|
||||
class="btn btn-{style} btn-sm"
|
||||
{disabled}
|
||||
style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
|
||||
on:click={click}><slot /></button
|
||||
|
@ -14,13 +14,9 @@ export const HLJS_THEMES = {
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-dark.min.css',
|
||||
}
|
||||
export const ALLOWED_THEMES: ThemeNames[] = [ThemeNames.Light, ThemeNames.Dark]
|
||||
export const DEFAULT_THEME: ThemeNames = ThemeNames.Light
|
||||
export const DEFAULT_THEME: ThemeNames = ThemeNames.Dark
|
||||
export const STORAGE_NAME: string = 'theme'
|
||||
export const THEME_ATTRIBUTE: string = 'data-bs-theme'
|
||||
export const THEME_ICONS: { [_ in ThemeNames]: string } = {
|
||||
[ThemeNames.Light]: 'bi bi-moon-stars',
|
||||
[ThemeNames.Dark]: 'bi bi-brightness-high',
|
||||
}
|
||||
export const THEME_ATTRIBUTE: string = 'data-theme'
|
||||
|
||||
export const html = () => {
|
||||
const htmlElement = document.querySelector('html')
|
||||
@ -35,16 +31,12 @@ export const getCurrentTheme = () => {
|
||||
return currentTheme
|
||||
}
|
||||
|
||||
export const currentIcon = () => {
|
||||
return THEME_ICONS[getCurrentTheme()]
|
||||
}
|
||||
|
||||
export const setCurrentTheme = (themeName: ThemeNames) => {
|
||||
html().setAttribute(THEME_ATTRIBUTE, themeName)
|
||||
const theme = document.querySelector<HTMLLinkElement>('#hljs-link')
|
||||
if (theme) {
|
||||
theme.href = HLJS_THEMES[themeName]
|
||||
}
|
||||
console.log(theme, themeName)
|
||||
|
||||
Cookies.set(STORAGE_NAME, themeName)
|
||||
}
|
||||
|
@ -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>
|
||||
import InitializeTooltips from '$components/InitializeTooltips.svelte'
|
||||
import Navbar from '$components/Navbar.svelte'
|
||||
import VerifyAccountBar from '$components/VerifyAccountBar.svelte'
|
||||
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
|
||||
import Meta from '$components/helpers/Meta.svelte'
|
||||
import Protect from '$components/helpers/Protect.svelte'
|
||||
import ThemeDetector from '$components/helpers/ThemeDetector.svelte'
|
||||
import MediaQuery from '$components/MediaQuery.svelte'
|
||||
import MobileNavDrawer from '$components/MobileNavDrawer.svelte'
|
||||
import '../app.css'
|
||||
|
||||
import { isUserLoggedIn } from '$util/stores'
|
||||
import Logo from '$components/Logo.svelte'
|
||||
</script>
|
||||
|
||||
<Meta />
|
||||
<Protect />
|
||||
<ThemeDetector />
|
||||
|
||||
<Navbar />
|
||||
{#if $isUserLoggedIn}
|
||||
<AuthStateGuard>
|
||||
<VerifyAccountBar />
|
||||
</AuthStateGuard>
|
||||
|
||||
<AuthStateGuard>
|
||||
<VerifyAccountBar />
|
||||
</AuthStateGuard>
|
||||
<div class="layout xl:flex">
|
||||
<MediaQuery query="(min-width: 1280px)" let:matches>
|
||||
{#if matches}
|
||||
<Navbar />
|
||||
{:else}
|
||||
<MobileNavDrawer>
|
||||
<Navbar />
|
||||
</MobileNavDrawer>
|
||||
{/if}
|
||||
</MediaQuery>
|
||||
|
||||
<main data-sveltekit-prefetch>
|
||||
<slot />
|
||||
</main>
|
||||
<div class="lg:p-4 lg:pt-0 xl:pt-4 min-h-screen grow">
|
||||
<div
|
||||
class="bg-base-300 border-base-300 border-[16px] xl:h-[calc(100vh-32px)] lg:p-4 rounded-2xl xl:overflow-hidden xl:overflow-y-auto"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<InitializeTooltips />
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
{#if !$isUserLoggedIn}
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FeatureCard from '$components/FeatureCard.svelte'
|
||||
import Logo from '$components/Logo.svelte'
|
||||
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
|
||||
import HomepageHeroAnimation from '$components/HomepageHeroAnimation.svelte'
|
||||
import InstanceGeneratorWidget from '$components/InstanceGeneratorWidget.svelte'
|
||||
import { PUBLIC_APP_DOMAIN } from '$src/env'
|
||||
import InstanceGeneratorWidget from '$components/login-register/InstanceGeneratorWidget.svelte'
|
||||
import { isUserLoggedIn } from '$util/stores'
|
||||
</script>
|
||||
|
||||
@ -11,210 +9,22 @@
|
||||
<title>Home - PocketHost</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<div class="row align-items-center justify-content-between hero">
|
||||
<div class="col-lg-6 mb-5 mb-lg-0">
|
||||
<h2>Deploy <span>PocketBase</span> in 30 seconds</h2>
|
||||
<div class="min-h-screen flex items-center justify-center">
|
||||
<div>
|
||||
<AuthStateGuard>
|
||||
<Logo />
|
||||
|
||||
<p class="mb-5">
|
||||
Spend less time on configuring your backend, and more time building new
|
||||
features for your web app.
|
||||
</p>
|
||||
{#if $isUserLoggedIn}
|
||||
<div class="">
|
||||
<a href="/dashboard" class="btn btn-primary"
|
||||
>Go to Your Dashboard <i class="bi bi-arrow-right-short" /></a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<AuthStateGuard>
|
||||
{#if $isUserLoggedIn}
|
||||
<div>
|
||||
<a href="/dashboard" class="btn btn-primary"
|
||||
>Go to Your Dashboard <i class="bi bi-arrow-right-short" /></a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !$isUserLoggedIn}
|
||||
<InstanceGeneratorWidget />
|
||||
{/if}
|
||||
</AuthStateGuard>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5 d-none d-sm-block">
|
||||
<HomepageHeroAnimation />
|
||||
</div>
|
||||
{#if !$isUserLoggedIn}
|
||||
<InstanceGeneratorWidget />
|
||||
{/if}
|
||||
</AuthStateGuard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section features">
|
||||
<div class="container">
|
||||
<h2 class="mb-5">Features</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-lg-4 mb-4">
|
||||
<FeatureCard
|
||||
title="Up in 30 seconds"
|
||||
icon="bi bi-stopwatch"
|
||||
fullHeight={true}
|
||||
>
|
||||
<p>
|
||||
A backend for your next app is as fast as signing up. No
|
||||
provisioning servers, no Docker fiddling, just B(ad)aaS
|
||||
productivity.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>Sign up</li>
|
||||
<li>Pick a unique project name</li>
|
||||
<li>Connect with our JS client</li>
|
||||
</ul>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4 mb-4">
|
||||
<FeatureCard
|
||||
title="Zero Config"
|
||||
icon="bi bi-check-lg"
|
||||
fullHeight={true}
|
||||
>
|
||||
<p>
|
||||
With PocketHost, batteries are included. You get a database,
|
||||
outgoing email, SSL, authentication, cloud functions, and high
|
||||
concurrency all in one stop.
|
||||
</p>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4 mb-4">
|
||||
<FeatureCard title="Database" icon="bi bi-hdd-stack" fullHeight={true}>
|
||||
<p>
|
||||
Your PocketHost instance is powered by its own internal SQLite
|
||||
instance. SQLite is <a
|
||||
href="https://pocketbase.io/faq/"
|
||||
target="_blank">more performant than mySQL or Postgres</a
|
||||
>
|
||||
and is
|
||||
<a href="https://www.sqlite.org/whentouse.html" target="_blank"
|
||||
>perfect for powering your next app</a
|
||||
>.
|
||||
</p>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-3 mb-4">
|
||||
<FeatureCard title="Auth" icon="bi bi-shield-lock" fullHeight={true}>
|
||||
<p>
|
||||
Email and oAuth authentication options work out of the box. Send
|
||||
transactional email to your users from our verified domain and your
|
||||
custom address <code>yoursubdomain@{PUBLIC_APP_DOMAIN}</code>.
|
||||
</p>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-3 mb-4">
|
||||
<FeatureCard title="Storage" icon="bi bi-archive" fullHeight={true}>
|
||||
<p>
|
||||
PocketHost securely stores your files on Amazon S3, or you can use
|
||||
your own key to manage your own storage.
|
||||
</p>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-3 mb-4">
|
||||
<FeatureCard
|
||||
title="Room to Grow"
|
||||
icon="bi bi-cloud-arrow-up"
|
||||
fullHeight={true}
|
||||
>
|
||||
<p>
|
||||
PocketHost is perfect for hobbist, low, and medium volume sites and
|
||||
apps.
|
||||
</p>
|
||||
<p>
|
||||
PocketHost, and the underlying PocketBase, can scale to well over
|
||||
10,000 simultaneous connections.
|
||||
</p>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-3 mb-4">
|
||||
<FeatureCard
|
||||
title="Self-host"
|
||||
icon="bi bi-house-door"
|
||||
fullHeight={true}
|
||||
>
|
||||
<p>
|
||||
When you're ready to take your project in-house, we have you
|
||||
covered. You can export your entire PocketHost environment along
|
||||
with a Dockerfile to run it.
|
||||
</p>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-6 mb-4">
|
||||
<FeatureCard
|
||||
title="Open Source Stack"
|
||||
icon="bi bi-code-slash"
|
||||
fullHeight={true}
|
||||
>
|
||||
<p>
|
||||
PocketHost is powered by Svelte, Vite, Typescript, PocketBase, and
|
||||
SQLite. Because the entire stack is open source, you'll never be
|
||||
locked into the whims of a vendor.
|
||||
</p>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-6 mb-4">
|
||||
<FeatureCard
|
||||
title="Coming Soon"
|
||||
icon="bi bi-card-checklist"
|
||||
fullHeight={true}
|
||||
>
|
||||
<ul>
|
||||
<li>JS/TS cloud functions</li>
|
||||
<li>Deploy to Fly.io</li>
|
||||
<li>Litestream</li>
|
||||
</ul>
|
||||
</FeatureCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
.hero h2 span {
|
||||
background-image: linear-gradient(
|
||||
83.2deg,
|
||||
rgba(150, 93, 233, 1) 10.8%,
|
||||
rgba(99, 88, 238, 1) 94.3%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.features {
|
||||
background-image: var(--gradient-white-lime);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 120px 0;
|
||||
}
|
||||
.section h2 {
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.hero {
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 65px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,51 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
|
||||
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
|
||||
import { client } from '$src/pocketbase'
|
||||
import {
|
||||
assertExists,
|
||||
createCleanupManager,
|
||||
logger,
|
||||
} from '@pockethost/common'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { getSingleInstance } from '$util/getInstances'
|
||||
import { createCleanupManager } from '@pockethost/common'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { instance } from './store'
|
||||
|
||||
const { instanceId } = $page.params
|
||||
|
||||
const cm = createCleanupManager()
|
||||
onMount(async () => {
|
||||
const { dbg, error } = logger().create(`layout.svelte`)
|
||||
const { watchInstanceById } = client()
|
||||
watchInstanceById(instanceId, (r) => {
|
||||
dbg(`Handling instance update`, r)
|
||||
const { action, record } = r
|
||||
assertExists(record, `Expected instance here`)
|
||||
instance.set(record)
|
||||
})
|
||||
.then(cm.add)
|
||||
.catch(error)
|
||||
})
|
||||
|
||||
// Run anytime the page params changes
|
||||
$: {
|
||||
getSingleInstance($page.params.instanceId)
|
||||
}
|
||||
|
||||
onDestroy(() => cm.shutdown())
|
||||
</script>
|
||||
|
||||
<AuthStateGuard>
|
||||
<div class="container">
|
||||
{#if $instance}
|
||||
<h2>
|
||||
{$instance.subdomain}
|
||||
<a
|
||||
href="{PUBLIC_APP_PROTOCOL}://{$instance.subdomain}.{PUBLIC_APP_DOMAIN}/_"
|
||||
target="_blank"
|
||||
rel="noreferrer"><i class="bi bi-box-arrow-up-right" /></a
|
||||
>
|
||||
</h2>
|
||||
<slot />
|
||||
<div class="text-center py-5">
|
||||
<a href="/dashboard" class="btn btn-light"
|
||||
><i class="bi bi-arrow-left-short" /> Back to Dashboard</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $instance}
|
||||
<slot />
|
||||
{/if}
|
||||
</AuthStateGuard>
|
||||
|
@ -1,35 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
|
||||
import { assertExists } from '@pockethost/common'
|
||||
import Code from './Code.svelte'
|
||||
import Danger from './Danger/Danger.svelte'
|
||||
import UsageChart from './UsageChart.svelte'
|
||||
import Ftp from './Ftpx.svelte'
|
||||
import Logging from './Logging.svelte'
|
||||
import Overview from './Overview.svelte'
|
||||
import Secrets from './Secrets/Secrets.svelte'
|
||||
import { instance } from './store'
|
||||
import { PUBLIC_APP_DOMAIN } from '$src/env'
|
||||
import DangerZoneTitle from './Danger/DangerZoneTitle.svelte'
|
||||
import RenameInstance from './Danger/RenameInstance.svelte'
|
||||
import Maintenance from './Danger/Maintenance.svelte'
|
||||
import VersionChange from './Danger/VersionChange.svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
$: ({ status, version, secondsThisMonth } = $instance)
|
||||
|
||||
assertExists($instance, `Expected instance here`)
|
||||
const { subdomain, maintenance } = $instance
|
||||
const url = `${PUBLIC_APP_PROTOCOL}://${subdomain}.${PUBLIC_APP_DOMAIN}`
|
||||
const code = `const url = '${url}'\nconst client = new PocketBase(url)`
|
||||
const { subdomain } = $instance
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{subdomain} details - PocketHost</title>
|
||||
</svelte:head>
|
||||
{#if $instance}
|
||||
{#if $instance.maintenance}
|
||||
<div class="text-warning">
|
||||
This instance is in Maintenance Mode and will not respond to requests.
|
||||
|
||||
<div class="flex md:flex-row flex-col items-center justify-between mb-6 gap-4">
|
||||
<div>
|
||||
<h2
|
||||
class="text-4xl md:text-left text-center text-base-content font-bold capitalize mb-3 break-words"
|
||||
>
|
||||
{$instance.subdomain}
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-wrap md:justify-start justify-center gap-2">
|
||||
<div class="badge badge-accent badge-outline">
|
||||
Status: <span class="capitalize">{status}</span>
|
||||
</div>
|
||||
<div class="badge badge-accent badge-outline">
|
||||
Usage: {Math.ceil(secondsThisMonth / 60)} mins
|
||||
</div>
|
||||
<div class="badge badge-accent badge-outline">Version: {version}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="accordion" id="accordionExample">
|
||||
<Overview />
|
||||
<Ftp />
|
||||
<Code />
|
||||
<Secrets />
|
||||
<Logging />
|
||||
<Danger />
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://{$instance.subdomain}.{PUBLIC_APP_DOMAIN}/_"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
<img src="/images/pocketbase-logo.svg" alt="PocketBase Logo" class="w-6" />
|
||||
Admin
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if $instance.maintenance}
|
||||
<div transition:slide class="alert alert-warning mb-6">
|
||||
<i class="fa-regular fa-triangle-person-digging"></i>
|
||||
<span
|
||||
>This instance is in Maintenance Mode and will not respond to requests</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid lg:grid-cols-2 grid-cols-1 gap-4 mb-4">
|
||||
<UsageChart />
|
||||
|
||||
<Code />
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-3 grid-cols-1 gap-4 mb-16">
|
||||
<Ftp />
|
||||
|
||||
<Logging />
|
||||
|
||||
<Secrets />
|
||||
</div>
|
||||
|
||||
<DangerZoneTitle />
|
||||
|
||||
<div class="grid lg:grid-cols-3 gap-4 mb-4">
|
||||
<RenameInstance />
|
||||
|
||||
<Maintenance />
|
||||
|
||||
<VersionChange />
|
||||
</div>
|
||||
|
@ -1,17 +1,56 @@
|
||||
<script lang="ts">
|
||||
import AccordionItem from '$components/AccordionItem.svelte'
|
||||
import CodeSample from '$components/CodeSample.svelte'
|
||||
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
|
||||
import { instance } from './store'
|
||||
import Card from '$components/cards/Card.svelte'
|
||||
import CardHeader from '$components/cards/CardHeader.svelte'
|
||||
|
||||
let code = ''
|
||||
let installSnippet = `npm i pocketbase`
|
||||
|
||||
let connectionSnippet = ''
|
||||
$: {
|
||||
const url = `${PUBLIC_APP_PROTOCOL}://${$instance.subdomain}.${PUBLIC_APP_DOMAIN}`
|
||||
code = `const url = '${url}'\nconst client = new PocketBase(url)`
|
||||
connectionSnippet = `import PocketBase from 'pocketbase';\n\nconst url = '${url}'\nconst client = new PocketBase(url)`
|
||||
}
|
||||
|
||||
let firstQuerySnippet = `const records = await client.collection('posts').getFullList({
|
||||
sort: '-created',
|
||||
});`
|
||||
</script>
|
||||
|
||||
<AccordionItem title="Code Samples">
|
||||
JavaScript:
|
||||
<CodeSample {code} />
|
||||
</AccordionItem>
|
||||
<Card>
|
||||
<CardHeader>Getting Started</CardHeader>
|
||||
|
||||
<div class="mb-4">
|
||||
<p>Installing PocketBase</p>
|
||||
<CodeSample code={installSnippet} />
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p>Connecting to Your Instance</p>
|
||||
<CodeSample code={connectionSnippet} />
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<p>Making Your First Query</p>
|
||||
<CodeSample code={firstQuerySnippet} />
|
||||
</div>
|
||||
|
||||
<p>Additional Resources:</p>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
<a
|
||||
href="https://pocketbase.io/docs/api-records/"
|
||||
target="_blank"
|
||||
class="link">PocketBase Web APIs</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.npmjs.com/package/pocketbase"
|
||||
target="_blank"
|
||||
class="link">PocketBase NPM Package</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
|
@ -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">
|
||||
import MiniToggle from '$components/MiniToggle.svelte'
|
||||
import Card from '$components/cards/Card.svelte'
|
||||
import CardHeader from '$components/cards/CardHeader.svelte'
|
||||
import { client } from '$src/pocketbase'
|
||||
import { instance } from '../store'
|
||||
|
||||
@ -7,19 +8,36 @@
|
||||
|
||||
$: ({ id, maintenance } = $instance)
|
||||
|
||||
const onMaintenance = (maintenance: boolean) =>
|
||||
setInstanceMaintenance({ instanceId: id, maintenance }).then(() => 'saved')
|
||||
const handleChange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const isChecked = target.checked
|
||||
|
||||
// Update the database with the new value
|
||||
setInstanceMaintenance({ instanceId: id, maintenance: isChecked }).then(
|
||||
() => 'saved',
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h3>Maintenance Mode</h3>
|
||||
<p class="text-danger">
|
||||
Your PocketHost instance will not be accessible while in maintenance mode.
|
||||
Use this when you are upgrading, downgrading, or backing up your data. See <a
|
||||
href="https://pockethost.io/docs/usage/maintenance">Maintenance Mode</a
|
||||
> for more information.
|
||||
</p>
|
||||
<MiniToggle value={maintenance} save={onMaintenance}
|
||||
>Maintenance Mode</MiniToggle
|
||||
<Card>
|
||||
<CardHeader
|
||||
documentation="https://pockethost.gitbook.io/manual/daily-usage/maintenance"
|
||||
>
|
||||
</div>
|
||||
Maintenance Mode
|
||||
</CardHeader>
|
||||
|
||||
<p class="mb-8">
|
||||
Your PocketHost instance will not be accessible while in maintenance mode.
|
||||
Use this when you are upgrading, downgrading, or backing up your data.
|
||||
</p>
|
||||
|
||||
<label class="label cursor-pointer justify-center gap-4">
|
||||
<span class="label-text">Maintenance Mode</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-warning"
|
||||
checked={!!maintenance}
|
||||
on:change={handleChange}
|
||||
/>
|
||||
</label>
|
||||
</Card>
|
||||
|
@ -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">
|
||||
import CodeSample from '$components/CodeSample.svelte'
|
||||
import Card from '$components/cards/Card.svelte'
|
||||
import CardHeader from '$components/cards/CardHeader.svelte'
|
||||
import { PUBLIC_APP_DOMAIN } from '$src/env'
|
||||
import { client } from '$src/pocketbase'
|
||||
import { bash } from 'svelte-highlight/languages'
|
||||
import AccordionItem from '../../../../components/AccordionItem.svelte'
|
||||
import { instance } from './store'
|
||||
|
||||
const { user } = client()
|
||||
$: ({ subdomain } = $instance)
|
||||
const { email } = user() || {}
|
||||
|
||||
// This will hide the component if the email was not found
|
||||
if (!email) {
|
||||
throw new Error(`Email expected here`)
|
||||
}
|
||||
@ -17,41 +18,49 @@
|
||||
)}@ftp.sfo-1.${PUBLIC_APP_DOMAIN}`
|
||||
</script>
|
||||
|
||||
<AccordionItem title="FTP Access">
|
||||
<p>
|
||||
<Card>
|
||||
<CardHeader
|
||||
documentation="https://pockethost.gitbook.io/manual/daily-usage/ftp"
|
||||
>
|
||||
FTP Access
|
||||
</CardHeader>
|
||||
|
||||
<p class="mb-8">
|
||||
Securely access your instance files via FTPS. Use your PocketHost account
|
||||
login and password.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://pockethost.io/docs/usage/ftp">Full documentation</a>
|
||||
</p>
|
||||
<p>Bash:</p>
|
||||
<CodeSample code={`ftp ${ftpUrl}`} language={bash} />
|
||||
<table>
|
||||
<thead><tr><th>Directory</th><th>Description</th></tr></thead>
|
||||
<tr>
|
||||
<th>pb_data</th><td>The PocketBase data directory</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>pb_public</th><td>Public files, such as a web frontend</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>pb_migrations</th><td>The PocketBase migrations directory</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>pb_hooks</th><td>The PocketBase JS hooks directory</td>
|
||||
</tr>
|
||||
</table>
|
||||
</AccordionItem>
|
||||
|
||||
<style lang="scss">
|
||||
table {
|
||||
margin: 10px;
|
||||
td,
|
||||
tr,
|
||||
th {
|
||||
border: 2px solid rgb(92, 92, 157);
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<p>Bash:</p>
|
||||
|
||||
<div class="mb-12">
|
||||
<CodeSample code={`ftp ${ftpUrl}`} language={bash} />
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b-2 border-neutral">Directory</th>
|
||||
<th class="border-b-2 border-neutral">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>pb_data</th>
|
||||
<td>The PocketBase data directory</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>pb_public</th>
|
||||
<td>Public files, such as a web frontend</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>pb_migrations</th>
|
||||
<td>The PocketBase migrations directory</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>pb_hooks</th>
|
||||
<td>The PocketBase JS hooks directory</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
|
@ -9,93 +9,81 @@
|
||||
import { values } from '@s-libs/micro-dash'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import AccordionItem from '../../../../components/AccordionItem.svelte'
|
||||
import { instance } from './store'
|
||||
import Card from '$components/cards/Card.svelte'
|
||||
import CardHeader from '$components/cards/CardHeader.svelte'
|
||||
|
||||
const { dbg, trace } = logger().create(`Logging.svelte`)
|
||||
|
||||
$: ({ id } = $instance)
|
||||
|
||||
// This takes in a log type and returns a specific text color
|
||||
const logColor = (type: string) => {
|
||||
if (type === 'system') return 'text-success'
|
||||
if (type === 'info') return 'text-info'
|
||||
if (type === 'error') return 'text-error'
|
||||
|
||||
return 'text-info'
|
||||
}
|
||||
|
||||
// This will take in the log message and return either the message or a string
|
||||
const logText = (log: any) => {
|
||||
try {
|
||||
return JSON.parse(log.message)
|
||||
} catch (e) {
|
||||
return log.message
|
||||
}
|
||||
}
|
||||
|
||||
const logs = writable<{ [_: RecordId]: InstanceLogFields }>({})
|
||||
let logsArray: InstanceLogFields[] = []
|
||||
|
||||
const cm = createCleanupManager()
|
||||
|
||||
onMount(async () => {
|
||||
dbg(`Watching instance log`)
|
||||
|
||||
const unsub = client().watchInstanceLog(id, (newLog) => {
|
||||
trace(`Got new log`, newLog)
|
||||
|
||||
logs.update((currentLogs) => {
|
||||
return { ...currentLogs, [newLog.id]: newLog }
|
||||
})
|
||||
|
||||
logsArray = values($logs)
|
||||
.sort((a, b) => (a.created > b.created ? 1 : -1))
|
||||
.slice(0, 1000)
|
||||
.reverse()
|
||||
})
|
||||
|
||||
cm.add(unsub)
|
||||
})
|
||||
|
||||
onDestroy(cm.shutdown)
|
||||
</script>
|
||||
|
||||
<AccordionItem title="Instance Logging">
|
||||
<p>
|
||||
<Card>
|
||||
<CardHeader>Instance Logging</CardHeader>
|
||||
|
||||
<p class="mb-4">
|
||||
Instance logs appear here in realtime, including <code>console.log</code> from
|
||||
JavaScript hooks.
|
||||
</p>
|
||||
<div class="log-window">
|
||||
{#each logsArray as log}
|
||||
<div class="log">
|
||||
<div class="time">{log.created}</div>
|
||||
|
||||
<div class={`stream ${log.stream}`}>{log.stream}</div>
|
||||
<div class={`message ${log.stream}`}>
|
||||
{(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(log.message)
|
||||
return `<pre><code>${parsed}</code></pre>`
|
||||
} catch (e) {
|
||||
return log.message
|
||||
}
|
||||
})()}
|
||||
<div class="mockup-code">
|
||||
<div class="h-[450px] flex flex-col-reverse overflow-y-scroll">
|
||||
{#each logsArray as log}
|
||||
<div class="px-4" data-prefix=">">
|
||||
<span class="text-xs mr-2"
|
||||
><i class="fa-regular fa-angle-right"></i></span
|
||||
>
|
||||
<span class="text-xs mr-1">{log.created}</span>
|
||||
<span class={`text-xs mr-1 font-bold ${logColor(log.stream)}`}
|
||||
>{log.stream}</span
|
||||
>
|
||||
<span class="text-xs mr-1 text-base-content">{logText(log)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div></AccordionItem
|
||||
>
|
||||
|
||||
<style lang="scss">
|
||||
.log-window {
|
||||
border: 1px solid gray;
|
||||
padding: 5px;
|
||||
height: 500px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
white-space: nowrap;
|
||||
.log {
|
||||
position: relative;
|
||||
font-family: monospace;
|
||||
.time {
|
||||
color: gray;
|
||||
display: inline-block;
|
||||
}
|
||||
.stream {
|
||||
color: gray;
|
||||
display: inline-block;
|
||||
&.system {
|
||||
color: orange;
|
||||
}
|
||||
&.info {
|
||||
color: blue;
|
||||
}
|
||||
&.error {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
.message {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -1,22 +1,23 @@
|
||||
<script lang="ts">
|
||||
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
|
||||
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
|
||||
import AccordionItem from '../../../../components/AccordionItem.svelte'
|
||||
import { instance } from './store'
|
||||
|
||||
$: ({ subdomain, status, version, secondsThisMonth } = $instance)
|
||||
|
||||
const url = `${PUBLIC_APP_PROTOCOL}://${subdomain}.${PUBLIC_APP_DOMAIN}`
|
||||
$: ({ status, version, secondsThisMonth } = $instance)
|
||||
</script>
|
||||
|
||||
<AccordionItem title="Overview" show={true}>
|
||||
<div class="card card-body bg-base-200">
|
||||
<h3 class="font-bold text-2xl">Overview</h3>
|
||||
|
||||
<div>
|
||||
Status: <ProvisioningStatus {status} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Usage: {Math.ceil(secondsThisMonth / 60)} mins
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Version: {version} (change in Danger Zone)
|
||||
Version: {version}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</div>
|
||||
|
@ -1,110 +1,126 @@
|
||||
<script lang="ts">
|
||||
import { SECRET_KEY_REGEX } from '@pockethost/common'
|
||||
// import the items as described in the store
|
||||
import { items } from './stores.js'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
// variables bound to the input elements
|
||||
let name: string = ''
|
||||
let value: string = ''
|
||||
// Keep track of the new key and value to be added
|
||||
let secretKey: string = ''
|
||||
let secretValue: string = ''
|
||||
|
||||
// These will validate the key and value before being submitted
|
||||
let isKeyValid = false
|
||||
let isValueValid = false
|
||||
let isFormValid = false
|
||||
|
||||
// following the submit event, proceed to update the store by adding the item or updating its value (if existing)
|
||||
const handleSubmit = () => {
|
||||
// findIndex returns -1 if a match is not found
|
||||
const index = $items.findIndex((item) => item.name === name)
|
||||
items.upsert({ name, value })
|
||||
name = ''
|
||||
value = ''
|
||||
// This will animate a success message when the key is saved
|
||||
let successfulSave = false
|
||||
|
||||
// Keep track of any error message
|
||||
let errorMessage: string = ''
|
||||
|
||||
// Watch for changes in real time and update the key and value as the user types them
|
||||
$: {
|
||||
secretKey = secretKey.toUpperCase()
|
||||
secretValue = secretValue.trim()
|
||||
isKeyValid = !!secretKey.match(SECRET_KEY_REGEX)
|
||||
isValueValid = secretValue.length > 0
|
||||
isFormValid = isKeyValid && isValueValid
|
||||
}
|
||||
|
||||
$: {
|
||||
name = name.toUpperCase()
|
||||
value = value.trim()
|
||||
isKeyValid = !!name.match(SECRET_KEY_REGEX)
|
||||
isValueValid = value.length > 0
|
||||
isFormValid = isKeyValid && isValueValid
|
||||
console.log({ isFormValid })
|
||||
// Submit the form to create the new environment variable
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Reset any messaging
|
||||
errorMessage = ''
|
||||
|
||||
try {
|
||||
// Block the button from submitting more than once
|
||||
isFormValid = false
|
||||
|
||||
// Save to the database
|
||||
items.upsert({ name: secretKey, value: secretValue })
|
||||
|
||||
// Reset the values when the POST is done
|
||||
secretKey = ''
|
||||
secretValue = ''
|
||||
|
||||
// Enable the submit button
|
||||
isFormValid = true
|
||||
|
||||
// Show the success message
|
||||
successfulSave = true
|
||||
|
||||
// Remove the success toast after a few seconds
|
||||
setTimeout(() => {
|
||||
successfulSave = false
|
||||
}, 5000)
|
||||
} catch (error: any) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- form component
|
||||
introduce the component with a heading
|
||||
describe the form with input elements of type text and number
|
||||
-->
|
||||
<div class="container">
|
||||
<h2>Add an Environment Variable</h2>
|
||||
<section>
|
||||
<!-- display a form with 2 fields
|
||||
- input[type="text"], describing the name
|
||||
- input[type="number"], describing the value (price, cost, currently undecided)
|
||||
<div class="mb-8">
|
||||
<h4 class="flex items-center font-bold h-9 text-lg mb-3">
|
||||
Add an Environment Variable
|
||||
</h4>
|
||||
|
||||
-->
|
||||
<!-- wrap each input in a label -->
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input class="form-control" required type="text" bind:value={name} />
|
||||
</label>
|
||||
<label>
|
||||
<span>Value</span>
|
||||
<input class="form-control" bind:value placeholder="" type="password" />
|
||||
</label>
|
||||
<form on:submit={handleSubmit} class="mb-4">
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="label" for="secret-key">
|
||||
<span class="label-text">Key</span>
|
||||
</label>
|
||||
|
||||
<!-- describe the action of the icon button through aria attributes -->
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
aria-label="Create entry"
|
||||
aria-describedby="description"
|
||||
disabled={!isFormValid}
|
||||
on:click={() => handleSubmit()}
|
||||
>Add
|
||||
</button>
|
||||
{#if !isKeyValid && name.length > 0}
|
||||
<div class="text-danger">
|
||||
All key names must be upper case, alphanumeric, and may include
|
||||
underscore (_).
|
||||
<input
|
||||
id="secret-key"
|
||||
type="text"
|
||||
bind:value={secretKey}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="secret-value">
|
||||
<span class="label-text">Value</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="secret-value"
|
||||
type="text"
|
||||
bind:value={secretValue}
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !isKeyValid && secretKey.length > 0}
|
||||
<div in:slide class="alert alert-error mb-4">
|
||||
<i class="fa-regular fa-circle-exclamation"></i>
|
||||
All key names must be upper case, alphanumeric, and may include underscore
|
||||
(_).
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
border: 1px solid black;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
width: 300px;
|
||||
h2 {
|
||||
font-size: 13pt;
|
||||
}
|
||||
/* display the input in a wrapping row
|
||||
flip the hue for the color and background
|
||||
*/
|
||||
section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
color: var(--bs-gray-600);
|
||||
background: var(--bs-gray-100);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
label {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
line-height: 2;
|
||||
margin: 10px;
|
||||
input {
|
||||
font-size: 1.1rem;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
background: none;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn btn-primary" disabled={!isFormValid}
|
||||
>Add <i class="fa-regular fa-floppy-disk"></i></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{#if successfulSave}
|
||||
<div in:slide class="alert alert-success">
|
||||
<i class="fa-regular fa-shield-check"></i>
|
||||
Your new environment variable has been saved.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if errorMessage}
|
||||
<div in:slide class="alert alert-error mb-4">
|
||||
<i class="fa-regular fa-circle-exclamation"></i>
|
||||
{errorMessage}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,87 +1,54 @@
|
||||
<script type="ts">
|
||||
// import the items as described in the store
|
||||
import { fade } from 'svelte/transition'
|
||||
import { items } from './stores'
|
||||
|
||||
let showSecretKeys = false
|
||||
</script>
|
||||
|
||||
{#if $items.length > 0}
|
||||
<!-- introduce the section with a heading and describe the items in a main element -->
|
||||
<section>
|
||||
<!-- display the articles in a grid, specifying the name and numerical values in a column -->
|
||||
<main>
|
||||
{#each $items as item}
|
||||
<article style="border-color: {item.color}">
|
||||
<h2>{item.name}</h2>
|
||||
<div class="value">
|
||||
{item.value.slice(0, 2)}{item.value.slice(2).replaceAll(/./g, '*')}
|
||||
</div>
|
||||
<!-- following a click on the button update the store with the delete operation -->
|
||||
<button on:click={() => items.delete(item.name)} aria-label="Delete">
|
||||
<svg viewBox="0 0 100 100" width="30" height="30">
|
||||
<use href="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</article>
|
||||
{/each}
|
||||
</main>
|
||||
</section>
|
||||
{/if}
|
||||
<div class="flex items-center justify-between mb-3 h-9">
|
||||
<h4 class="font-bold text-lg">Current Environment Variables</h4>
|
||||
|
||||
<style lang="scss">
|
||||
/* display the items as squares in a grid */
|
||||
main {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
grid-template-columns: repeat(auto-fill, 150px);
|
||||
grid-auto-rows: 150px;
|
||||
grid-gap: 2rem;
|
||||
}
|
||||
/* display the text elements in a column */
|
||||
article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: hsla(240, 25%, 50%, 0.1);
|
||||
border: 5px solid currentColor;
|
||||
border-radius: 25px;
|
||||
position: relative;
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
font-size: 10pt;
|
||||
}
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text text-accent mr-2">Show Secrets</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
bind:checked={showSecretKeys}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
.value {
|
||||
font-size: 10pt;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-2/5 border-b-2 border-neutral">Key</th>
|
||||
<th class="w-2/5 border-b-2 border-neutral">Value</th>
|
||||
<th class="w-1/5 border-b-2 border-neutral text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
right: 0%;
|
||||
transform: translate(50%, -50%);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: inherit;
|
||||
background: currentColor;
|
||||
/* use the same hue as the background to fake a clip on the border underneath */
|
||||
box-shadow: 0 0 0 0.5rem hsl(240, 25%, 20%);
|
||||
svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: hsl(240, 25%, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* absolute position the button in the top right corner */
|
||||
</style>
|
||||
<tbody>
|
||||
{#each $items as item}
|
||||
<tr transition:fade>
|
||||
<th>{item.name}</th>
|
||||
<td
|
||||
>{showSecretKeys
|
||||
? item.value
|
||||
: item.value.slice(0, 2) +
|
||||
item.value.slice(2).replaceAll(/./g, '*')}</td
|
||||
>
|
||||
<td class="text-right">
|
||||
<button
|
||||
aria-label="Delete"
|
||||
on:click={() => items.delete(item.name)}
|
||||
type="button"
|
||||
class="btn btn-sm btn-square btn-outline btn-warning"
|
||||
><i class="fa-regular fa-trash"></i></button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -8,30 +8,46 @@
|
||||
} from '@pockethost/common'
|
||||
import { forEach, reduce } from '@s-libs/micro-dash'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import AccordionItem from '../../../../../components/AccordionItem.svelte'
|
||||
import { instance } from '../store'
|
||||
import Form from './Form.svelte'
|
||||
import List from './List.svelte'
|
||||
import { items } from './stores'
|
||||
import SvgIcons from './SvgIcons.svelte'
|
||||
import Card from '$components/cards/Card.svelte'
|
||||
import CardHeader from '$components/cards/CardHeader.svelte'
|
||||
|
||||
// TODO: Hot Reload is causing an infinite loop in the network tab for some reason. Wasn't able to figure out why
|
||||
|
||||
$: ({ id, secrets } = $instance)
|
||||
|
||||
// Keep track of which tab the user has selected
|
||||
let activeTab = 0
|
||||
|
||||
// Toggle between the tabs on click
|
||||
const handleTabChange = (id: number) => {
|
||||
activeTab = id
|
||||
}
|
||||
|
||||
const { dbg } = logger().create(`Secrets.svelte`)
|
||||
|
||||
const cm = createCleanupManager()
|
||||
|
||||
onMount(() => {
|
||||
items.clear()
|
||||
|
||||
forEach(secrets || {}, (value, name) => {
|
||||
items.upsert({ name, value })
|
||||
})
|
||||
|
||||
let initial = false
|
||||
|
||||
const unsub = items.subscribe(async (secrets) => {
|
||||
if (!initial) {
|
||||
initial = true
|
||||
return
|
||||
}
|
||||
|
||||
dbg(`Got change`, secrets)
|
||||
|
||||
await client().saveSecrets({
|
||||
instanceId: id,
|
||||
secrets: reduce(
|
||||
@ -45,56 +61,62 @@
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
cm.add(unsub)
|
||||
})
|
||||
|
||||
onDestroy(cm.shutdown)
|
||||
</script>
|
||||
|
||||
<AccordionItem title="Secrets">
|
||||
<p>
|
||||
<Card>
|
||||
<CardHeader>Secrets</CardHeader>
|
||||
|
||||
<p class="mb-4">
|
||||
These secrets are passed into your <code>pocketbase</code> executable and
|
||||
can be accessed from <code>pb_hooks</code> JS hooks.
|
||||
</p>
|
||||
<CodeSample
|
||||
code={$items
|
||||
.map((secret) => `const ${secret.name} = process.env.${secret.name}`)
|
||||
.join('\n')}
|
||||
/>
|
||||
|
||||
<SvgIcons />
|
||||
<Form />
|
||||
<List />
|
||||
</AccordionItem>
|
||||
<!-- If the user has any secrets, render them in a code block -->
|
||||
{#if $items.length > 0}
|
||||
<div class="mb-8">
|
||||
<CodeSample code={`const YOUR_KEY = process.env.YOUR_KEY`} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.secrets {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.secrets {
|
||||
h2 {
|
||||
position: relative;
|
||||
padding: 0.25rem;
|
||||
span {
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
right: 100%;
|
||||
transform: translateY(-50%);
|
||||
display: block;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
border-radius: 0.75rem;
|
||||
background: hsla(240, 25%, 50%, 0.3);
|
||||
}
|
||||
span,
|
||||
span svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 0 3px hsla(240, 25%, 0%, 0.5));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{#if $items.length === 0}
|
||||
<div class="alert border-2 border-neutral mb-8">
|
||||
<i class="fa-regular fa-shield-keyhole"></i>
|
||||
<span>No Environment Variables Found</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="tabs mb-4 border-b-[1px] border-neutral">
|
||||
<button
|
||||
on:click={() => handleTabChange(0)}
|
||||
type="button"
|
||||
class="tab border-b-2 {activeTab === 0
|
||||
? 'tab-active font-bold border-base-content'
|
||||
: 'border-neutral'}"
|
||||
><i class="fa-regular fa-plus mr-2"></i> Add New</button
|
||||
>
|
||||
|
||||
<button
|
||||
on:click={() => handleTabChange(1)}
|
||||
type="button"
|
||||
class="tab border-b-2 {activeTab === 1
|
||||
? 'tab-active font-bold border-base-content'
|
||||
: 'border-neutral'}"
|
||||
><i class="fa-regular fa-list mr-2"></i> Current List</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if activeTab === 0}
|
||||
<Form />
|
||||
{/if}
|
||||
|
||||
{#if activeTab === 1}
|
||||
<List />
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -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
|
||||
upsert: (item: SecretItem) => {
|
||||
dbg(`Upserting`, item)
|
||||
|
||||
const { name, value } = sanitize(item)
|
||||
|
||||
return update((n) => {
|
||||
return formatInput([
|
||||
...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">
|
||||
import AlertBar from '$components/AlertBar.svelte'
|
||||
import Card from '$components/cards/Card.svelte'
|
||||
import CardHeader from '$components/cards/CardHeader.svelte'
|
||||
import { PUBLIC_APP_DOMAIN } from '$src/env'
|
||||
import { handleCreateNewInstance } from '$util/database'
|
||||
import { generateSlug } from 'random-word-slugs'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
let instanceName: string = generateSlug(2)
|
||||
let formError: string = ''
|
||||
@ -36,77 +38,49 @@
|
||||
<title>New Instance - PocketHost</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<div class="py-4">
|
||||
<h1 class="text-center">Choose a name for your PocketBase app.</h1>
|
||||
</div>
|
||||
<h2 class="text-4xl text-base-content font-bold capitalize mb-6">
|
||||
Create A New App
|
||||
</h2>
|
||||
|
||||
<div class="row g-3 align-items-center justify-content-center mb-4">
|
||||
<div class="col-auto">
|
||||
<label for="instance-name" class="col-form-label">Instance Name:</label>
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-2 grid-cols-1">
|
||||
<Card>
|
||||
<form on:submit={handleSubmit}>
|
||||
<CardHeader>Choose a name for your PocketBase app.</CardHeader>
|
||||
|
||||
<div class="col-auto pe-1 position-relative">
|
||||
<input
|
||||
type="text"
|
||||
id="instance-name"
|
||||
class="form-control"
|
||||
bind:value={instanceName}
|
||||
/>
|
||||
<div class="flex rename-instance-form-container-query gap-4">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={instanceName}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
|
||||
<button
|
||||
aria-label="Regenerate Instance Name"
|
||||
type="button"
|
||||
style="transform: rotate({rotationCounter}deg);"
|
||||
class="btn btn-light rounded-circle regenerate-instance-name-btn"
|
||||
on:click={handleInstanceNameRegeneration}
|
||||
>
|
||||
<i class="bi bi-arrow-repeat" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-secondary"
|
||||
aria-label="Regenerate Instance Name"
|
||||
on:click={handleInstanceNameRegeneration}
|
||||
><i class="fa-regular fa-arrows-rotate"></i></button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="col-auto ps-0">
|
||||
<span class="form-text">.{PUBLIC_APP_DOMAIN}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="text-center font-bold py-12">
|
||||
https://{instanceName}.{PUBLIC_APP_DOMAIN}
|
||||
</h4>
|
||||
|
||||
{#if formError}
|
||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||
{/if}
|
||||
{#if formError}
|
||||
<div transition:slide class="alert alert-error mb-5">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
<span>{formError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="text-center">
|
||||
<a href="/dashboard" class="btn btn-light" disabled={isFormButtonDisabled}
|
||||
>Cancel</a
|
||||
>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<a href="/dashboard" class="btn">Cancel</a>
|
||||
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
disabled={isFormButtonDisabled}
|
||||
on:click={handleSubmit}
|
||||
>
|
||||
Create <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary" disabled={isFormButtonDisabled}>
|
||||
Create <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
max-width: 600px;
|
||||
min-height: 70vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.regenerate-instance-name-btn {
|
||||
padding: 0;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
position: absolute;
|
||||
z-index: 500;
|
||||
top: 2px;
|
||||
right: 6px;
|
||||
transition: all 200ms;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,52 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
|
||||
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
|
||||
import RetroBoxContainer from '$components/RetroBoxContainer.svelte'
|
||||
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
|
||||
import { client } from '$src/pocketbase'
|
||||
import {
|
||||
logger,
|
||||
type InstanceFields,
|
||||
type InstanceId,
|
||||
type InstanceRecordsById,
|
||||
} from '@pockethost/common'
|
||||
import { globalInstancesStore } from '$util/stores'
|
||||
import { values } from '@s-libs/micro-dash'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import { fade } from 'svelte/transition'
|
||||
import InstanceList from './InstanceList.svelte'
|
||||
|
||||
const { error } = logger()
|
||||
let apps: InstanceRecordsById = {}
|
||||
|
||||
const instancesStore = writable<{ [_: InstanceId]: InstanceFields }>({})
|
||||
$: isFirstApplication = values($instancesStore).length === 0
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
;(async () => {
|
||||
const { getAllInstancesById } = client()
|
||||
const instances = await getAllInstancesById()
|
||||
instancesStore.set(instances)
|
||||
|
||||
console.log({ instances })
|
||||
client()
|
||||
.client.collection('instances')
|
||||
.subscribe<InstanceFields>('*', (data) => {
|
||||
instancesStore.update((instances) => {
|
||||
instances[data.record.id] = data.record
|
||||
return instances
|
||||
})
|
||||
})
|
||||
})().catch(error)
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (browser) {
|
||||
client().client.collection('instances').unsubscribe('*').catch(error)
|
||||
}
|
||||
})
|
||||
$: isFirstApplication = values($globalInstancesStore).length === 0
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@ -54,112 +12,17 @@
|
||||
</svelte:head>
|
||||
|
||||
<AuthStateGuard>
|
||||
<div class="container" in:fade={{ duration: 30 }}>
|
||||
{#if !isFirstApplication}
|
||||
<div class="py-4">
|
||||
<h1 class="text-center">Your Apps</h1>
|
||||
</div>
|
||||
{#if !isFirstApplication}
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-4xl text-base-content font-bold capitalize">Dashboard</h2>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
{#each values($instancesStore) as app}
|
||||
<div class="col-xl-4 col-md-6 col-12 mb-5">
|
||||
<div class="card">
|
||||
<div
|
||||
class="server-status d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<div class="server-status-minutes">
|
||||
Usage: {Math.ceil(app.secondsThisMonth / 60)} mins
|
||||
{#if app.maintenance}
|
||||
<span class="text-warning">Maintenance Mode</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-flex align-items-center gap-3 server-status-minutes"
|
||||
>
|
||||
{app.version}
|
||||
<ProvisioningStatus status={app.status} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mb-4 font-monospace">{app.subdomain}</h2>
|
||||
|
||||
<div class="d-flex justify-content-around">
|
||||
<a href={`/app/instances/${app.id}`} class="btn btn-light">
|
||||
<i class="bi bi-gear-fill" />
|
||||
<span>Details</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="btn btn-light pocketbase-button"
|
||||
href={`${PUBLIC_APP_PROTOCOL}://${app.subdomain}.${PUBLIC_APP_DOMAIN}/_`}
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
src="/images/pocketbase-logo.svg"
|
||||
alt="PocketBase Logo"
|
||||
class="img-fluid"
|
||||
/>
|
||||
<span>Admin</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="first-app-screen">
|
||||
<RetroBoxContainer minHeight={isFirstApplication ? 500 : 0}>
|
||||
<div class="px-lg-5">
|
||||
<h2 class="mb-4">
|
||||
Create Your {isFirstApplication ? 'First' : 'Next'} App
|
||||
</h2>
|
||||
<a href="/app/new" class="btn btn-primary btn-lg"
|
||||
><i class="bi bi-plus" /> New App</a
|
||||
>
|
||||
</div>
|
||||
</RetroBoxContainer>
|
||||
<a href="/app/new" class="m-3 btn btn-primary"
|
||||
><i class="fa-solid fa-plus"></i> New App</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<UsageChartForAllInstances />-->
|
||||
|
||||
<InstanceList />
|
||||
{/if}
|
||||
</AuthStateGuard>
|
||||
|
||||
<style lang="scss">
|
||||
.first-app-screen {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 85px 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0;
|
||||
padding: 42px 24px 24px 24px;
|
||||
box-shadow: var(--soft-box-shadow);
|
||||
}
|
||||
|
||||
.server-status {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 16px;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.server-status-minutes {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.server-status-icons {
|
||||
}
|
||||
|
||||
.pocketbase-button img {
|
||||
max-width: 25px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.first-app-screen {
|
||||
min-height: 70vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
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">
|
||||
import { handleLogin } from '$util/database'
|
||||
import AlertBar from '$components/AlertBar.svelte'
|
||||
import { handleLogin } from '$util/database'
|
||||
|
||||
let email: string = ''
|
||||
let password: string = ''
|
||||
@ -26,90 +26,59 @@
|
||||
<title>Sign In - PocketHost</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page-bg">
|
||||
<div class="card">
|
||||
<h2 class="mb-4">Login</h2>
|
||||
<div class="flex justify-center">
|
||||
<div class="card w-96 bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">Login</h2>
|
||||
|
||||
<form on:submit={handleSubmit}>
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
bind:value={email}
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
<label for="email">Email address</label>
|
||||
<form on:submit={handleSubmit}>
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label" for="email">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
bind:value={email}
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label" for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
bind:value={password}
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="m-3"></p>
|
||||
|
||||
{#if formError}
|
||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100"
|
||||
disabled={isFormButtonDisabled}
|
||||
>
|
||||
Log In <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="py-4"><hr /></div>
|
||||
|
||||
<div class="text-center">
|
||||
<a class="link" href="/signup">sign up</a> |
|
||||
<a class="link" href="/login/password-reset">forgot password</a>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
bind:value={password}
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<label for="password">Password</label>
|
||||
</div>
|
||||
|
||||
<p class="password-reset-container">
|
||||
<a href="/login/password-reset">Forgot Your Password?</a>
|
||||
</p>
|
||||
|
||||
{#if formError}
|
||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100"
|
||||
disabled={isFormButtonDisabled}
|
||||
>
|
||||
Log In <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="py-4"><hr /></div>
|
||||
|
||||
<div class="text-center">
|
||||
Need to <a href="/signup">create an account</a>?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.page-bg {
|
||||
background-color: #222;
|
||||
background-image: var(--gradient-light-soft-blue);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 91px);
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0;
|
||||
box-shadow: var(--soft-box-shadow);
|
||||
padding: 24px;
|
||||
max-width: 425px;
|
||||
width: 100%;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.password-reset-container {
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.card {
|
||||
padding: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { handleUnauthenticatedPasswordReset } from '$util/database'
|
||||
import AlertBar from '$components/AlertBar.svelte'
|
||||
import { handleUnauthenticatedPasswordReset } from '$util/database'
|
||||
|
||||
let email: string = ''
|
||||
let formError: string = ''
|
||||
@ -28,81 +28,52 @@
|
||||
<title>Password Reset - PocketHost</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page-bg">
|
||||
<div class="card">
|
||||
{#if userShouldCheckTheirEmail}
|
||||
<div class="text-center">
|
||||
<h2 class="mb-4">Check Your Email</h2>
|
||||
<p>
|
||||
A verification link has been sent to <br /><strong>{email}</strong>
|
||||
</p>
|
||||
<div class="flex justify-center">
|
||||
<div class="card w-96 bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
{#if userShouldCheckTheirEmail}
|
||||
<div class="text-center">
|
||||
<h2 class="mb-4">Check Your Email</h2>
|
||||
<p>
|
||||
A verification link has been sent to <br /><strong>{email}</strong>
|
||||
</p>
|
||||
|
||||
<div class="display-1">
|
||||
<i class="bi bi-envelope-check" />
|
||||
<div class="display-1">
|
||||
<i class="bi bi-envelope-check" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<h2 class="mb-4">Password Reset</h2>
|
||||
{:else}
|
||||
<h2 class="card-title mb-4">Password Reset</h2>
|
||||
|
||||
<form on:submit={handleSubmit}>
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
bind:value={email}
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
<label for="email">Email address</label>
|
||||
</div>
|
||||
<form on:submit={handleSubmit}>
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label" for="email">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
bind:value={email}
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if formError}
|
||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||
{/if}
|
||||
{#if formError}
|
||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100"
|
||||
disabled={isFormButtonDisabled}
|
||||
>
|
||||
Send Verification Email <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<div class="py-4"><hr /></div>
|
||||
|
||||
<div class="text-center">
|
||||
Need to <a href="/signup">create an account</a>?
|
||||
<div class="mt-4 card-actions justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100"
|
||||
disabled={isFormButtonDisabled}
|
||||
>
|
||||
Send Verification Email <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.page-bg {
|
||||
background-color: #222;
|
||||
background-image: var(--gradient-light-soft-blue-vertical);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 91px);
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0;
|
||||
box-shadow: var(--soft-box-shadow);
|
||||
padding: 24px;
|
||||
max-width: 425px;
|
||||
width: 100%;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.card {
|
||||
padding: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -37,81 +37,56 @@
|
||||
<title>Sign Up - PocketHost</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page-bg">
|
||||
<div class="card">
|
||||
<h2 class="mb-4">Sign Up</h2>
|
||||
<div class="flex justify-center">
|
||||
<div class="card w-96 bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title mb-4">Sign Up</h2>
|
||||
|
||||
<form on:submit={handleSubmit}>
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
bind:value={email}
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
<label for="email">Email address</label>
|
||||
<form on:submit={handleSubmit}>
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label" for="email">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
bind:value={email}
|
||||
required
|
||||
autocomplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control w-full max-w-xs">
|
||||
<label class="label" for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
bind:value={password}
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if formError}
|
||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="m-3 btn btn-primary w-100"
|
||||
disabled={isFormButtonDisabled}
|
||||
>
|
||||
Sign Up <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="py-4"><hr /></div>
|
||||
|
||||
<div class="text-center">
|
||||
<a class="link" href="/login">log in</a>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
bind:value={password}
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<label for="password">Password</label>
|
||||
</div>
|
||||
|
||||
{#if formError}
|
||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary w-100"
|
||||
disabled={isFormButtonDisabled}
|
||||
>
|
||||
Sign Up <i class="bi bi-arrow-right-short" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="py-4"><hr /></div>
|
||||
|
||||
<div class="text-center">
|
||||
Already have an account? <a href="/login">Log in</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.page-bg {
|
||||
background-color: #222;
|
||||
background-image: var(--gradient-dark-soft-blue);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 91px);
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0;
|
||||
box-shadow: var(--soft-box-shadow);
|
||||
padding: 24px;
|
||||
max-width: 425px;
|
||||
width: 100%;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.card {
|
||||
padding: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
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 { client } from '$src/pocketbase'
|
||||
import type { AuthStoreProps } from '$src/pocketbase/PocketbaseClient'
|
||||
import { logger } from '@pockethost/common'
|
||||
import {
|
||||
logger,
|
||||
type InstanceFields,
|
||||
type InstanceId,
|
||||
} from '@pockethost/common'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export const authStoreState = writable<AuthStoreProps>({
|
||||
@ -9,6 +13,7 @@ export const authStoreState = writable<AuthStoreProps>({
|
||||
model: null,
|
||||
token: '',
|
||||
})
|
||||
|
||||
export const isUserLoggedIn = writable(false)
|
||||
export const isUserVerified = writable(false)
|
||||
export const isAuthStateInitialized = writable(false)
|
||||
@ -20,7 +25,7 @@ if (browser) {
|
||||
* Listen for auth change events. When we get at least one, the auth state is initialized.
|
||||
*/
|
||||
onAuthChange((authStoreProps) => {
|
||||
const { dbg, error, warn } = logger()
|
||||
const { dbg } = logger()
|
||||
dbg(`onAuthChange in store`, { ...authStoreProps })
|
||||
authStoreState.set(authStoreProps)
|
||||
isAuthStateInitialized.set(true)
|
||||
@ -28,9 +33,14 @@ if (browser) {
|
||||
|
||||
// Update derived stores when authStore changes
|
||||
authStoreState.subscribe((authStoreProps) => {
|
||||
const { dbg, error, warn } = logger()
|
||||
const { dbg } = logger()
|
||||
dbg(`subscriber change`, authStoreProps)
|
||||
isUserLoggedIn.set(authStoreProps.isValid)
|
||||
isUserVerified.set(!!authStoreProps.model?.verified)
|
||||
})
|
||||
}
|
||||
|
||||
// This holds an array of all the user's instances and their data
|
||||
export const globalInstancesStore = writable<{
|
||||
[_: InstanceId]: InstanceFields
|
||||
}>({})
|
||||
|
@ -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/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@kurkle/color@^0.3.0":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
|
||||
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
|
||||
|
||||
"@mapbox/node-pre-gyp@^1.0.0":
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"
|
||||
@ -1324,6 +1329,13 @@ character-parser@^2.2.0:
|
||||
dependencies:
|
||||
is-regex "^1.0.3"
|
||||
|
||||
chart.js@4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.0.tgz#df843fdd9ec6bd88d7f07e2b95348d221bd2698c"
|
||||
integrity sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==
|
||||
dependencies:
|
||||
"@kurkle/color" "^0.3.0"
|
||||
|
||||
chokidar-cli@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar-cli/-/chokidar-cli-3.0.0.tgz#29283666063b9e167559d30f247ff8fc48794eb7"
|
||||
@ -1685,6 +1697,17 @@ daisyui@^3.7.7:
|
||||
postcss-js "^4"
|
||||
tailwindcss "^3"
|
||||
|
||||
daisyui@^3.8.1:
|
||||
version "3.8.3"
|
||||
resolved "https://registry.yarnpkg.com/daisyui/-/daisyui-3.8.3.tgz#75ff35d544d2ff3c745093be84e6bd267d560ed9"
|
||||
integrity sha512-64QuR6niTv58E5YyAVBkoXQGngp04jt6XOcKGxnq1C/7r5ZE3bDuOSc4ktT7CPacTRjl5cCeNYY13VxLpZP+7A==
|
||||
dependencies:
|
||||
colord "^2.9"
|
||||
css-selector-tokenizer "^0.8"
|
||||
postcss "^8"
|
||||
postcss-js "^4"
|
||||
tailwindcss "^3"
|
||||
|
||||
data-uri-to-buffer@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
|
||||
@ -5121,6 +5144,11 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
svelte-chartjs@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/svelte-chartjs/-/svelte-chartjs-3.1.2.tgz#741114fcf8168615535de33b360a6bafcee2344e"
|
||||
integrity sha512-3+6gY2IJ9Ua8R9pk3iS1ypa7Z9OoXCJb9oPwIfTp7caJM+X+RrWnH2CTkGAq7FeSxc2nnmW08tYN88Q8Y+5M+w==
|
||||
|
||||
svelte-check@^3.5.2:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-3.5.2.tgz#d6e650996afbe80f5e5b9b02d3fb9489f7d6fb8a"
|
||||
|
Loading…
x
Reference in New Issue
Block a user