enh: rename pockethost.io to dashboard

This commit is contained in:
Ben Allfree
2023-10-02 08:41:49 -07:00
parent b0cc205ae0
commit cbcec4a720
89 changed files with 9 additions and 7 deletions

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { uniqueId } from '@s-libs/micro-dash'
export let title = ''
export let show: boolean = false
export let header: 'primary' | 'danger' = 'primary'
const uid = uniqueId('a')
const headerId = `header${uid}`
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>
</div>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { AlertTypes } from '$components/AlertBar.types'
export let title: string = ''
export let text: string = ''
export let icon: string = ''
export let alertType: AlertTypes = AlertTypes.Warning
</script>
<div class="alert alert-{alertType} d-flex gap-3 align-items-center" role="alert">
{#if icon}
<i class={icon} />
{/if}
<div class="w-100">
{#if title}<p class="fw-bold mb-0">{title}</p>{/if}
{#if text}
{text}
{:else}
<slot />
{/if}
</div>
</div>

View File

@@ -0,0 +1,10 @@
export enum AlertTypes {
Primary = 'primary',
Secondary = 'secondary',
Success = 'success',
Danger = 'danger',
Warning = 'warning',
Info = 'info',
Light = 'light',
Dark = 'dark',
}

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { createEventDispatcher, tick } from 'svelte'
const dispatch = createEventDispatcher()
export let text: string
let textarea: HTMLTextAreaElement
async function copy() {
textarea.select()
document.execCommand('Copy')
await tick()
textarea.blur()
dispatch('copy')
}
</script>
<slot {copy} />
<textarea bind:this={textarea} value={text} />
<style>
textarea {
left: 0;
bottom: 0;
margin: 0;
padding: 0;
opacity: 0;
width: 1px;
height: 1px;
border: none;
display: block;
position: absolute;
}
</style>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import CopyButton from '$components/CopyButton.svelte'
import { logger } from '@pockethost/common'
import { Highlight } from 'svelte-highlight'
import { typescript, type LanguageType } from 'svelte-highlight/languages'
const { dbg } = logger()
export let code: string
export let language: LanguageType<'typescript' | 'bash'> = typescript
const handleCopy = () => {
dbg('copied')
}
</script>
<div class="copy-container">
<Highlight {language} {code} />
<div class="copy-button">
<CopyButton {code} copy={handleCopy} />
</div>
</div>
<style lang="scss">
.copy-container {
position: relative;
margin: 5px;
border: 1px solid gray;
.copy-button {
position: absolute;
top: 2px;
right: 2px;
}
}
</style>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import Clipboard from '$components/Clipboard.svelte'
import TinyButton from './helpers/TinyButton.svelte'
let isCopied = false
export let code: string
export let copy: () => void
const handleCopy = () => {
isCopied = true
copy()
}
</script>
<Clipboard text={code} let:copy on:copy={handleCopy}>
<TinyButton click={copy} style={isCopied ? 'success' : 'primary'}
>{isCopied ? 'Copied!' : 'Copy'}</TinyButton
>
</Clipboard>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
export let title: string = ''
export let subtitle: string = ''
export let icon: string = ''
export let fullHeight: boolean = false
</script>
<div class="card {fullHeight && 'h-100'}">
<div class="card-body">
{#if icon}
<div class="d-flex align-items-center gap-3 mb-3">
<div class="card-icon">
<i class={icon} />
</div>
<div>
{#if title}<h5 class="card-title {!subtitle && 'mb-0'}">{title}</h5>{/if}
{#if subtitle}<h6 class="card-subtitle mb-0 text-muted">{subtitle}</h6>{/if}
</div>
</div>
{:else}
{#if title}<h5 class="card-title">{title}</h5>{/if}
{#if subtitle}<h6 class="card-subtitle mb-2 text-muted">{subtitle}</h6>{/if}
{/if}
<slot />
</div>
</div>
<style>
.card {
border: 0;
box-shadow: var(--soft-box-shadow);
border-radius: 18px;
}
.card-icon {
background-color: var(--bs-gray-200);
width: 35px;
height: 35px;
border-radius: 35px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

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

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { fade } from 'svelte/transition'
import { onMount } from 'svelte'
import RetroBoxContainer from '$components/RetroBoxContainer.svelte'
let isReady: boolean = false
onMount(() => {
setTimeout(() => {
isReady = true
}, 3000)
})
</script>
<RetroBoxContainer minHeight={500} bgColor="#fff">
{#if !isReady}
<div class="hero-animation-content text-center">
<p>Creating Your New Instance...</p>
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
{/if}
{#if isReady}
<div in:fade={{ duration: 1000 }}>
<img
src="/images/pocketbase-intro-screen.jpg"
alt="Screenshot of the Pocketbase Intro UI"
class="img-fluid"
/>
</div>
{/if}
</RetroBoxContainer>
<style>
.hero-animation-content {
color: #222;
}
</style>

View File

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

View File

@@ -0,0 +1,154 @@
<script lang="ts">
import AlertBar from '$components/AlertBar.svelte'
import { handleInstanceGeneratorWidget } from '$util/database'
import { getRandomElementFromArray } from '$util/utilities'
import { generateSlug } from 'random-word-slugs'
// Controls the spin animation of the instance regeneration button
let rotationCounter: number = 0
let email: string = ''
let password: string = ''
let instanceName: string = generateSlug(2)
let formError: string = ''
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled = email.length === 0 || password.length === 0 || instanceName.length === 0
let isProcessing: boolean = false
// Fun quotes when waiting for the instance to load. This could take up to 10 seconds
let processingQuotesArray = [
'Did you know it takes fourteen sentient robots to create each instance on PocketHost?'
]
let processingQuote = getRandomElementFromArray(processingQuotesArray)
const handleInstanceNameRegeneration = () => {
rotationCounter = rotationCounter + 180
instanceName = generateSlug(2)
}
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault()
isFormButtonDisabled = true
isProcessing = true
await handleInstanceGeneratorWidget(email, password, instanceName, (error) => {
formError = error
})
isFormButtonDisabled = false
isProcessing = false
}
</script>
{#if isProcessing}
<div class="d-flex align-items-center gap-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div>
<h3 class="mb-1">Creating Your New Instance...</h3>
<p class="small text-muted mb-0">{processingQuote}</p>
</div>
</div>
{:else}
<h3 class="mb-3">Create Your Instance Now</h3>
<form class="row align-items-center" on:submit={handleSubmit}>
<div class="col-lg-6 col-12">
<div class="form-floating mb-3 mb-lg-3">
<input
type="email"
class="form-control"
id="email"
placeholder="name@example.com"
autocomplete="email"
bind:value={email}
required
/>
<label for="email">Email</label>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="form-floating mb-3 mb-lg-3">
<input
type="password"
class="form-control"
id="password"
placeholder="Password"
autocomplete="new-password"
bind:value={password}
required
/>
<label for="password">Password</label>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="form-floating mb-3 mb-lg-3">
<input
type="text"
class="form-control"
id="instance"
placeholder="Instance"
bind:value={instanceName}
required
/>
<label for="instance">Instance Name</label>
<button
aria-label="Regenerate Instance Name"
type="button"
style="transform: rotate({rotationCounter}deg);"
class="btn btn-light rounded-circle regenerate-instance-name-btn"
on:click={handleInstanceNameRegeneration}
>
<i class="bi bi-arrow-repeat" />
</button>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="mb-3 mb-lg-3 text-lg-start text-center">
<button type="submit" class="btn btn-primary" disabled={isFormButtonDisabled}>
Create <i class="bi bi-arrow-right-short" />
</button>
</div>
</div>
{#if formError}
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
{/if}
</form>
{/if}
<style>
form {
max-width: 600px;
}
.row {
--bs-gutter-x: 0.5rem;
}
.btn.btn-primary {
--bs-btn-padding-y: 12px;
}
.regenerate-instance-name-btn {
padding: 0;
width: 40px;
height: 40px;
position: absolute;
z-index: 500;
top: 10px;
right: 7px;
transition: all 200ms;
}
</style>

View File

@@ -0,0 +1,42 @@
<script lang="ts">
// Documentation Source
// https://svelte.dev/repl/26eb44932920421da01e2e21539494cd?version=3.51.0
import { onMount } from 'svelte'
export let query = ''
let mql: MediaQueryList | undefined = undefined
let mqlListener: (e: MediaQueryListEvent) => void
let wasMounted = false
let matches = false
onMount(() => {
wasMounted = true
return () => {
removeActiveListener()
}
})
$: {
if (wasMounted) {
removeActiveListener()
addNewListener(query)
}
}
function addNewListener(query: string) {
mql = window.matchMedia(query)
mqlListener = (v) => (matches = v.matches)
mql.addListener(mqlListener)
matches = mql.matches
}
function removeActiveListener() {
if (mql && mqlListener) {
mql.removeListener(mqlListener)
}
}
</script>
<slot {matches} />

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import TinyButton from '$components/helpers/TinyButton.svelte'
import { logger } from '@pockethost/common'
export let value: string = ''
export let disabled: boolean = false
export let save: (newValue: string) => Promise<string> = async () => 'saved'
const { dbg, error } = logger().create('MiniEdit.svelte')
let msg = ''
let err = ''
let oldValue = value
let editedValue = value
let editMode = false
let inputField: HTMLInputElement
const startEdit = () => {
msg = ''
err = ''
oldValue = editedValue
editMode = true
setTimeout(() => {
inputField.focus()
inputField.select()
}, 0)
}
const cancelEdit = () => {
editedValue = oldValue
editMode = false
}
const saveEdit = () => {
msg = ''
err = ''
save(editedValue)
.then((res) => {
editMode = false
msg = res
})
.catch((e) => {
error(`Got an error on save`, e)
err = e.data?.data?.subdomain?.message || e.message
})
}
</script>
{#if !editMode || disabled}
{editedValue}
<TinyButton click={startEdit} {disabled}>edit</TinyButton>
{/if}
{#if editMode && !disabled}
<input
bind:this={inputField}
type="text"
bind:value={editedValue}
on:focus={(event) => inputField.select()}
/>
<TinyButton style="success" {disabled} click={saveEdit}>save</TinyButton>
<TinyButton style="danger" {disabled} click={cancelEdit}>cancel</TinyButton>
{/if}
{#if msg}
<span class="text-success">{msg}</span>
{/if}
{#if err}
<span class="text-danger">{err}</span>
{/if}

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { uniqueId } from '@s-libs/micro-dash'
export let value: boolean = false
export let save: (newValue: boolean) => Promise<string> = async () => 'saved'
const id = uniqueId()
let msg = ''
const onChange = () => {
save(value)
.then((res) => {
msg = res
})
.catch((e) => {
msg = e.message
})
}
</script>
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
{id}
bind:checked={value}
on:change={onChange}
/>
<label class="form-check-label" for={id}><slot /></label>
</div>

View File

@@ -0,0 +1,195 @@
<script lang="ts">
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'
</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>
<button
class="btn btn-light mobile-nav-button navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#nav-links"
aria-controls="nav-links"
aria-expanded="false"
aria-label="Toggle navigation"
>
<i class="bi bi-list" />
</button>
<div class="collapse navbar-collapse" id="nav-links">
<ul class="navbar-nav ms-auto mb-2 mb-md-0">
<AuthStateGuard>
{#if $isUserLoggedIn}
<li class="nav-item text-md-start text-center">
<a class="nav-link" href="/dashboard">Dashboard</a>
</li>
<MediaQuery query="(min-width: 768px)" let:matches>
{#if matches}
<li class="nav-item dropdown">
<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.gitbook.io/manual/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.gitbook.io/manual/"
class="nav-link btn btn-outline-dark rounded-1 d-inline-block px-3"
rel="noreferrer">Docs</a
>
</li>
<li class="nav-item">
<a
class="nav-link text-md-start text-center"
href="https://github.com/benallfree/pockethost"
target="_blank"
aria-label="Link to our Github Project"
title="Link to our Github Project"
rel="noopener"
>
<i class="bi bi-github" /><span class="nav-github-link">Github</span>
</a>
</li>
<li class="nav-item text-center">
<ThemeToggle navLink={true} />
</li>
</ul>
</div>
</nav>
</header>
<style lang="scss">
header {
background-color: var(--bs-body-bg);
padding: 12px 24px;
border-bottom: 1px solid var(--bs-gray-300);
}
.logo {
img {
max-width: 50px;
margin-right: 16px;
}
h1 {
font-size: 36px;
font-weight: 300;
margin: 0;
color: var(--bs-body-color);
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;
}
}
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>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { InstanceStatus } from '@pockethost/common/src/schema'
import { onMount } from 'svelte'
export let status: InstanceStatus = InstanceStatus.Idle
let badgeColor: string = 'bg-secondary'
if (!status) {
status = InstanceStatus.Idle
}
const handleBadgeColor = () => {
switch (status) {
case 'idle':
badgeColor = 'bg-secondary'
break
case 'porting':
badgeColor = 'bg-info'
break
case 'starting':
badgeColor = 'bg-warning'
break
case 'running':
badgeColor = 'bg-success'
break
case 'failed':
badgeColor = 'bg-danger'
break
default:
badgeColor = 'bg-secondary'
break
}
}
onMount(() => {
handleBadgeColor()
})
// Watch for changes with the status variable and update the badge color
$: if (status) handleBadgeColor()
</script>
<div class={`badge ${badgeColor} ${status === 'running' && 'pulse'}`}>{status}</div>
<style lang="scss">
.pulse {
box-shadow: 0 0 0 0 rgba(46, 204, 113, 1);
transform: scale(1);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(46, 204, 113, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(46, 204, 113, 0);
}
}
</style>

View File

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

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { browser } from '$app/environment'
import { onMount } from 'svelte'
import {
THEME_ICONS,
ThemeNames,
currentIcon,
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() : ''
// Wait for the DOM to be available
onMount(() => {
updateTheme(getCurrentTheme())
})
// Alternate the theme values on toggle click
const handleClick = () => {
const newTheme = getCurrentTheme() === ThemeNames.Light ? 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>

View File

@@ -0,0 +1,63 @@
<script lang="ts">
import AlertBar from '$components/AlertBar.svelte'
import { AlertTypes } from '$components/AlertBar.types'
import { handleResendVerificationEmail } from '$util/database'
import { isUserLoggedIn, isUserVerified } from '$util/stores'
let defaultAlertBarType: AlertTypes = AlertTypes.Warning
let isButtonProcessing: boolean = false
let formError: string = ''
const handleClick = () => {
// Reset the alert type if resubmitted
defaultAlertBarType = AlertTypes.Warning
// Update the state
isButtonProcessing = true
handleResendVerificationEmail((error) => {
formError = error
defaultAlertBarType = AlertTypes.Danger
isButtonProcessing = false
})
// Wait a bit after the success before showing the button again
setTimeout(() => {
isButtonProcessing = false
}, 5000)
}
</script>
{#if $isUserLoggedIn && !$isUserVerified}
<div class="container py-3">
<AlertBar alertType={defaultAlertBarType}>
<div class="d-flex flex-wrap align-items-center justify-content-center gap-3">
<i class="bi bi-envelope-exclamation" />
<div>Please verify your account by clicking the link in your email</div>
{#if isButtonProcessing}
<div class="success-icon">
<i class="bi bi-check-square" />
Sent!
</div>
{:else}
<button type="button" class="btn btn-outline-secondary" on:click={handleClick}
>Resend Email</button
>
{/if}
</div>
{#if formError}
<div class="border-top text-center mt-2 pt-2">{formError}</div>
{/if}
</AlertBar>
</div>
{/if}
<style>
.success-icon {
padding: 0.375rem 0.75rem;
}
</style>

View File

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

View File

@@ -0,0 +1,30 @@
<script>
import { PUBLIC_APP_DOMAIN, PUBLIC_APP_PROTOCOL } from '$src/env'
const baseUrl = `${PUBLIC_APP_PROTOCOL}://${PUBLIC_APP_DOMAIN}/`
const imageUrl = `${baseUrl}poster.png`
const tagline = `Get a PocketBase backend for your next app in under 10 seconds.`
</script>
<svelte:head>
<!-- HTML Meta Tags -->
<title>PocketHost</title>
<meta name="description" content={tagline} />
<link rel="manifest" href="/manifest.json" />
<!-- Facebook Meta Tags -->
<meta property="og:url" content={baseUrl} />
<meta property="og:type" content="website" />
<meta property="og:title" content="PocketHost" />
<meta property="og:description" content={tagline} />
<meta property="og:image" content={imageUrl} />
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content={PUBLIC_APP_DOMAIN} />
<meta property="twitter:url" content={baseUrl} />
<meta name="twitter:title" content="PocketHost" />
<meta name="twitter:description" content={tagline} />
<meta name="twitter:image" content={imageUrl} />
</svelte:head>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { PUBLIC_ROUTES } from '$src/env'
import { client } from '$src/pocketbase'
import { getRouter } from '$util/utilities'
import { logger } from '@pockethost/common'
import { onMount } from 'svelte'
onMount(() => {
const { isLoggedIn } = client()
if (isLoggedIn()) return
const router = getRouter()
const { pathname } = router
if (!PUBLIC_ROUTES.find((matcher) => matcher.match(pathname))) {
const { warn } = logger()
// Send user to the homepage
warn(`${pathname} is a private route`)
window.location.href = '/'
}
})
</script>

View File

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

View File

@@ -0,0 +1,13 @@
<script lang="ts">
export let disabled: boolean = false
export let style: 'primary' | 'warning' | 'danger' | 'success' = 'primary'
export let click: () => void = () => {}
</script>
<button
type="button"
class="btn btn-{style}"
{disabled}
style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
on:click={click}><slot /></button
>

View File

@@ -0,0 +1,50 @@
import { assertTruthy } from '@pockethost/common'
import { find } from '@s-libs/micro-dash'
import Cookies from 'js-cookie'
// Set some default values to be referenced later
export enum ThemeNames {
Light = 'light',
Dark = 'dark',
}
export const HLJS_THEMES = {
[ThemeNames.Light]:
'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css',
[ThemeNames.Dark]:
'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 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 html = () => {
const htmlElement = document.querySelector('html')
assertTruthy(htmlElement, `Expected <html> element to exist`)
return htmlElement
}
export const getCurrentTheme = () => {
const savedTheme = Cookies.get(STORAGE_NAME)
const currentTheme =
find(ALLOWED_THEMES, (v) => savedTheme === v) || DEFAULT_THEME
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)
}