Superadmin
115
.github/workflows/publish.yaml
vendored
@ -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 }}`'
|
||||
|
5
frontends/superadmin/.env-template
Normal 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
@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
dist-server
|
||||
.idea
|
1
frontends/superadmin/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
engine-strict=true
|
20
frontends/superadmin/.prettierignore
Normal 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
|
41
frontends/superadmin/README.md
Normal 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.
|
8
frontends/superadmin/marked.config.js
Normal 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 }
|
43
frontends/superadmin/package.json
Normal 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"
|
||||
}
|
||||
}
|
3
frontends/superadmin/postcss.config.cjs
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
plugins: [require('tailwindcss'), require('autoprefixer')],
|
||||
}
|
3
frontends/superadmin/src/app.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
25
frontends/superadmin/src/app.html
Normal 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>
|
51
frontends/superadmin/src/components/AlertBar.svelte
Normal 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}
|
36
frontends/superadmin/src/components/Clipboard.svelte
Normal 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>
|
41
frontends/superadmin/src/components/CodeSample.svelte
Normal 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>
|
19
frontends/superadmin/src/components/CopyButton.svelte
Normal 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>
|
17
frontends/superadmin/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'}">
|
||||
Superadmin
|
||||
</h1>
|
||||
</div>
|
42
frontends/superadmin/src/components/MediaQuery.svelte
Normal 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} />
|
31
frontends/superadmin/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="/" 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>
|
75
frontends/superadmin/src/components/Navbar.svelte
Normal 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>
|
15
frontends/superadmin/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>
|
24
frontends/superadmin/src/components/cards/CardHeader.svelte
Normal 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}
|
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { isAuthStateInitialized } from '$util/stores'
|
||||
</script>
|
||||
|
||||
{#if $isAuthStateInitialized}
|
||||
<slot />
|
||||
{/if}
|
30
frontends/superadmin/src/components/helpers/Meta.svelte
Normal 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>
|
@ -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
|
||||
>
|
@ -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}
|
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { isUserLoggedIn } from '$util/stores'
|
||||
</script>
|
||||
|
||||
{#if !$isUserLoggedIn}
|
||||
<slot />
|
||||
{/if}
|
7
frontends/superadmin/src/components/helpers/theme.ts
Normal 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
|
||||
}
|
@ -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>
|
114
frontends/superadmin/src/env.ts
Normal 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`
|
164
frontends/superadmin/src/pocketbase-client/PocketbaseClient.ts
Normal 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,
|
||||
}
|
||||
}
|
16
frontends/superadmin/src/pocketbase-client/index.ts
Normal 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
|
||||
}
|
||||
})()
|
30
frontends/superadmin/src/routes/+error.svelte
Normal 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>
|
33
frontends/superadmin/src/routes/+layout.svelte
Normal 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>
|
2
frontends/superadmin/src/routes/+layout.ts
Normal file
@ -0,0 +1,2 @@
|
||||
const ssr = false
|
||||
export { ssr }
|
26
frontends/superadmin/src/routes/+page.svelte
Normal 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>
|
7
frontends/superadmin/src/routes/dashboard/+layout.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import UserLoggedIn from '$components/helpers/UserLoggedIn.svelte'
|
||||
</script>
|
||||
|
||||
<UserLoggedIn redirect>
|
||||
<slot />
|
||||
</UserLoggedIn>
|
42
frontends/superadmin/src/routes/dashboard/Dashboard.svelte
Normal 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>
|
87
frontends/superadmin/src/routes/login/+page.svelte
Normal 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>
|
12
frontends/superadmin/src/services.ts
Normal 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,
|
||||
})
|
||||
}
|
15
frontends/superadmin/src/util/componentCleanup.ts
Normal 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)
|
||||
}
|
||||
}
|
14
frontends/superadmin/src/util/database.ts
Normal 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
|
||||
}
|
||||
}
|
24
frontends/superadmin/src/util/events.ts
Normal 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]
|
||||
}
|
26
frontends/superadmin/src/util/stores.ts
Normal 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)
|
BIN
frontends/superadmin/static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
frontends/superadmin/static/favicon.png
Normal file
After Width: | Height: | Size: 27 KiB |
12
frontends/superadmin/static/icons/all.min.css
vendored
Normal file
6
frontends/superadmin/static/icons/brands.min.css
vendored
Normal file
9
frontends/superadmin/static/icons/fontawesome.min.css
vendored
Normal file
BIN
frontends/superadmin/static/icons/icon-192x192.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
frontends/superadmin/static/icons/icon-256x256.png
Normal file
After Width: | Height: | Size: 768 B |
BIN
frontends/superadmin/static/icons/icon-384x384.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
frontends/superadmin/static/icons/icon-512x512.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
frontends/superadmin/static/images/logo-square.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
frontends/superadmin/static/images/pocketbase-intro-screen.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
9
frontends/superadmin/static/images/pocketbase-logo.svg
Normal 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 |
BIN
frontends/superadmin/static/images/pockethost-cloud-logo.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
36
frontends/superadmin/static/manifest.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
BIN
frontends/superadmin/static/poster.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
frontends/superadmin/static/webfonts/fa-brands-400.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-brands-400.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-duotone-900.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-duotone-900.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-light-300.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-light-300.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-regular-400.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-regular-400.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-sharp-light-300.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-sharp-light-300.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-sharp-regular-400.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-sharp-regular-400.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-sharp-solid-900.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-sharp-solid-900.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-solid-900.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-solid-900.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-thin-100.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-thin-100.woff2
Normal file
BIN
frontends/superadmin/static/webfonts/fa-v4compatibility.ttf
Normal file
BIN
frontends/superadmin/static/webfonts/fa-v4compatibility.woff2
Normal file
24
frontends/superadmin/svelte.config.js
Normal 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
|
32
frontends/superadmin/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')],
|
||||
}
|
20
frontends/superadmin/tsconfig.json
Normal 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
|
||||
}
|
14
frontends/superadmin/vite.config.ts
Normal 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
|
@ -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
@ -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:
|
||||
|
183
src/mothership-app/migrations/1702470438_created_stats.js
Normal 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);
|
||||
})
|