Superadmin

This commit is contained in:
Ben Allfree 2023-12-13 23:25:11 -08:00
parent ed7d4aeb7d
commit 0527fd6972
85 changed files with 1791 additions and 35 deletions

View File

@ -28,34 +28,20 @@ jobs:
ls
ls dist
- name: Get changed lander files
id: changed-lander-files
uses: tj-actions/changed-files@v40.1.0
with:
files: |
frontends/lander/**
- name: Expose git commit data
uses: rlespinasse/git-commit-data-action@v1.5.0
- name: Get changed dashboard files
# ===================
# DASHBOARD
# ===================
- name: DASHBOARD - Get changed dashboard files
id: changed-dashboard-files
uses: tj-actions/changed-files@v40.1.0
with:
files: |
frontends/dashboard/**
- name: Publish lander to Cloudflare Pages
uses: cloudflare/pages-action@v1.5.0
if: steps.changed-lander-files.outputs.any_changed == 'true'
id: lander_deploy
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: pockethost
directory: ./dist/lander
branch: ${{ github.head_ref || github.ref_name }}
wranglerVersion: '3'
- name: Publish dashboard to Cloudflare Pages
- name: DASHBOARD - Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1.5.0
if: steps.changed-dashboard-files.outputs.any_changed == 'true'
id: dashboard_deploy
@ -68,10 +54,7 @@ jobs:
branch: ${{ github.head_ref || github.ref_name }}
wranglerVersion: '3'
- name: Expose git commit data
uses: rlespinasse/git-commit-data-action@v1.5.0
- name: Discord feature branch notification for dashboard
- name: DASHBOARD - Discord feature branch notification
if: github.ref_name != 'master' && steps.changed-dashboard-files.outputs.any_changed == 'true'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
@ -79,15 +62,7 @@ jobs:
with:
args: '**DASHBOARD PREVIEW** ${{ steps.dashboard_deploy.outputs.url }} was generated by ${{ github.actor }} in [${{ env.GIT_COMMIT_SHORT_SHA }}](https://github.com/${{ github.repository}}/commit/${{ github.sha }}) on [${{ github.repository}}#${{ github.ref_name }}](https://github.com/${{ github.repository}}/tree/${{ github.ref_name }}) with memo `${{ env.GIT_COMMIT_MESSAGE_SUBJECT }}`'
- name: Discord feature branch notification for lander
if: github.ref_name != 'master' && steps.changed-lander-files.outputs.any_changed == 'true'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**LANDER PREVIEW** ${{ steps.lander_deploy.outputs.url }} was generated by ${{ github.actor }} in [${{ env.GIT_COMMIT_SHORT_SHA }}](https://github.com/${{ github.repository}}/commit/${{ github.sha }}) on [${{ github.repository}}#${{ github.ref_name }}](https://github.com/${{ github.repository}}/tree/${{ github.ref_name }}) with memo `${{ env.GIT_COMMIT_MESSAGE_SUBJECT }}`'
- name: Discord live branch notification for dashboard
- name: DASHBOARD - Discord live branch notification
if: github.ref_name == 'master' && steps.changed-dashboard-files.outputs.any_changed == 'true'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
@ -95,10 +70,80 @@ jobs:
with:
args: '**DASHBOARD LIVE** https://app.pockethost.io ([permalink](${{ steps.dashboard_deploy.outputs.url }})) was updated by ${{ github.actor }} in [${{ env.GIT_COMMIT_SHORT_SHA }}](https://github.com/${{ github.repository}}/commit/${{ github.sha }}) on [${{ github.repository}}#${{ github.ref_name }}](https://github.com/${{ github.repository}}/tree/${{ github.ref_name }}): `${{ env.GIT_COMMIT_MESSAGE_SUBJECT }}`'
- name: Discord live branch notification for lander
# ===================
# LANDER
# ===================
- name: LANDER - Get changed files
id: changed-lander-files
uses: tj-actions/changed-files@v40.1.0
with:
files: |
frontends/lander/**
- name: LANDER - Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1.5.0
if: steps.changed-lander-files.outputs.any_changed == 'true'
id: lander_deploy
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: pockethost-lander
directory: ./dist/lander
branch: ${{ github.head_ref || github.ref_name }}
wranglerVersion: '3'
- name: LANDER - Discord feature branch notification
if: github.ref_name != 'master' && steps.changed-lander-files.outputs.any_changed == 'true'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**LANDER PREVIEW** ${{ steps.lander_deploy.outputs.url }} was generated by ${{ github.actor }} in [${{ env.GIT_COMMIT_SHORT_SHA }}](https://github.com/${{ github.repository}}/commit/${{ github.sha }}) on [${{ github.repository}}#${{ github.ref_name }}](https://github.com/${{ github.repository}}/tree/${{ github.ref_name }}) with memo `${{ env.GIT_COMMIT_MESSAGE_SUBJECT }}`'
- name: LANDER - Discord live branch notification
if: github.ref_name == 'master' && steps.changed-lander-files.outputs.any_changed == 'true'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**LANDER LIVE** https://pockethost.io ([permalink](${{ steps.lander_deploy.outputs.url }})) was updated by ${{ github.actor }} in [${{ env.GIT_COMMIT_SHORT_SHA }}](https://github.com/${{ github.repository}}/commit/${{ github.sha }}) on [${{ github.repository}}#${{ github.ref_name }}](https://github.com/${{ github.repository}}/tree/${{ github.ref_name }}): `${{ env.GIT_COMMIT_MESSAGE_SUBJECT }}`'
# ===================
# SUPERADMIN
# ===================
- name: SUPERADMIN - Get changed files
id: changed-superadmin-files
uses: tj-actions/changed-files@v40.1.0
with:
files: |
frontends/superadmin/**
- name: SUPERADMIN - Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1.5.0
if: steps.changed-superadmin-files.outputs.any_changed == 'true'
id: superadmin_deploy
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: pockethost-superadmin
directory: ./dist/superadmin
branch: ${{ github.head_ref || github.ref_name }}
wranglerVersion: '3'
- name: SUPERADMIN - Discord feature branch notification
if: github.ref_name != 'master' && steps.changed-superadmin-files.outputs.any_changed == 'true'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**SUPERADMIN PREVIEW** ${{ steps.superadmin_deploy.outputs.url }} was generated by ${{ github.actor }} in [${{ env.GIT_COMMIT_SHORT_SHA }}](https://github.com/${{ github.repository}}/commit/${{ github.sha }}) on [${{ github.repository}}#${{ github.ref_name }}](https://github.com/${{ github.repository}}/tree/${{ github.ref_name }}) with memo `${{ env.GIT_COMMIT_MESSAGE_SUBJECT }}`'
- name: SUPERADMIN - Discord live branch notification
if: github.ref_name == 'master' && steps.changed-superadmin-files.outputs.any_changed == 'true'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**SUPERADMIN LIVE** https://superadmin.pockethost.io ([permalink](${{ steps.superadmin_deploy.outputs.url }})) was updated by ${{ github.actor }} in [${{ env.GIT_COMMIT_SHORT_SHA }}](https://github.com/${{ github.repository}}/commit/${{ github.sha }}) on [${{ github.repository}}#${{ github.ref_name }}](https://github.com/${{ github.repository}}/tree/${{ github.ref_name }}): `${{ env.GIT_COMMIT_MESSAGE_SUBJECT }}`'

View File

@ -0,0 +1,5 @@
PUBLIC_APP_URL=https://app.pockethost.io
PUBLIC_APEX_DOMAIN=pockethost.io
PUBLIC_BLOG_URL=https://pockethost.io
PUBLIC_HTTP_PROTOCOL=https:
PUBLIC_MOTHERSHIP_URL=https://pockethost-central.pockethost.io

7
frontends/superadmin/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
dist-server
.idea

View File

@ -0,0 +1 @@
engine-strict=true

View File

@ -0,0 +1,20 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
# Source files
/src/assets/_bootstrap.css
dist-server
# Ignore the static files
static

View File

@ -0,0 +1,41 @@
# PocketHost UI
## Built with SvelteKit and Typescript
Description about PocketHost goes here!
## Developing Locally
To run this project, navigate to the `/frontends/dashboard` folder and run `pnpm 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!
## Routing
There is a file called `public-routes.json` that controls which URLs are accessible to non-authenticated users. Any public facing page needs to have its URL added to this list. Otherwise, the authentication system will kick in and send them to the homepage.
## User Management
This app uses [Svelte Stores](https://svelte.dev/docs#run-time-svelte-store) to track the user's information. At the top is the `globalUserData` store. This contains everything about the user that comes from Pocketbase, including their JWT Token.
### Derived User Values
There are additional derived values that are useful for showing and hiding components across the site. The first one is `isUserLoggedIn`. This one will return a true or false depending on the state of the logged-in user. It is dependent on the `email` property in the Pocketbase response.
The second derived value is `isUserVerified`. This will return a true or false boolean depending on if their Pocketbase account has been verified via the email they got when they initially registered.
An example of showing or hiding components can be found with the `<VerifyAccountBar>` component, that prompts the user to make sure to verify their account before continuing. The code looks roughly like this:
```svelte
<script>
import { isUserLoggedIn, isUserVerified } from '$util/stores'
</script>
{#if $isUserLoggedIn && !$isUserVerified}
<YourComponentHere />
{/if}
```
This particular example will only render the component if the user is logged in, and their account **has not** been verified. Notice the `$` symbol as well, this is required for [Svelte Stores](https://svelte.dev/docs#run-time-svelte-store) when using Store values in the UI.
If you need to use these values in a normal javascript/typescript file instead, you can utilize [Svelte's `get()` method](https://svelte.dev/docs#run-time-svelte-store-get) instead.

View File

@ -0,0 +1,8 @@
import hljs from 'highlight.js'
const highlight = (code, lang) => {
lang = lang && hljs.getLanguage(lang) ? lang : 'plaintext'
return hljs.highlight(code, { language: lang }).value
}
export default { highlight }

View File

@ -0,0 +1,43 @@
{
"name": "@pockethost/superadmin",
"version": "0.0.1",
"private": true,
"main": "./src/app.html",
"scripts": {
"check:types": "svelte-check",
"preview": "npx http-server@latest ./build -P \"http://localhost:8080?\"",
"dev": "vite dev --force --host=0.0.0.0",
"build": "NODE_ENV=production vite build",
"lint": "prettier --check .",
"format": "prettier --write ."
},
"type": "module",
"devDependencies": {
"@microsoft/fetch-event-source": "https://github.com/pockethost/fetch-event-source.git#ebe3b7122647b48b93fd11effbbfb915d98956b0",
"@s-libs/micro-dash": "^16.1.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.25.2",
"@tailwindcss/typography": "^0.5.10",
"@types/bootstrap": "^5.2.6",
"@types/d3-scale": "^4.0.3",
"@types/d3-scale-chromatic": "^3.0.0",
"@types/js-cookie": "^3.0.2",
"autoprefixer": "^10.4.16",
"boolean": "^3.2.0",
"chart.js": "4.4.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",
"pocketbase": "^0.19.0",
"sass": "^1.68.0",
"svelte": "^4.2.1",
"svelte-chartjs": "3.1.2",
"svelte-check": "^3.5.2",
"svelte-highlight": "^7.3.0",
"svelte-preprocess": "^5.0.4",
"tailwindcss": "^3.3.3",
"vite": "^4.4.9"
}
}

View File

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

View File

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

View File

@ -0,0 +1,25 @@
<!doctype html>
<!--suppress ALL -->
<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, initial-scale=1" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-dark.min.css"
id="hljs-link"
/>
<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>
</body>
</html>

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { slide } from 'svelte/transition'
// https://daisyui.com/components/alert/
type AlertTypes = 'default' | 'info' | 'success' | 'warning' | 'error'
export let message: string = ''
export let type: AlertTypes
export let additionalClasses: string = ''
// Set up the default alert classes and icon
let alertTypeClass = ''
let alertTypeIcon = `<i class="fa-regular fa-circle-info"></i>`
if (type === 'default') {
alertTypeClass = ''
alertTypeIcon = `<i class="fa-regular fa-circle-info"></i>`
}
if (type === 'info') {
alertTypeClass = 'alert-info'
alertTypeIcon = `<i class="fa-regular fa-circle-info"></i>`
}
if (type === 'success') {
alertTypeClass = 'alert-success'
alertTypeIcon = `<i class="fa-regular fa-circle-check"></i>`
}
if (type === 'warning') {
alertTypeClass = 'alert-warning'
alertTypeIcon = `<i class="fa-regular fa-triangle-exclamation"></i>`
}
if (type === 'error') {
alertTypeClass = 'alert-error'
alertTypeIcon = `<i class="fa-regular fa-circle-xmark"></i>`
}
</script>
{#if message}
<div
class="alert mb-4 {alertTypeClass} {additionalClasses} justify-center"
transition:slide
role="alert"
>
{@html alertTypeIcon}
<span>{message}</span>
</div>
{/if}

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}>{text}</textarea>
<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,41 @@
<script lang="ts">
import CopyButton from '$components/CopyButton.svelte'
import { Highlight } from 'svelte-highlight'
import { typescript, type LanguageType } from 'svelte-highlight/languages'
export let code: string
export let language: LanguageType<'typescript' | 'bash'> = typescript
const handleCopy = () => {}
</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-radius: 16px;
overflow: hidden;
.copy-button {
transition: all 300ms;
opacity: 0;
position: absolute;
top: 8px;
right: 8px;
}
&:hover {
.copy-button {
opacity: 1;
}
}
}
</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,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'}">
Superadmin
</h1>
</div>

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,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="/" class="flex gap-2 items-center justify-center">
<Logo hideLogoText={true} logoWidth="w-16" />
</a>
<label for="mobile-nav-drawer" class="btn drawer-button">
<i class="fa-regular fa-bars text-2xl"></i>
</label>
</div>
</div>
<div class="drawer-side z-50">
<label
for="mobile-nav-drawer"
aria-label="close sidebar"
class="drawer-overlay"
></label>
<div class="bg-base-100 w-80 min-h-full">
<slot />
</div>
</div>
</div>

View File

@ -0,0 +1,75 @@
<script lang="ts">
import { page } from '$app/stores'
import Logo from '$components/Logo.svelte'
import MediaQuery from '$components/MediaQuery.svelte'
import { client } from '$src/pocketbase-client'
import { globalInstancesStore } from '$util/stores'
import { values } from '@s-libs/micro-dash'
import UserLoggedIn from './helpers/UserLoggedIn.svelte'
type TypeInstanceObject = {
id: string
subdomain: string
maintenance: boolean
}
let arrayOfActiveInstances: TypeInstanceObject[] = []
$: {
if ($globalInstancesStore) {
arrayOfActiveInstances = values($globalInstancesStore).filter(
(app) => !app.maintenance,
)
}
}
// Log the user out and redirect them to the homepage
const handleLogoutAndRedirect = async () => {
const { logOut } = client()
// Clear out the pocketbase information about the current user
logOut()
// Hard refresh to make sure any remaining data is cleared
window.location.href = '/'
}
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<HTMLElement>('.drawer-overlay')?.click()
}
</script>
<aside class="p-4 min-w-[250px] max-w-[250px] flex flex-col">
<MediaQuery query="(min-width: 1280px)" let:matches>
{#if matches}
<a href="/" class="flex gap-2 items-center justify-center">
<Logo hideLogoText={true} logoWidth="w-20" />
</a>
{/if}
</MediaQuery>
<div class="flex flex-col gap-2 mb-auto">
<a on:click={handleClick} href="/" class={linkClasses}>
<i
class="fa-regular fa-table-columns {$page.url.pathname === '/' &&
'text-primary'}"
></i> Dashboard
</a>
<UserLoggedIn>
<button
type="button"
class={linkClasses}
on:click={handleLogoutAndRedirect}
><i class="fa-regular fa-arrow-up-left-from-circle"></i> Logout</button
>
</UserLoggedIn>
</div>
</aside>

View File

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

View File

@ -0,0 +1,24 @@
<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"
target="_blank"
>Full documentation <i
class="fa-regular fa-arrow-up-right-from-square opacity-50 text-sm"
></i></a
>
</div>
{/if}
{#if !documentation}
<h3 class="text-xl font-bold mb-4">
<slot />
</h3>
{/if}

View File

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

View File

@ -0,0 +1,30 @@
<script>
import { APP_URL } from '$src/env'
const baseUrl = APP_URL()
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={APP_URL()} />
<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,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} btn-sm"
{disabled}
style="--bs-btn-padding-y: .05rem; --bs-btn-padding-x: .5rem; --bs-btn-font-size: .75rem;"
on:click={click}><slot /></button
>

View File

@ -0,0 +1,14 @@
<script lang="ts">
import { isAuthStateInitialized, isUserLoggedIn } from '$util/stores'
export let redirect = false
$: {
if ($isAuthStateInitialized && redirect && !$isUserLoggedIn) {
window.location.href = '/'
}
}
</script>
{#if $isUserLoggedIn}
<slot />
{/if}

View File

@ -0,0 +1,7 @@
<script lang="ts">
import { isUserLoggedIn } from '$util/stores'
</script>
{#if !$isUserLoggedIn}
<slot />
{/if}

View File

@ -0,0 +1,7 @@
import { assertTruthy } from '$shared'
export const html = () => {
const htmlElement = document.querySelector('html')
assertTruthy(htmlElement, `Expected <html> element to exist`)
return htmlElement
}

View File

@ -0,0 +1,87 @@
<script lang="ts">
import AlertBar from '$components/AlertBar.svelte'
import { client } from '$src/pocketbase-client'
const { authViaEmail } = client()
// 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
// Handle the form submission
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault()
isFormButtonDisabled = true
isButtonLoading = true
formError = ''
try {
await authViaEmail(email, password)
} catch (error) {
const e = error as Error
formError = `Something went wrong with logging you in. ${e.message}`
}
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="current-password"
bind:value={password}
required
/>
</div>
<AlertBar message={formError} type="error" />
<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>

View File

@ -0,0 +1,114 @@
import { boolean } from 'boolean'
/**
* These environment variables default to pointing to the production build so frontend development is easy.
* If they are specified in .env, those values will prevail.
*/
// The domain name where this dashboard lives
export const PUBLIC_APP_URL =
import.meta.env.PUBLIC_APP_URL || 'https://app.pockethost.io'
// The apex domain of this whole operation. Also known as the "app" or "dashboard"
export const PUBLIC_APEX_DOMAIN =
import.meta.env.PUBLIC_APEX_DOMAIN || `pockethost.io`
// The domain name of the lander/marketing site
export const PUBLIC_BLOG_URL =
import.meta.env.PUBLIC_BLOG_URL || 'https://pockethost.io'
// The protocol to use, almost always will be https
export const PUBLIC_HTTP_PROTOCOL =
import.meta.env.PUBLIC_HTTP_PROTOCOL || `https:`
// The complete URL to the mothership
export const PUBLIC_MOTHERSHIP_URL =
import.meta.env.PUBLIC_MOTHERSHIP_URL ||
`https://pockethost-central.pockethost.io`
// Whether we are in debugging mode - default TRUE
export const PUBLIC_DEBUG = boolean(import.meta.env.PUBLIC_DEBUG || 'true')
/**
* This helper function will take a dynamic list of values and join them together with a slash.
* @param {Array<string>} paths This is an optional list of additional paths to append to the lander URL.
* @example
* mkPath('a', 'b', 'c') // a/b/c
*/
const mkPath = (...paths: string[]) => {
return paths.filter((v) => !!v).join('/')
}
/**
* Helpful alias for the lander url.
* @param {Array<string>} paths This is an optional list of additional paths to append to the lander URL.
* @example
* LANDER_URL() // https://pockethost.io/
* LANDER_URL('showcase') // https://pockethost.io/showcase/
*/
export const LANDER_URL = (...paths: string[]) => {
return `${PUBLIC_BLOG_URL}/${mkPath(...paths)}/`
}
/**
* Helpful alias for the blog url.
* @param {Array<string>} paths This is an optional list of additional paths to append to the blogs URL.
* @example
* BLOG_URL() // https://pockethost.io/blog
* BLOG_URL('new-features-2023') // https://pockethost.io/blog/new-features-2023/
*/
export const BLOG_URL = (...paths: string[]) => {
return LANDER_URL(`blog`, ...paths)
}
/**
* Helpful alias for the docs url.
* @param {Array<string>} paths This is an optional list of additional paths to append to the docs URL.
* @example
* DOCS_URL() // https://pockethost.io/docs
* DOCS_URL('overview', 'help') // https://pockethost.io/docs/overview/help/
*/
export const DOCS_URL = (...paths: string[]) => {
return LANDER_URL(`docs`, ...paths)
}
/**
* Helpful alias for the app url.
* @param {Array<string>} paths This is an optional list of additional paths to append to the app URL.
* @example
* APP_URL() // https://app.pockethost.io/
* APP_URL('dashboard') // https://app.pockethost.io/dashboard
*/
export const APP_URL = (...paths: string[]) => {
return `${PUBLIC_APP_URL}/${mkPath(...paths)}`
}
/**
* Helpful alias for generating the URL for a specific instance
* @param {string} name This is the unique instance name
* @param {Array<string>} paths This is an optional list of additional paths to append to the instance URL.
* @example
* INSTANCE_URL('my-cool-instance') // https://my-cool-instance.pockethost.io/
* INSTANCE_URL('my-cool-instance', 'dashboard') // https://my-cool-instance.pockethost.io/dashboard
*/
export const INSTANCE_URL = (name: string, ...paths: string[]) => {
return `${PUBLIC_HTTP_PROTOCOL}//${name}.${PUBLIC_APEX_DOMAIN}/${mkPath(
...paths,
)}`
}
/**
* Helpful alias for generating the URL for a specific instance's admin panel
* @param {string} name This is the unique instance name
* @example
* INSTANCE_ADMIN_URL('my-cool-instance') // https://my-cool-instance.pockethost.io/_/
*/
export const INSTANCE_ADMIN_URL = (name: string) => {
return INSTANCE_URL(name, `_/`)
}
export const FTP_URL = (email: string) => {
return `"${email}"@ftp.sfo-1.${PUBLIC_APEX_DOMAIN}`
}
export const DISCORD_URL = `https://discord.gg/HsSjcuPRWX`

View File

@ -0,0 +1,164 @@
import { createGenericSyncEvent } from '$util/events'
import { keys, map } from '@s-libs/micro-dash'
import PocketBase, {
BaseAuthStore,
ClientResponseError,
type AuthModel,
} from 'pocketbase'
export type AuthToken = string
export type AuthStoreProps = {
token: AuthToken
model: AuthModel | null
isValid: boolean
}
export type PocketbaseClientConfig = {
url: string
}
export type PocketbaseClient = ReturnType<typeof createPocketbaseClient>
export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const { url } = config
const client = new PocketBase(url)
const { authStore } = client
const user = () => authStore.model as AuthStoreProps['model']
const isLoggedIn = () => authStore.isValid
const logOut = () => authStore.clear()
/**
* This will let a user confirm their new account via a token in their email
* @param token {string} The token from the verification email
*/
const confirmVerification = async (token: string) => {
return await client.collection('users').confirmVerification(token)
}
/**
* This will reset an unauthenticated user's password by sending a verification link to their email, and includes an optional error handler
* @param email {string} The email of the user
*/
const requestPasswordReset = async (email: string) => {
return await client.collection('users').requestPasswordReset(email)
}
/**
* This will let an unauthenticated user save a new password after verifying their email
* @param token {string} The token from the verification email
* @param password {string} The new password of the user
*/
const requestPasswordResetConfirm = async (
token: string,
password: string,
) => {
return await client
.collection('users')
.confirmPasswordReset(token, password, password)
}
/**
* This will log a user into Pocketbase, and includes an optional error handler
* @param {string} email The email of the user
* @param {string} password The password of the user
*/
const authViaEmail = async (email: string, password: string) => {
return await client.admins.authWithPassword(email, password)
}
const refreshAuthToken = () => client.admins.authRefresh()
const parseError = (e: Error): string[] => {
if (!(e instanceof ClientResponseError)) return [e.message]
if (e.data.message && keys(e.data.data).length === 0)
return [e.data.message]
return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter(
(v) => !!v,
)
}
const getAuthStoreProps = (): AuthStoreProps => {
const { isAdmin, model, token, isValid } = client.authStore
if (isAdmin) throw new Error(`Admin models not supported`)
if (model && !model.email)
throw new Error(`Expected model to be a user here`)
return {
token,
model,
isValid,
}
}
/**
* Use synthetic event for authStore changers, so we can broadcast just
* the props we want and not the actual authStore object.
*/
const [onAuthChange, fireAuthChange] = createGenericSyncEvent<BaseAuthStore>()
/**
* This section is for initialization
*/
{
/**
* Listen for native authStore changes and convert to synthetic event
*/
client.authStore.onChange(() => {
fireAuthChange(client.authStore)
})
/**
* Refresh the auth token immediately upon creating the client. The auth token may be
* out of date, or fields in the user record may have changed in the backend.
*/
refreshAuthToken()
.catch((error) => {
client.authStore.clear()
})
.finally(() => {
fireAuthChange(client.authStore)
})
/**
* Listen for auth state changes and subscribe to realtime _user events.
* This way, when the verified flag is flipped, it will appear that the
* authstore model is updated.
*
* Polling is a stopgap til v.0.8. Once 0.8 comes along, we can do a realtime
* watch on the user record and update auth accordingly.
*/
const unsub = onAuthChange((authStore) => {
const { model, isAdmin } = authStore
if (!model) return
if (isAdmin) return
if (model.verified) {
unsub()
return
}
setTimeout(refreshAuthToken, 1000)
// TODO - THIS DOES NOT WORK, WE HAVE TO POLL INSTEAD. FIX IN V0.8
// unsub = subscribe<User>(`users/${model.id}`, (user) => {
// fireAuthChange({ ...authStore, model: user })
// })
})
}
return {
client,
getAuthStoreProps,
parseError,
authViaEmail,
requestPasswordReset,
requestPasswordResetConfirm,
confirmVerification,
logOut,
onAuthChange,
isLoggedIn,
user,
}
}

View File

@ -0,0 +1,16 @@
import { PUBLIC_MOTHERSHIP_URL } from '$src/env'
import {
createPocketbaseClient,
type PocketbaseClient,
} from './PocketbaseClient'
export const client = (() => {
let clientInstance: PocketbaseClient | undefined
return () => {
if (clientInstance) return clientInstance
const url = PUBLIC_MOTHERSHIP_URL
clientInstance = createPocketbaseClient({ url })
return clientInstance
}
})()

View File

@ -0,0 +1,30 @@
<script>
import { page } from '$app/stores'
</script>
<svelte:head>
<title>{$page.status} - PocketHost</title>
</svelte:head>
<div class="container text-center py-4">
<h1 class="mb-5">{$page.status}: {$page.error?.message}</h1>
<div class="gif mb-5">
<img
src="https://media4.giphy.com/media/V9sdMLcmIFqqk/giphy.gif?cid=790b76118f409453704f5eaabaea1a3dc7380a9daf4fca63&rid=giphy.gif&ct=g"
alt="Scene from a movie talking about something is missing"
class="img-fluid w-100"
/>
</div>
<a href="/" class="btn btn-light"
><i class="bi bi-arrow-left-short" /> Back to Home</a
>
</div>
<style>
.gif {
max-width: 600px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,33 @@
<script>
import MediaQuery from '$components/MediaQuery.svelte'
import MobileNavDrawer from '$components/MobileNavDrawer.svelte'
import Navbar from '$components/Navbar.svelte'
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import Meta from '$components/helpers/Meta.svelte'
import UserLoggedIn from '$components/helpers/UserLoggedIn.svelte'
import '../app.css'
</script>
<Meta />
<AuthStateGuard>
<div class="layout xl:flex">
<UserLoggedIn>
<MediaQuery query="(min-width: 1280px)" let:matches>
{#if matches}
<Navbar />
{:else}
<MobileNavDrawer>
<Navbar />
</MobileNavDrawer>
{/if}
</MediaQuery>
</UserLoggedIn>
<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] lg:p-4 rounded-2xl">
<slot />
</div>
</div>
</div>
</AuthStateGuard>

View File

@ -0,0 +1,2 @@
const ssr = false
export { ssr }

View File

@ -0,0 +1,26 @@
<script lang="ts">
import Logo from '$components/Logo.svelte'
import UserLoggedIn from '$components/helpers/UserLoggedIn.svelte'
import UserLoggedOut from '$components/helpers/UserLoggedOut.svelte'
import LoginForm from '$components/login-register/LoginForm.svelte'
import Dashboard from './dashboard/Dashboard.svelte'
</script>
<svelte:head>
<title>Home - PocketHost</title>
</svelte:head>
<div>
<UserLoggedIn>
<Dashboard />
</UserLoggedIn>
<UserLoggedOut>
<div class="min-h-screen flex items-center justify-center">
<div>
<Logo />
<LoginForm />
</div>
</div>
</UserLoggedOut>
</div>

View File

@ -0,0 +1,7 @@
<script>
import UserLoggedIn from '$components/helpers/UserLoggedIn.svelte'
</script>
<UserLoggedIn redirect>
<slot />
</UserLoggedIn>

View File

@ -0,0 +1,42 @@
<script lang="ts">
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
import { client } from '$src/pocketbase-client'
import { onMount } from 'svelte'
import { writable } from 'svelte/store'
type Plugin = {
slug: string
name: string
version: string
migration: number
enabled: boolean
}
const plugins = writable<Plugin[]>([])
const stats = writable({})
onMount(async () => {
plugins.set(await client().client.collection('plugins').getFullList())
stats.set(
((await client().client.collection('stats').getFullList()) || []).pop() ||
{},
)
})
</script>
<svelte:head>
<title>Dashboard - PocketHost</title>
</svelte:head>
<AuthStateGuard>
<div>
{#each Object.entries($stats) as [key, idx]}
<div>{key}: {idx}</div>
{/each}
</div>
<div>
{#each $plugins as plugin}
<div>{plugin.name}</div>
{/each}
</div>
</AuthStateGuard>

View File

@ -0,0 +1,87 @@
<script lang="ts">
import { client } from '$src/pocketbase-client'
import { slide } from 'svelte/transition'
const { authViaEmail } = client()
let email: string = ''
let password: string = ''
let formError: string = ''
let isFormButtonDisabled: boolean = true
$: isFormButtonDisabled = email.length === 0 || password.length === 0
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault()
isFormButtonDisabled = true
try {
await authViaEmail(email, password)
} catch (error) {
const e = error as Error
formError = `Something has gone wrong with logging in. ${e.message}`
}
isFormButtonDisabled = false
}
</script>
<svelte:head>
<title>Sign In - PocketHost</title>
</svelte:head>
<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-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}
<div transition:slide class="alert alert-error mb-5">
<i class="fa-solid fa-circle-exclamation"></i>
<span>{formError}</span>
</div>
{/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>
</div>
</div>

View File

@ -0,0 +1,12 @@
import { LoggerService, LogLevelName } from '$shared'
import { PUBLIC_DEBUG } from './env'
// Initiate the logging service
// TODO: Document this
try {
LoggerService()
} catch {
LoggerService({
level: PUBLIC_DEBUG ? LogLevelName.Debug : LogLevelName.Info,
})
}

View File

@ -0,0 +1,15 @@
import { createCleanupManager } from '$shared'
import { onDestroy } from 'svelte'
// TODO: Document this more
// This is used to queue up functions and then destroy them once completed
// Currently being used on the Logging.svelte file to better handle the Real time subscription from Pocketbase.
export const mkCleanup = () => {
const cm = createCleanupManager()
onDestroy(() => cm.shutdown().catch(console.error))
return (cb: () => any) => {
cm.add(cb)
}
}

View File

@ -0,0 +1,14 @@
import { client } from '$src/pocketbase-client'
export type FormErrorHandler = (value: string) => void
export const handleFormError = (e: Error, setError?: FormErrorHandler) => {
const { parseError } = client()
if (setError) {
const message = parseError(e)[0]
setError(message || 'Unknown message')
} else {
throw e
}
}

View File

@ -0,0 +1,24 @@
import { forEach } from '@s-libs/micro-dash'
export type Unsubscribe = () => void
export const createGenericSyncEvent = <TPayload>(): [
(cb: (payload: TPayload) => void) => Unsubscribe,
(payload: TPayload) => void,
] => {
let i = 0
const callbacks: any = {}
const onEvent = (cb: (payload: TPayload) => void) => {
const id = i++
callbacks[id] = cb
return () => {
delete callbacks[id]
}
}
const fireEvent = (payload: TPayload) => {
forEach(callbacks, (cb) => cb(payload))
}
return [onEvent, fireEvent]
}

View File

@ -0,0 +1,26 @@
import { type InstanceFields, type InstanceId } from '$shared'
import { client } from '$src/pocketbase-client'
import { writable } from 'svelte/store'
// TODO: Removing this will cause the app to crash
// Theres a reference inside of `createPocketbaseClient.ts` that needs the information that comes from this file
import '../services'
const { onAuthChange } = client()
export const isUserLoggedIn = writable(false)
export const isAuthStateInitialized = writable(false)
/**
* Listen for auth change events. When we get at least one, the auth state is initialized.
*/
onAuthChange((authStoreProps) => {
isUserLoggedIn.set(authStoreProps.isValid)
isAuthStateInitialized.set(true)
})
// This holds an array of all the user's instances and their data
export const globalInstancesStore = writable<{
[_: InstanceId]: InstanceFields
}>({})
export const globalInstancesStoreReady = writable(false)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,9 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="25.536" y="13.4861" width="1.71467" height="16.7338" transform="rotate(45.9772 25.536 13.4861)" fill="white"/>
<path d="M26 14H36.8C37.4628 14 38 14.5373 38 15.2V36.8C38 37.4628 37.4628 38 36.8 38H15.2C14.5373 38 14 37.4628 14 36.8V26" fill="white"/>
<path d="M26 14H36.8C37.4628 14 38 14.5373 38 15.2V36.8C38 37.4628 37.4628 38 36.8 38H15.2C14.5373 38 14 37.4628 14 36.8V26" stroke="#16161a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M26 14V3.2C26 2.53726 25.4628 2 24.8 2H3.2C2.53726 2 2 2.53726 2 3.2V24.8C2 25.4628 2.53726 26 3.2 26H14" fill="white"/>
<path d="M26 14V3.2C26 2.53726 25.4628 2 24.8 2H3.2C2.53726 2 2 2.53726 2 3.2V24.8C2 25.4628 2.53726 26 3.2 26H14" stroke="#16161a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 20C9.44772 20 9 19.5523 9 19V8C9 7.44772 9.44772 7 10 7H13.7531C14.4801 7 15.1591 7.07311 15.7901 7.21932C16.4348 7.35225 16.9904 7.58487 17.4568 7.91718C17.9369 8.2362 18.3141 8.6682 18.5885 9.21319C18.8628 9.74489 19 10.4029 19 11.1871C19 11.9448 18.856 12.6028 18.5679 13.161C18.2936 13.7193 17.9163 14.1779 17.4362 14.5368C16.9561 14.8957 16.4005 15.1616 15.7695 15.3344C15.1385 15.5072 14.4664 15.5936 13.7531 15.5936H13.0247C12.4724 15.5936 12.0247 16.0413 12.0247 16.5936V19C12.0247 19.5523 11.577 20 11.0247 20H10ZM12.0247 12.2607C12.0247 12.813 12.4724 13.2607 13.0247 13.2607H13.5679C15.214 13.2607 16.037 12.5695 16.037 11.1871C16.037 10.5092 15.8244 10.0307 15.3992 9.75153C14.9877 9.47239 14.3772 9.33282 13.5679 9.33282H13.0247C12.4724 9.33282 12.0247 9.78054 12.0247 10.3328V12.2607Z" fill="#16161a"/>
<path d="M22 33C21.4477 33 21 32.5523 21 32V21C21 20.4477 21.4477 20 22 20H25.4877C26.1844 20 26.8265 20.0532 27.4139 20.1595C28.015 20.2526 28.5342 20.4254 28.9713 20.6779C29.4085 20.9305 29.75 21.2628 29.9959 21.6748C30.2555 22.0869 30.3852 22.6053 30.3852 23.2301C30.3852 23.5225 30.3374 23.8149 30.2418 24.1074C30.1598 24.3998 30.0232 24.6723 29.832 24.9248C29.6407 25.1774 29.4016 25.4034 29.1148 25.6028C28.837 25.7958 28.5081 25.939 28.1279 26.0323C28.1058 26.0378 28.0902 26.0575 28.0902 26.0802V26.0802C28.0902 26.1039 28.1073 26.1242 28.1306 26.1286C29.0669 26.3034 29.7774 26.6332 30.2623 27.1181C30.7541 27.6099 31 28.2945 31 29.1718C31 29.8364 30.8702 30.408 30.6107 30.8865C30.3511 31.365 29.9891 31.7638 29.5246 32.0828C29.0601 32.3885 28.5137 32.6212 27.8852 32.7807C27.2705 32.9269 26.6011 33 25.8771 33H22ZM24.0123 24.2239C24.0123 24.7762 24.46 25.2239 25.0123 25.2239H25.3443C26.082 25.2239 26.6148 25.0844 26.9426 24.8052C27.2705 24.5261 27.4344 24.1339 27.4344 23.6288C27.4344 23.1503 27.2637 22.8113 26.9221 22.612C26.5943 22.3993 26.0751 22.2929 25.3648 22.2929H25.0123C24.46 22.2929 24.0123 22.7407 24.0123 23.2929V24.2239ZM24.0123 29.7071C24.0123 30.2593 24.46 30.7071 25.0123 30.7071H25.6311C27.2432 30.7071 28.0492 30.1222 28.0492 28.9525C28.0492 28.3809 27.8511 27.9688 27.4549 27.7163C27.0724 27.4637 26.4645 27.3374 25.6311 27.3374H25.0123C24.46 27.3374 24.0123 27.7851 24.0123 28.3374V29.7071Z" fill="#16161a"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,36 @@
{
"name": "PocketHost",
"short_name": "PocketHost",
"description": "Get a pocketbase backend for your next app is as fast as signing up.",
"theme_color": "#222222",
"background_color": "#222222",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "/icons/icon-512x512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
},
{
"src": "/icons/icon-384x384.png",
"type": "image/png",
"sizes": "384x384",
"purpose": "maskable"
},
{
"src": "/icons/icon-256x256.png",
"type": "image/png",
"sizes": "256x256",
"purpose": "maskable"
},
{
"src": "/icons/icon-192x192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "any"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,24 @@
import adapter from '@sveltejs/adapter-static'
import preprocess from 'svelte-preprocess'
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
adapter: adapter({
pages: '../../dist/superadmin',
fallback: 'index.html',
}),
alias: {
$components: './src/components',
$util: './src/util',
$src: './src',
$shared: '../../src/shared',
},
},
target: '#svelte',
}
export default config

View File

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

View File

@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"preserveValueImports": false,
"importsNotUsedAsValues": "preserve",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@ -0,0 +1,14 @@
import { sveltekit } from '@sveltejs/kit/vite'
import type { UserConfig } from 'vite'
const isProd = process.env.NODE_ENV === 'production'
const config: UserConfig = {
plugins: [sveltekit()],
optimizeDeps: {
include: ['highlight.js', 'highlight.js/lib/core'],
},
envPrefix: 'PUBLIC_',
envDir: isProd ? '.' : undefined,
}
export default config

View File

@ -14,6 +14,7 @@
"build-frontends": "concurrently 'pnpm:build:frontend:*'",
"build:frontend:dashboard": "cd frontends/dashboard && pnpm build",
"build:frontend:lander": "cd frontends/lander && pnpm build",
"build:frontend:superadmin": "cd frontends/superadmin && pnpm build",
"dev": "concurrently 'pnpm:dev:*'",
"dev-daemon": "concurrently 'pnpm:dev:daemon:*'",
"dev:lander": "cd frontends/lander && pnpm start",

81
pnpm-lock.yaml generated
View File

@ -341,6 +341,87 @@ importers:
specifier: ^3.0.3
version: 3.0.3
frontends/superadmin:
devDependencies:
'@microsoft/fetch-event-source':
specifier: https://github.com/pockethost/fetch-event-source.git#ebe3b7122647b48b93fd11effbbfb915d98956b0
version: github.com/pockethost/fetch-event-source/ebe3b7122647b48b93fd11effbbfb915d98956b0
'@s-libs/micro-dash':
specifier: ^16.1.0
version: 16.1.0
'@sveltejs/adapter-static':
specifier: ^2.0.3
version: 2.0.3(@sveltejs/kit@1.27.2)
'@sveltejs/kit':
specifier: ^1.25.2
version: 1.27.2(svelte@4.2.2)(vite@4.5.0)
'@tailwindcss/typography':
specifier: ^0.5.10
version: 0.5.10(tailwindcss@3.3.5)
'@types/bootstrap':
specifier: ^5.2.6
version: 5.2.8
'@types/d3-scale':
specifier: ^4.0.3
version: 4.0.6
'@types/d3-scale-chromatic':
specifier: ^3.0.0
version: 3.0.1
'@types/js-cookie':
specifier: ^3.0.2
version: 3.0.5
autoprefixer:
specifier: ^10.4.16
version: 10.4.16(postcss@8.4.31)
boolean:
specifier: ^3.2.0
version: 3.2.0
chart.js:
specifier: 4.4.0
version: 4.4.0
d3-scale:
specifier: ^4.0.2
version: 4.0.2
d3-scale-chromatic:
specifier: ^3.0.0
version: 3.0.0
daisyui:
specifier: ^3.8.1
version: 3.9.4
date-fns:
specifier: ^2.30.0
version: 2.30.0
highlight.js:
specifier: ^11.8.0
version: 11.9.0
pocketbase:
specifier: ^0.19.0
version: 0.19.0
sass:
specifier: ^1.68.0
version: 1.69.5
svelte:
specifier: ^4.2.1
version: 4.2.2
svelte-chartjs:
specifier: 3.1.2
version: 3.1.2(chart.js@4.4.0)(svelte@4.2.2)
svelte-check:
specifier: ^3.5.2
version: 3.5.2(postcss@8.4.31)(sass@1.69.5)(svelte@4.2.2)
svelte-highlight:
specifier: ^7.3.0
version: 7.4.1
svelte-preprocess:
specifier: ^5.0.4
version: 5.0.4(postcss@8.4.31)(sass@1.69.5)(svelte@4.2.2)(typescript@5.2.2)
tailwindcss:
specifier: ^3.3.3
version: 3.3.5
vite:
specifier: ^4.4.9
version: 4.5.0(@types/node@20.8.10)(sass@1.69.5)
packages:
/@11ty/dependency-tree@2.0.1:

View File

@ -0,0 +1,183 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((db) => {
const collection = new Collection({
"id": "buq519uv711078p",
"created": "2023-12-13 12:27:18.834Z",
"updated": "2023-12-13 12:27:18.834Z",
"name": "stats",
"type": "view",
"system": false,
"schema": [
{
"system": false,
"id": "3yphuaaj",
"name": "total_users",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "38n3tegi",
"name": "total_verified_users",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "8f9f6zdu",
"name": "total_legacy_subscribers",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "zjwddaeq",
"name": "total_free_subscribers",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "uuokjxef",
"name": "total_pro_subscribers",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "mnldzliz",
"name": "total_lifetime_subscribers",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "ckwdam5b",
"name": "verified_instances",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "ymm1aa7u",
"name": "verified_instances_last_hour",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "mdn95zm0",
"name": "verified_instances_last_24_hours",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "sg7l3nmm",
"name": "verified_instances_last_7_days",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
},
{
"system": false,
"id": "oi12fxeb",
"name": "verified_instances_last_30_days",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {
"query": "SELECT\n (ROW_NUMBER() OVER()) as id,\n COUNT(DISTINCT users.id) AS total_users,\n COUNT(DISTINCT CASE WHEN users.verified = 1 THEN users.id END) AS total_verified_users,\n COUNT(DISTINCT CASE WHEN users.subscription ='legacy' THEN users.id END) AS total_legacy_subscribers,\n COUNT(DISTINCT CASE WHEN users.subscription ='free' THEN users.id END) AS total_free_subscribers,\n COUNT(DISTINCT CASE WHEN users.subscription= 'premium' THEN users.id END) AS total_pro_subscribers,\n COUNT(DISTINCT CASE WHEN users.subscription= 'lifetime' THEN users.id END) AS total_lifetime_subscribers,\n COUNT(DISTINCT CASE WHEN users.verified = 1 THEN instances.id END) AS verified_instances,\n COUNT(DISTINCT CASE WHEN users.verified = 1 AND instances.updated > DATETIME('now', '-1 hour') THEN instances.id END) AS verified_instances_last_hour,\n COUNT(DISTINCT CASE WHEN users.verified = 1 AND instances.updated > DATETIME('now', '-24 hours') THEN instances.id END) AS verified_instances_last_24_hours,\n COUNT(DISTINCT CASE WHEN users.verified = 1 AND instances.updated > DATETIME('now', '-7 days') THEN instances.id END) AS verified_instances_last_7_days,\n COUNT(DISTINCT CASE WHEN users.verified = 1 AND instances.updated > DATETIME('now', '-30 days') THEN instances.id END) AS verified_instances_last_30_days\nFROM\n users\nLEFT JOIN\n instances ON users.id = instances.uid;\n"
}
});
return Dao(db).saveCollection(collection);
}, (db) => {
const dao = new Dao(db);
const collection = dao.findCollectionByNameOrId("buq519uv711078p");
return dao.deleteCollection(collection);
})