lander consolidation & new pricing

This commit is contained in:
Ben Allfree 2024-10-16 12:24:06 -07:00
parent 48b84bbc6a
commit b0808b0b62
489 changed files with 6175 additions and 12230 deletions

View File

@ -5,9 +5,12 @@ on:
paths:
- .github/workflows/publish-dashboard.yaml
- packages/dashboard/**
- package.json
- pnpm-lock.yaml
env:
PUBLIC_APEX_DOMAIN: ${{ vars.PUBLIC_APEX_DOMAIN }}
MAIN_BRANCH: v0/main
jobs:
publish:
@ -47,13 +50,13 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: pockethost-dashboard
directory: ./dist/dashboard
projectName: pockethost-lander
directory: ./packages/dashboard/build
branch: ${{ github.head_ref || github.ref_name }}
wranglerVersion: '3'
- name: Discord feature branch notification
if: github.ref_name != 'v0/main'
if: github.ref_name != env.MAIN_BRANCH
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
@ -61,9 +64,9 @@ jobs:
args: '**DASHBOARD PREVIEW** ${{ steps.deployment.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
if: github.ref_name == 'v0/main'
if: github.ref_name == env.MAIN_BRANCH
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**DASHBOARD LIVE** https://app.pockethost.io ([permalink](${{ steps.deployment.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 }}`'
args: '**DASHBOARD LIVE** https://pockethost.io ([permalink](${{ steps.deployment.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

@ -1,69 +0,0 @@
name: Publish Lander to Cloudflare Pages
on:
push:
paths:
- .github/workflows/publish-lander.yaml
- packages/lander/**
env:
PUBLIC_APEX_DOMAIN: ${{ vars.PUBLIC_APEX_DOMAIN }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
name: Publish to Cloudflare Pages
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: pnpm
uses: pnpm/action-setup@v3.0.0
with:
version: 9.9.0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Prepare build
run: |
pnpm i
cd packages/lander
pnpm build
- name: Expose git commit data
uses: rlespinasse/git-commit-data-action@v1.5.0
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1.5.0
id: deployment
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: Discord feature branch notification
if: github.ref_name != 'v0/main'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**LANDER PREVIEW** ${{ steps.deployment.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
if: github.ref_name == 'v0/main'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**LANDER LIVE** https://pockethost.io ([permalink](${{ steps.deployment.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

@ -1,69 +0,0 @@
name: Publish Superadmin to Cloudflare Pages
on:
push:
paths:
- .github/workflows/publish-superadmin.yaml
- packages/superadmin/**
env:
PUBLIC_APEX_DOMAIN: ${{ vars.PUBLIC_APEX_DOMAIN }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
name: Publish to Cloudflare Pages
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: pnpm
uses: pnpm/action-setup@v3.0.0
with:
version: 9.9.0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Prepare build
run: |
pnpm i
cd packages/superadmin
pnpm build
- name: Expose git commit data
uses: rlespinasse/git-commit-data-action@v1.5.0
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1.5.0
id: deployment
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: Discord feature branch notification
if: github.ref_name != 'v0/main'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**SUPERADMIN PREVIEW** ${{ steps.deployment.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
if: github.ref_name == 'v0/main'
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
with:
args: '**SUPERADMIN LIVE** https://superadmin.pockethost.io ([permalink](${{ steps.deployment.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

@ -1 +0,0 @@
openai-secret-key

View File

@ -1 +0,0 @@
The primary audience is software developers, specifically web and nodejs programmers. Lots of interest in cloud services and avoiding writing backends. Many of our users use SvelteKit, React, Vercel, Firebase, and Supabase. My audience does NOT like puffery or flowery language. They like technical inspiration and spending time with tools that will enhance their ability to touch the lives of others with their creations.

View File

@ -1 +0,0 @@
I run a site called pockethost.io. I am a Typescript and nodejs programmer. I'm pretty experienced, but I focus mostly on backend stuff and I know I'm not the best at frontend ui/ux. I have a passion for open source projects and for helping people make their own creations reach people. I have a lot of experience and am seasoned, yet very patient with newcomers. I like to teach. I like to encourage people. I love to learn. I don't trust horses, and I like jiu jitsu.

View File

@ -1,3 +0,0 @@
This is a blog for web and mobile app developers. They are interested in BaaS, as this is our central product. They tend to be well-versed in Typescript and JavaScript, but only have 2-5 years of experience. They like making little apps and inventions to serve specific verticals that they find interesting. They like open source, and they like independence and sovereignty. The blog should have a factual tone with no embellishments. You can include up to one pop culture reference per article, but it should not be a central point.
Use third person language. Do not refer to 'we' or use colloquialisms. Avoid phrases like "squashed pesky bugs" and playful language. Be direct, to the point, and do not attempt to introduce personality into the prose.

View File

@ -1,46 +0,0 @@
pockethost.io is a hosted version of the open source project named PocketHost (licensed under the MIT open source license) which is based on the PocketBase open source project.
PocketBase is essentially a REST API and admin UI over a SQLite database. PocketBase is written in golang. It provides user management, logging, REST security, as well as an advanced SQLite schema editor and data editor.
PocketHost is a multitenant 'one click' hosting and deployment service for PocketBase. Users can create accounts, create projects with uniquely named subdomains off the pockethost.io apex domain. This way, they don't have to do any setup to get PocketBase running. Our motto is "Up and running in 30 seconds with zero config". As you might imagine, preparing PocketBase for production use takes a lot of Linux administration skills. SSL, DNS, Domain management, CNAMES, Docker containers, restarting on errors, SMTP, etc. PocketHost does all this for the user. We have a Discord channel to provide support and collect feature ideas.
PocketHost has been received very positively by the community and has over 400 stars. It is used by many hobbyist developers as well as live applications.
### Overview
PocketHost hosts your [PocketBase](https://pocketbase.io) projects, so you don't have to. Create a project like you would in Firebase and Supabase and let PocketHost do the rest.
PocketHost is a cloud hosting platform for PocketBase. You can use it to instantly provision a PocketBase backend for your latest project. Features include:
- Create unlimited PocketBase projects, each with a custom subdomain
- Each instance runs on a subdomain of `pockethost.io`
- Access your PocketBase instance using the PocketBase JavaScript SDK as easily as `new PocketBase('https://my-project.pockethost.io')`
- Run your instance in an ultra-beefy shared environment
### Focus on your app
Get a live PocketBase instance in 10 seconds with no backend setup:
1. Create an account at pockethost.io
2. Provision your first PocketBase instance
3. Connect from anywhere
```ts
import PocketBase from 'pocketbase'
const client = new PocketBase(`https://harvest.pockethost.io`)
```
### Batteries Included
Here's all the Linux/devops stuff that PocketHost does for you:
- Email and DKIM+SPF and more
- DNS jargon: MX, TXT, CNAME
- SSL cert provisioning and management
- Storage
- Volume mounts
- Cloud computing or VPS deployment
- CDN and static asset hosting
- Amazon AWS
- Lots more - scaling, firewalls, DDoS defense, user security, log rotation, patches, updates, build tools, CPU architectures, multitenancy, on and on

View File

@ -1 +0,0 @@
The primary audience is software developers, specifically web and nodejs programmers. Lots of interest in cloud services and avoiding writing backends. Many of our users use SvelteKit, React, Vercel, Firebase, and Supabase. My audience does NOT like puffery or flowery language. They like technical inspiration and spending time with tools that will enhance their ability to touch the lives of others with their creations.

View File

@ -1 +0,0 @@
I run a site called pockethost.io. I am a Typescript and nodejs programmer. I'm pretty experienced, but I focus mostly on backend stuff and I know I'm not the best at frontend ui/ux. I have a passion for open source projects and for helping people make their own creations reach people. I have a lot of experience and am seasoned, yet very patient with newcomers. I like to teach. I like to encourage people. I love to learn. I don't trust horses, and I like jiu jitsu.

View File

@ -1 +0,0 @@
This prose is technical documentation. It should have an edgy, modern voice that is fun-loving but accurate.

View File

@ -1,46 +0,0 @@
pockethost.io is a hosted version of the open source project named PocketHost (licensed under the MIT open source license) which is based on the PocketBase open source project.
PocketBase is essentially a REST API and admin UI over a SQLite database. PocketBase is written in golang. It provides user management, logging, REST security, as well as an advanced SQLite schema editor and data editor.
PocketHost is a multitenant 'one click' hosting and deployment service for PocketBase. Users can create accounts, create projects with uniquely named subdomains off the pockethost.io apex domain. This way, they don't have to do any setup to get PocketBase running. Our motto is "Up and running in 30 seconds with zero config". As you might imagine, preparing PocketBase for production use takes a lot of Linux administration skills. SSL, DNS, Domain management, CNAMES, Docker containers, restarting on errors, SMTP, etc. PocketHost does all this for the user. We have a Discord channel to provide support and collect feature ideas.
PocketHost has been received very positively by the community and has over 400 stars. It is used by many hobbyist developers as well as live applications.
### Overview
PocketHost hosts your [PocketBase](https://pocketbase.io) projects, so you don't have to. Create a project like you would in Firebase and Supabase and let PocketHost do the rest.
PocketHost is a cloud hosting platform for PocketBase. You can use it to instantly provision a PocketBase backend for your latest project. Features include:
- Create unlimited PocketBase projects, each with a custom subdomain
- Each instance runs on a subdomain of `pockethost.io`
- Access your PocketBase instance using the PocketBase JavaScript SDK as easily as `new PocketBase('https://my-project.pockethost.io')`
- Run your instance in an ultra-beefy shared environment
### Focus on your app
Get a live PocketBase instance in 10 seconds with no backend setup:
1. Create an account at pockethost.io
2. Provision your first PocketBase instance
3. Connect from anywhere
```ts
import PocketBase from 'pocketbase'
const client = new PocketBase(`https://harvest.pockethost.io`)
```
### Batteries Included
Here's all the Linux/devops stuff that PocketHost does for you:
- Email and DKIM+SPF and more
- DNS jargon: MX, TXT, CNAME
- SSL cert provisioning and management
- Storage
- Volume mounts
- Cloud computing or VPS deployment
- CDN and static asset hosting
- Amazon AWS
- Lots more - scaling, firewalls, DDoS defense, user security, log rotation, patches, updates, build tools, CPU architectures, multitenancy, on and on

View File

@ -1 +0,0 @@
The primary audience is software developers, specifically web and nodejs programmers. Lots of interest in cloud services and avoiding writing backends. Many of our users use SvelteKit, React, Vercel, Firebase, and Supabase. My audience does NOT like puffery or flowery language. They like technical inspiration and spending time with tools that will enhance their ability to touch the lives of others with their creations.

View File

@ -1 +0,0 @@
I run a site called pockethost.io. I am a Typescript and nodejs programmer. I'm pretty experienced, but I focus mostly on backend stuff and I know I'm not the best at frontend ui/ux. I have a passion for open source projects and for helping people make their own creations reach people. I have a lot of experience and am seasoned, yet very patient with newcomers. I like to teach. I like to encourage people. I love to learn. I don't trust horses, and I like jiu jitsu.

View File

@ -1 +0,0 @@
This is a blog post announcing a new version of our open source project, PocketHost. It should have an intelligently humorous opening paragraph followed by a dry, accurate, meticulous description of changes and updates, and why they are important. You can casually mathematically or logically clever notions with hidden meanings.

View File

@ -1,46 +0,0 @@
pockethost.io is a hosted version of the open source project named PocketHost (licensed under the MIT open source license) which is based on the PocketBase open source project.
PocketBase is essentially a REST API and admin UI over a SQLite database. PocketBase is written in golang. It provides user management, logging, REST security, as well as an advanced SQLite schema editor and data editor.
PocketHost is a multitenant 'one click' hosting and deployment service for PocketBase. Users can create accounts, create projects with uniquely named subdomains off the pockethost.io apex domain. This way, they don't have to do any setup to get PocketBase running. Our motto is "Up and running in 30 seconds with zero config". As you might imagine, preparing PocketBase for production use takes a lot of Linux administration skills. SSL, DNS, Domain management, CNAMES, Docker containers, restarting on errors, SMTP, etc. PocketHost does all this for the user. We have a Discord channel to provide support and collect feature ideas.
PocketHost has been received very positively by the community and has over 400 stars. It is used by many hobbyist developers as well as live applications.
### Overview
PocketHost hosts your [PocketBase](https://pocketbase.io) projects, so you don't have to. Create a project like you would in Firebase and Supabase and let PocketHost do the rest.
PocketHost is a cloud hosting platform for PocketBase. You can use it to instantly provision a PocketBase backend for your latest project. Features include:
- Create unlimited PocketBase projects, each with a custom subdomain
- Each instance runs on a subdomain of `pockethost.io`
- Access your PocketBase instance using the PocketBase JavaScript SDK as easily as `new PocketBase('https://my-project.pockethost.io')`
- Run your instance in an ultra-beefy shared environment
### Focus on your app
Get a live PocketBase instance in 10 seconds with no backend setup:
1. Create an account at pockethost.io
2. Provision your first PocketBase instance
3. Connect from anywhere
```ts
import PocketBase from 'pocketbase'
const client = new PocketBase(`https://harvest.pockethost.io`)
```
### Batteries Included
Here's all the Linux/devops stuff that PocketHost does for you:
- Email and DKIM+SPF and more
- DNS jargon: MX, TXT, CNAME
- SSL cert provisioning and management
- Storage
- Volume mounts
- Cloud computing or VPS deployment
- CDN and static asset hosting
- Amazon AWS
- Lots more - scaling, firewalls, DDoS defense, user security, log rotation, patches, updates, build tools, CPU architectures, multitenancy, on and on

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -6,10 +6,10 @@ PocketHost hosts your [PocketBase](https://pocketbase.io) projects, so you don't
PocketHost is a cloud hosting platform for PocketBase. You can use it to instantly provision a PocketBase backend for your latest project. Features include:
- Create unlimited PocketBase projects, each with a custom subdomain
- Create PocketBase projects, each with a custom subdomain
- Each instance runs on a subdomain of `pockethost.io`
- Access your PocketBase instance using the PocketBase JavaScript SDK as easily as
`new PocketBase('https://my-project.pockethost.io')`
- Access your PocketBase instance using the PocketBase JavaScript SDK as easily as
`new PocketBase('https://my-project.pockethost.io')`
- Run your instance in an ultra-beefy shared environment.
## 🎯 Focus on your app
@ -26,7 +26,7 @@ import PocketBase from 'pocketbase'
const client = new PocketBase(`https://harvest.pockethost.io`)
```
## 🔋Batteries Included
## 🔋Batteries Included
Here's all the Linux/DevOps stuff that PocketHost does for you:
@ -38,4 +38,4 @@ Here's all the Linux/DevOps stuff that PocketHost does for you:
- Cloud computing or VPS deployment
- CDN and static asset hosting
- Amazon AWS
- Lots more - scaling, firewalls, DDoS protection, user security, log retention, patches, updates, build tools, CPU architectures, multitenancy, and the list goes on
- Lots more - scaling, firewalls, DDoS protection, user security, log retention, patches, updates, build tools, CPU architectures, multitenancy, and the list goes on

View File

@ -9,11 +9,9 @@
"lint": "prettier -c \"./**/*.{ts,js,cjs,svelte,json}\"",
"lint:fix": "prettier -w \"./**/*.{ts,js,cjs,svelte,json}\"",
"dev:cli": "cd packages/pockethost && pnpm dev",
"dev:lander": "cd packages/lander && pnpm start",
"dev:dashboard": "cd packages/dashboard && pnpm dev",
"dev:superadmin": "cd packages/superadmin && pnpm dev",
"prod:cli": "cd packages/pockethost && pnpm start",
"plop": "plop --no-progress",
"nofile": "cat /proc/sys/fs/file-nr",
"mail": "tsx ./packages/pockethost/src/cli/sendmail.ts"
},
@ -65,13 +63,11 @@
"ncp": "^2.0.0",
"nodemon": "^3.0.3",
"ora": "^7.0.1",
"plop": "^4.0.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "^3.0.3",
"prettier-plugin-jsdoc": "^1.3.0",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-svelte": "^3.0.3",
"rizzdown": "^0.0.7",
"svelte": "^4.2.2",
"tslib": "^2.6.2",
"tsx": "^3.14.0",
@ -80,7 +76,8 @@
},
"pnpm": {
"patchedDependencies": {
"eventsource@2.0.2": "patches/eventsource@2.0.2.patch"
"eventsource@2.0.2": "patches/eventsource@2.0.2.patch",
"@sveltejs/enhanced-img": "patches/@sveltejs__enhanced-img.patch"
}
}
}

View File

@ -1,3 +0,0 @@
# Modify this file to force a build
asd

View File

@ -1,3 +1,4 @@
import fancyImage from '$src/lib/fancyImage'
import hljs from 'highlight.js'
const highlight = (code, lang) => {
@ -5,4 +6,4 @@ const highlight = (code, lang) => {
return hljs.highlight(code, { language: lang }).value
}
export default { highlight }
export default { highlight, fancyImage }

View File

@ -5,40 +5,49 @@
"main": "./src/app.html",
"scripts": {
"check:types": "svelte-check",
"preview": "npx http-server@latest ../../dist/dashboard -P \"http://localhost:8080?\"",
"preview": "npx http-server@latest ./build -P \"http://localhost:8080?\"",
"dev": "vite dev",
"build": "NODE_ENV=production vite build",
"lint": "prettier --check .",
"format": "prettier --write ."
"format": "prettier --write .",
"postbuild": "svelte-sitemap --domain https://pockethost.io"
},
"type": "module",
"devDependencies": {
"@microsoft/fetch-event-source": "https://github.com/pockethost/fetch-event-source.git#ebe3b7122647b48b93fd11effbbfb915d98956b0",
"pockethost": "workspace:../pockethost",
"@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",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@microsoft/fetch-event-source": "github:pockethost/fetch-event-source#ebe3b7122647b48b93fd11effbbfb915d98956b0",
"@s-libs/micro-dash": "^18.0.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/enhanced-img": "^0.3.8",
"@sveltejs/kit": "^2.6.2",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@tailwindcss/typography": "^0.5.15",
"@types/bootstrap": "^5.2.10",
"@types/d3-scale": "^4.0.8",
"@types/d3-scale-chromatic": "^3.0.3",
"@types/js-cookie": "^3.0.6",
"autoprefixer": "^10.4.20",
"boolean": "^3.2.0",
"chart.js": "4.4.0",
"d3-scale": "^4.0.2",
"d3-scale-chromatic": "^3.0.0",
"daisyui": "^4.4.23",
"date-fns": "^2.30.0",
"highlight.js": "^11.8.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",
"@pockethost/plugin-console-logger": "workspace:*"
"d3-scale-chromatic": "^3.1.0",
"daisyui": "^4.12.12",
"date-fns": "^4.1.0",
"highlight.js": "^11.10.0",
"just-camel-case": "^6.2.0",
"mdsvex": "^0.12.3",
"mdsvex-enhanced-images": "^0.2.2",
"pockethost": "workspace:^",
"sass": "^1.79.4",
"svelte": "^4.2.19",
"svelte-check": "^4.0.4",
"svelte-fa": "^4.0.3",
"svelte-highlight": "^7.7.0",
"svelte-preprocess": "^6.0.3",
"svelte-sitemap": "^2.6.0",
"tailwindcss": "^3.4.13",
"unist-util-visit": "^5.0.0",
"vite": "^5.4.8"
}
}

View File

@ -10,5 +10,14 @@
}
.prose :where(p):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
@apply mt-0 mb-0;
@apply mt-2 mb-2;
}
.docs-content img {
@apply border-accent rounded-lg border;
}
.prose :where(li):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
@apply mt-0 mb-0;
line-height: 1.5em;
}

View File

@ -7,19 +7,9 @@
<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 class="h-full">
<body>
<div>%sveltekit.body%</div>
</body>
</html>

View File

@ -1,4 +1,12 @@
<script lang="ts">
import {
faCircleCheck,
faCircleInfo,
faCircleXmark,
faTriangleExclamation,
type IconDefinition,
} from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
import { slide } from 'svelte/transition'
// https://daisyui.com/components/alert/
@ -7,45 +15,54 @@
export let message: string = ''
export let type: AlertTypes
export let additionalClasses: string = ''
export let flash = false
let isHidden = false
$: {
if (flash) {
setTimeout(() => {
isHidden = true
}, 5000)
}
}
// Set up the default alert classes and icon
let alertTypeClass = ''
let alertTypeIcon = `<i class="fa-regular fa-circle-info"></i>`
let alertTypeIcon: IconDefinition = faCircleInfo
if (type === 'default') {
alertTypeClass = ''
alertTypeIcon = `<i class="fa-regular fa-circle-info"></i>`
alertTypeIcon = faCircleInfo
}
if (type === 'info') {
alertTypeClass = 'alert-info'
alertTypeIcon = `<i class="fa-regular fa-circle-info"></i>`
alertTypeIcon = faCircleInfo
}
if (type === 'success') {
alertTypeClass = 'alert-success'
alertTypeIcon = `<i class="fa-regular fa-circle-check"></i>`
alertTypeIcon = faCircleCheck
}
if (type === 'warning') {
alertTypeClass = 'alert-warning'
alertTypeIcon = `<i class="fa-regular fa-triangle-exclamation"></i>`
alertTypeIcon = faTriangleExclamation
}
if (type === 'error') {
alertTypeClass = 'alert-error'
alertTypeIcon = `<i class="fa-regular fa-circle-xmark"></i>`
alertTypeIcon = faCircleXmark
}
</script>
{#if message}
{#if message && !isHidden}
<div
class="alert mb-4 {alertTypeClass} {additionalClasses} justify-center"
transition:slide
role="alert"
>
{@html alertTypeIcon}
<Fa icon={alertTypeIcon} />
<span>{@html message}</span>
</div>
{/if}

View File

@ -2,6 +2,7 @@
import CopyButton from '$components/CopyButton.svelte'
import { Highlight } from 'svelte-highlight'
import { typescript, type LanguageType } from 'svelte-highlight/languages'
import a11yDark from "svelte-highlight/styles/a11y-dark";
export let code: string
export let language: LanguageType<'typescript' | 'bash' | 'dns'> = typescript
@ -9,6 +10,10 @@
const handleCopy = () => {}
</script>
<svelte:head>
{@html a11yDark}
</svelte:head>
<div class="copy-container">
<Highlight {language} {code} />

View File

@ -1,6 +1,6 @@
<script lang="ts">
import Clipboard from '$components/Clipboard.svelte'
import TinyButton from './helpers/TinyButton.svelte'
import TinyButton from '$components/helpers/TinyButton.svelte'
let isCopied = false
export let code: string

View File

@ -1,15 +0,0 @@
<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"
class="mix-blend-lighten {logoWidth}"
alt="PocketHost Logo"
/>
<h1 class="text-white font-bold text-2xl {hideLogoText && 'sr-only'}">
PocketHost
</h1>
</div>

View File

@ -1,42 +0,0 @@
<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

@ -1,31 +0,0 @@
<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="h-full">
<slot />
</div>
</div>
</div>

View File

@ -1,139 +0,0 @@
<script lang="ts">
import Logo from '$components/Logo.svelte'
import { DOCS_URL } from '$src/env'
import { client } from '$src/pocketbase-client'
import InstancesGuard from '$src/routes/InstancesGuard.svelte'
import { globalInstancesStore, userStore } from '$util/stores'
import { values } from '@s-libs/micro-dash'
import { writable } from 'svelte/store'
type TypeInstanceObject = {
id: string
subdomain: string
maintenance: boolean
}
let arrayOfActiveInstances: TypeInstanceObject[] = []
async function gravatarHash(email: string) {
// Normalize the email by trimming and converting to lowercase
const normalizedEmail = email.trim().toLowerCase()
console.log('normalizedEmail', normalizedEmail)
// Convert the normalized email to a UTF-8 byte array
const msgBuffer = new TextEncoder().encode(normalizedEmail)
// Hash the email using MD5
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
// Convert the hash to a hex stringc
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
return hashHex
}
const avatar = writable('')
$: {
if ($userStore?.email) {
gravatarHash($userStore.email).then((hash) => {
avatar.set(`https://www.gravatar.com/avatar/${hash}`)
})
}
}
$: {
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 handleMobileNavDismiss = () => {
document.querySelector<HTMLElement>('summary.apps')?.click()
}
</script>
<div class="navbar bg-base-100">
<div class="flex-1">
<a href="/" role="tab" class="tab">
<Logo hideLogoText={true} logoWidth="h-8" />
<div class="hidden md:block ml-1">PocketHost</div>
</a>
</div>
<div class="flex-none gap-2">
<ul class="menu menu-horizontal px-1">
<li>
<details>
<summary class="apps">Apps</summary>
<ul class="bg-base-100 rounded-t-none p-2">
<InstancesGuard>
<ul role="list" class="z-50 bg-base-100">
<li>
<a href="/app/new" on:click={handleMobileNavDismiss}
>+ New App</a
>
</li>
{#each arrayOfActiveInstances as app}
<li>
<a
href={`/app/instances/${app.id}`}
on:click={handleMobileNavDismiss}>{app.subdomain}</a
>
</li>
{/each}
</ul>
</InstancesGuard>
</ul>
</details>
</li>
</ul>
<ul class="menu menu-horizontal px-1">
<li>
<a href="https://pockethost.io/support" target="_blank" rel="noreferrer"
>Support</a
>
</li>
<li>
<a href={`${DOCS_URL()}`} target="_blank" rel="noreferrer">Docs</a>
</li>
<li>
<a
href="https://github.com/pockethost/pockethost"
target="_blank"
rel="noreferrer"
>
<i class="fa-brands fa-github mt-1" />
</a>
</li>
</ul>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
<div class="w-10 rounded-full">
<img src={$avatar} />
</div>
</div>
<ul
tabindex="0"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
>
<li><a href="/account">Settings</a></li>
<li><a on:click={handleLogoutAndRedirect}>Logout</a></li>
</ul>
</div>
</div>
</div>

View File

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

View File

@ -1,58 +0,0 @@
<script lang="ts">
import { userSubscriptionType } from '$util/stores'
import { PLAN_NAMES, SubscriptionType } from 'pockethost/common'
export let handleClick: any = () => {}
</script>
<div class=" flex flex-col gap-4 mb-4">
<div
class="card bg-accent shadow-xl rounded-lg overflow-hidden bg-[url('/images/pockethost-cloud-logo.jpg')]"
>
<div class="card-body backdrop-blur-md text-white">
<h2 class="card-title">
{PLAN_NAMES[$userSubscriptionType]}
</h2>
{#if $userSubscriptionType === SubscriptionType.Free}
<p>
You're on the free Hacker plan. Unlock more features such as unlimited
projects.
</p>
<div class="card-actions justify-end">
<a class="btn btn-primary" href="/account" on:click={handleClick}
>Unlock</a
>
</div>
{/if}
{#if $userSubscriptionType === SubscriptionType.Legacy}
<p>
You're on the Legacy plan. Everything works, but you can't create new
projects. Unlock more features by supporting PocketHost. This plan may
be sunset eventually.
</p>
<div class="card-actions justify-end">
<a class="btn btn-primary" href="/account" on:click={handleClick}
>Unlock</a
>
</div>
{/if}
{#if $userSubscriptionType === SubscriptionType.Premium}
<p>
Your Pro membership is active. Thank you for supporting PocketHost!
</p>
{/if}
{#if $userSubscriptionType === SubscriptionType.Founder}
<p>
What an absolute chad you are. Thank you for supporting PocketHost
with a Founder's membership!
</p>
{/if}
</div>
</div>
</div>

View File

@ -2,6 +2,9 @@
import { slide } from 'svelte/transition'
import { isUserLoggedIn, isUserVerified } from '$util/stores'
import { client } from '$src/pocketbase-client'
import UserLoggedIn from './guards/UserLoggedIn.svelte'
import { faCheck, faEnvelopeSquare } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
const { resendVerificationEmail } = client()
@ -26,30 +29,32 @@
}
</script>
{#if $isUserLoggedIn && !$isUserVerified}
<div class="alert alert-info mb-8">
<i class="fa-light fa-envelopes"></i>
<UserLoggedIn>
{#if !$isUserVerified}
<div class="alert alert-info mb-8">
<Fa icon={faEnvelopeSquare} />
<div>Please verify your account by clicking the link in your email</div>
<div>Please verify your account by clicking the link in your email</div>
<div class="text-right">
{#if isButtonProcessing}
<div class="btn btn-success">
<i class="fa-regular fa-check"></i> Sent!
</div>
{:else}
<button
type="button"
class="btn btn-outline-secondary btn-sm"
on:click={handleClick}>Resend Email</button
>
{/if}
<div class="text-right">
{#if isButtonProcessing}
<div class="btn btn-success">
<Fa icon={faCheck} /> Sent!
</div>
{:else}
<button
type="button"
class="btn btn-outline-secondary btn-sm"
on:click={handleClick}>Resend Email</button
>
{/if}
{#if formError}
<div transition:slide class="border-top text-center mt-2 pt-2">
{formError}
</div>
{/if}
{#if formError}
<div transition:slide class="border-top text-center mt-2 pt-2">
{formError}
</div>
{/if}
</div>
</div>
</div>
{/if}
{/if}
</UserLoggedIn>

View File

@ -1,4 +1,7 @@
<script lang="ts">
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
export let documentation: string = ''
</script>
@ -10,9 +13,10 @@
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
>Full documentation <Fa
icon={faArrowUpRightFromSquare}
class="opacity-50 text-sm"
/></a
>
</div>
{/if}

View File

@ -4,4 +4,6 @@
{#if $isAuthStateInitialized}
<slot />
{:else}
<slot name="loading" />
{/if}

View File

@ -0,0 +1,23 @@
<script lang="ts">
import {
isAuthStateInitialized,
isUserLoggedIn,
userStore,
} from '$util/stores'
export let role = ''
let hasRole = false
$: {
if ($isAuthStateInitialized && $isUserLoggedIn) {
switch (role) {
case 'stats':
hasRole = !!$userStore?.isStatsRole
break
}
}
}
</script>
{#if hasRole}
<slot />
{/if}

View File

@ -4,7 +4,7 @@
export let redirect = false
$: {
if ($isAuthStateInitialized && redirect && !$isUserLoggedIn) {
window.location.href = '/'
window.location.href = '/get-started'
}
}
</script>

View File

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

View File

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

View File

@ -1,10 +0,0 @@
<script lang="ts">
import { isUserVerified } from '$util/stores'
import AuthStateGuard from './AuthStateGuard.svelte'
</script>
<AuthStateGuard>
{#if $isUserVerified}
<slot />
{/if}
</AuthStateGuard>

View File

@ -1,10 +0,0 @@
<script lang="ts">
import { isUserVerified } from '$util/stores'
import AuthStateGuard from './AuthStateGuard.svelte'
</script>
<AuthStateGuard>
{#if !$isUserVerified}
<slot />
{/if}
</AuthStateGuard>

View File

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

View File

@ -1,38 +0,0 @@
<script lang="ts">
import { page } from '$app/stores'
import type { MouseEventHandler } from 'svelte/elements'
export let url: string = ''
export let icon: string = ''
export let brandIcon: boolean = false
export let iconSmall: boolean = false
export let external: boolean = false
export let handleClick: MouseEventHandler<HTMLElement> = () => {}
let activeLink = $page.url.pathname === url
$: activeLink = $page.url.pathname.includes(url)
</script>
<a
href={url}
class="{activeLink
? 'text-white bg-gray-800'
: 'text-gray-400'} capitalize hover:text-white hover:bg-gray-800 group flex items-center gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
on:click={handleClick}
>
<i
class="h-6 w-6 flex items-center justify-center {activeLink
? 'text-primary'
: ''} {brandIcon ? 'fa-brands' : 'fa-light'} fa-{icon} {iconSmall
? 'text-sm'
: 'text-xl'}"
></i>
<slot />
{#if external}
<i
class="fa-regular fa-arrow-up-right-from-square ml-auto opacity-50 text-xs"
></i>
{/if}
</a>

View File

@ -1,4 +0,0 @@
<th scope="row" class="w-1/4 py-3 pr-4 text-left text-sm font-normal leading-6">
<slot />
<div class="absolute inset-x-8 mt-3 h-px bg-gray-600"></div>
</th>

View File

@ -1,15 +0,0 @@
<script lang="ts">
import TextBlock from '$components/tables/pricing-table/TextBlock.svelte'
import YesBlock from '$components/tables/pricing-table/YesBlock.svelte'
import NoBlock from '$components/tables/pricing-table/NoBlock.svelte'
export let item: string
</script>
{#if item === 'YesBlock'}
<YesBlock />
{:else if item === 'NoBlock'}
<NoBlock />
{:else}
<TextBlock>{item}</TextBlock>
{/if}

View File

@ -1,6 +0,0 @@
<td class="relative w-1/4 px-4 py-0 text-center">
<span class="relative h-full w-full py-3">
<i class="fa-regular fa-x text-error"></i>
<span class="sr-only">Yes</span>
</span>
</td>

View File

@ -1,327 +0,0 @@
<script lang="ts">
import MediaQuery from '$components/MediaQuery.svelte'
import FeatureName from '$components/tables/pricing-table/FeatureName.svelte'
import FeatureSupportBlock from '$components/tables/pricing-table/FeatureSupportBlock.svelte'
import MobileTable from '$components/tables/pricing-table/MobileTable.svelte'
import { DISCORD_URL, DOCS_URL } from '$src/env'
import { userSubscriptionType } from '$util/stores'
import { PLAN_NAMES, SubscriptionType } from 'pockethost/common'
type ItemValue = '1' | 'Unlimited' | 'YesBlock' | 'NoBlock'
interface Item {
name: string
items: ItemValue[]
isNew?: boolean
infoUrl?: string
}
const items: Item[] = [
{
name: 'Number of Projects',
items: ['1', 'Unlimited', 'Unlimited'],
infoUrl: '/usage/usage-limits',
},
{
name: 'Unlimited Bandwidth*',
items: ['YesBlock', 'YesBlock', 'YesBlock'],
infoUrl: '/usage/usage-limits',
},
{
name: 'Unlimited Storage*',
items: ['YesBlock', 'YesBlock', 'YesBlock'],
infoUrl: '/usage/usage-limits',
},
{
name: 'Unlimited CPU*',
items: ['YesBlock', 'YesBlock', 'YesBlock'],
infoUrl: '/usage/usage-limits',
},
{
name: 'FTP access',
items: ['YesBlock', 'YesBlock', 'YesBlock'],
infoUrl: '/usage/ftp',
},
{
name: 'Run every version of PocketBase',
items: ['YesBlock', 'YesBlock', 'YesBlock'],
infoUrl: '/usage/upgrading',
},
{
name: 'Secure infrastructure',
items: ['YesBlock', 'YesBlock', 'YesBlock'],
infoUrl: '/overview/faq/#data-privacy-and-security',
},
{
name: 'Community Discord',
items: ['YesBlock', 'YesBlock', 'YesBlock'],
infoUrl: DISCORD_URL,
},
{
name: 'Custom Domains',
items: ['NoBlock', 'YesBlock', 'YesBlock'],
isNew: true,
infoUrl: `/usage/custom-domain`,
},
{ name: 'Priority Discord', items: ['NoBlock', 'YesBlock', 'YesBlock'] },
{ name: "Founder's Discord", items: ['NoBlock', 'NoBlock', 'YesBlock'] },
{ name: 'Founders mug/tee', items: ['NoBlock', 'NoBlock', 'YesBlock'] },
]
</script>
<div class="relative lg:pt-14">
<div class="mx-auto px-6 py-24 sm:py-32 lg:px-8">
<MediaQuery query="(min-width: 1024px)" let:matches>
{#if matches}
<section aria-labelledby="comparison-heading">
<h2 id="comparison-heading" class="sr-only">Feature comparison</h2>
<div class="-mt-6 space-y-16">
<div>
<div class="relative -mx-8 mt-10">
<table
class="relative w-full border-separate border-spacing-x-8"
>
<thead>
<tr class="text-left">
<th scope="col">
<span class="sr-only">Feature</span>
</th>
<th scope="col" class="text-center">
{PLAN_NAMES[SubscriptionType.Free]}
</th>
<th scope="col" class="text-center">
{PLAN_NAMES[SubscriptionType.Premium]}
</th>
<th scope="col" class="text-center">
{PLAN_NAMES[SubscriptionType.Founder]}
</th>
</tr>
</thead>
<tbody>
{#each items as item}
<tr>
<FeatureName
>{item.name}
{#if item.isNew}
<span class="badge badge-primary">new</span>
{/if}
{#if item.infoUrl}
<a
href={item.infoUrl.startsWith(`http`)
? item.infoUrl
: DOCS_URL(item.infoUrl)}
class="badge badge-neutral"
target="_blank">i</a
>
{/if}
</FeatureName>
<FeatureSupportBlock item={item.items[0] ?? ''} />
<FeatureSupportBlock item={item.items[1] ?? ''} />
<FeatureSupportBlock item={item.items[2] ?? ''} />
</tr>
{/each}
</tbody>
</table>
<!-- Fake card borders -->
<div
class="pointer-events-none absolute inset-x-8 inset-y-0 grid grid-cols-4 gap-x-8 before:block"
aria-hidden="true"
>
{#if $userSubscriptionType === SubscriptionType.Legacy || $userSubscriptionType === SubscriptionType.Free}
<div class="rounded-lg ring-2 ring-primary"></div>
{:else}
<div class="rounded-lg ring-1 ring-transparent"></div>
{/if}
{#if $userSubscriptionType === SubscriptionType.Premium}
<div class="rounded-lg ring-2 ring-primary"></div>
{:else}
<div class="rounded-lg ring-1 ring-transparent"></div>
{/if}
{#if $userSubscriptionType === SubscriptionType.Founder}
<div class="rounded-lg ring-2 ring-primary"></div>
{:else}
<div class="rounded-lg ring-1 ring-transparent"></div>
{/if}
</div>
</div>
</div>
</div>
</section>
{:else}
<section aria-labelledby="mobile-comparison-heading">
<h2 id="mobile-comparison-heading" class="sr-only">
Feature comparison
</h2>
<div class="mx-auto max-w-2xl space-y-16">
<div>
<div class="-mt-px pt-10">
<h3 class="text-sm font-semibold leading-6 text-primary">
Free
</h3>
<p class="mt-1 text-sm leading-6">
Free forever. Use PocketHost for your next project and enjoy
all the same features the paid tiers get.
</p>
</div>
<div class="mt-10 space-y-10">
<div>
<h4 class="text-sm font-semibold leading-6">
Catered for business
</h4>
<div class="relative mt-6">
<!-- Fake card background -->
<div
aria-hidden="true"
class="absolute inset-y-0 right-0 hidden w-1/2 rounded-lg bg-gray-800 shadow-sm sm:block"
></div>
<div
class={`relative rounded-lg bg-gray-800 shadow-sm sm:rounded-none sm:bg-transparent sm:shadow-none sm:ring-0`}
>
<dl class="divide-y divide-gray-200 text-sm leading-6">
{#each items as item}
<MobileTable
feature={item.name}
item={item.items[0] || ''}
/>
{/each}
</dl>
</div>
<!-- Fake card border -->
{#if $userSubscriptionType === SubscriptionType.Legacy || $userSubscriptionType === SubscriptionType.Free}
<div
aria-hidden="true"
class="pointer-events-none absolute inset-y-0 right-0 hidden w-1/2 rounded-lg sm:block ring-2 ring-primary"
></div>
{:else}
<div
aria-hidden="true"
class="pointer-events-none absolute inset-y-0 right-0 hidden w-1/2 rounded-lg sm:block ring-1 ring-gray-700"
></div>
{/if}
</div>
</div>
</div>
</div>
<div class="border-t border-gray-900/10">
<div class="-mt-px pt-10">
<h3 class="text-sm font-semibold leading-6 text-primary">
{PLAN_NAMES[SubscriptionType.Premium]}
</h3>
<p class="mt-1 text-sm leading-6">
Want all your PocketHost projects in one place? That's what
the Pro tier is all about.
</p>
</div>
<div class="mt-10 space-y-10">
<div>
<h4 class="text-sm font-semibold leading-6">
Catered for business
</h4>
<div class="relative mt-6">
<!-- Fake card background -->
<div
aria-hidden="true"
class="absolute inset-y-0 right-0 hidden w-1/2 rounded-lg bg-gray-800 shadow-sm sm:block"
></div>
<div
class={`relative rounded-lg bg-gray-800 shadow-sm sm:rounded-none sm:bg-transparent sm:shadow-none sm:ring-0`}
>
<dl class="divide-y divide-gray-200 text-sm leading-6">
{#each items as item}
<MobileTable
feature={item.name}
item={item.items[1] || ''}
/>
{/each}
</dl>
</div>
<!-- Fake card border -->
{#if $userSubscriptionType === SubscriptionType.Premium}
<div
aria-hidden="true"
class="pointer-events-none absolute inset-y-0 right-0 hidden w-1/2 rounded-lg sm:block ring-2 ring-primary"
></div>
{:else}
<div
aria-hidden="true"
class="pointer-events-none absolute inset-y-0 right-0 hidden w-1/2 rounded-lg sm:block ring-1 ring-gray-700"
></div>
{/if}
</div>
</div>
</div>
</div>
<div class="border-t border-gray-900/10">
<div class="-mt-px pt-10">
<h3 class="text-sm font-semibold leading-6 text-primary">
{PLAN_NAMES[SubscriptionType.Founder]}
</h3>
<p class="mt-1 text-sm leading-6">
Super elite! The Founder's Edition is our way of saying thanks
for supporting PocketHost in these early days. Choose between
lifetime and annual options.
</p>
</div>
<div class="mt-10 space-y-10">
<div>
<h4 class="text-sm font-semibold leading-6">
Catered for business
</h4>
<div class="relative mt-6">
<!-- Fake card background -->
<div
aria-hidden="true"
class="absolute inset-y-0 right-0 hidden w-1/2 rounded-lg bg-gray-800 shadow-sm sm:block"
></div>
<div
class={`relative rounded-lg bg-gray-800 shadow-sm sm:rounded-none sm:bg-transparent sm:shadow-none sm:ring-0`}
>
<dl class="divide-y divide-gray-200 text-sm leading-6">
{#each items as item}
<MobileTable
feature={item.name}
item={item.items[2] || ''}
/>
{/each}
</dl>
</div>
<!-- Fake card border -->
{#if $userSubscriptionType === SubscriptionType.Founder}
<div
aria-hidden="true"
class="pointer-events-none absolute inset-y-0 right-0 hidden w-1/2 rounded-lg sm:block ring-2 ring-primary"
></div>
{:else}
<div
aria-hidden="true"
class="pointer-events-none absolute inset-y-0 right-0 hidden w-1/2 rounded-lg sm:block ring-1 ring-gray-700"
></div>
{/if}
</div>
</div>
</div>
</div>
</div>
</section>
{/if}
</MediaQuery>
</div>
</div>

View File

@ -1,5 +0,0 @@
<td class="relative px-4 py-0 text-center">
<span class="relative h-full w-full py-3">
<slot />
</span>
</td>

View File

@ -1,6 +0,0 @@
<td class="relative w-1/4 px-4 py-0 text-center">
<span class="relative h-full w-full py-3">
<i class="fa-regular fa-check text-primary"></i>
<span class="sr-only">Yes</span>
</span>
</td>

View File

@ -1,5 +1,5 @@
import { boolean } from 'boolean'
import { InstanceFields } from 'pockethost/common'
import { type InstanceFields, SubscriptionType } from 'pockethost/common'
/**
* These environment variables default to pointing to the production build so
@ -11,13 +11,8 @@ import { InstanceFields } from 'pockethost/common'
export const PUBLIC_APEX_DOMAIN =
import.meta.env.PUBLIC_APEX_DOMAIN || `pockethost.io`
// The domain name where this dashboard lives
export const PUBLIC_APP_URL =
import.meta.env.PUBLIC_APP_URL || `https://app.${PUBLIC_APEX_DOMAIN}`
// The domain name of the lander/marketing site
export const PUBLIC_BLOG_URL =
import.meta.env.PUBLIC_BLOG_URL || `https://${PUBLIC_APEX_DOMAIN}`
import.meta.env.PUBLIC_APP_URL || `https://${PUBLIC_APEX_DOMAIN}`
// The protocol to use, almost always will be https
export const PUBLIC_HTTP_PROTOCOL =
@ -31,72 +26,19 @@ export const PUBLIC_MOTHERSHIP_URL =
// 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.
*
* @example
* mkPath('a', 'b', 'c') // a/b/c
*
* @param {string[]} paths This is an optional list of additional paths to
* append to the lander URL.
*/
const mkPath = (...paths: string[]) => {
return paths.filter((v) => !!v).join('/')
}
/**
* Helpful alias for the lander url.
*
* @example
* LANDER_URL() // https://pockethost.io/
* LANDER_URL('showcase') // https://pockethost.io/showcase/
*
* @param {string[]} paths This is an optional list of additional paths to
* append to the lander URL.
*/
export const LANDER_URL = (...paths: string[]) => {
return `${PUBLIC_BLOG_URL}/${mkPath(...paths)}`
export const MAX_INSTANCE_COUNTS = {
[SubscriptionType.Free]: 25,
[SubscriptionType.Legacy]: 25,
[SubscriptionType.Founder]: 999,
[SubscriptionType.Premium]: 250,
[SubscriptionType.Flounder]: 250, // Added to get the error away
}
export const FREE_MAX_INSTANCE_COUNT = 25
/**
* Helpful alias for the blog url.
*
* @example
* BLOG_URL() // https://pockethost.io/blog
* BLOG_URL('new-features-2023') // https://pockethost.io/blog/new-features-2023/
*
* @param {string[]} paths This is an optional list of additional paths to
* append to the blogs URL.
*/
export const BLOG_URL = (...paths: string[]) => {
return LANDER_URL(`blog`, ...paths)
}
/**
* Helpful alias for the docs url.
*
* @example
* DOCS_URL() // https://pockethost.io/docs
* DOCS_URL('overview', 'help') // https://pockethost.io/docs/overview/help/
*
* @param {string[]} paths This is an optional list of additional paths to
* append to the docs URL.
*/
export const DOCS_URL = (...paths: string[]) => {
return LANDER_URL(`docs`, ...paths)
}
/**
* Helpful alias for the app url.
*
* @example
* APP_URL() // https://app.pockethost.io/
* APP_URL('dashboard') // https://app.pockethost.io/dashboard
*
* @param {string[]} paths This is an optional list of additional paths to
* append to the app URL.
*/
export const APP_URL = (...paths: string[]) => {
return `${PUBLIC_APP_URL}/${mkPath(...paths)}`
}

View File

@ -0,0 +1,42 @@
// shims.d.ts
interface Window {
createLemonSqueezy: () => void
LemonSqueezy: {
/**
* Initialises Lemon.js on your page.
*
* @param options - An object with a single property, eventHandler, which
* is a function that will be called when Lemon.js emits an event.
*/
Setup: (options: {
eventHandler: (event: { event: string }) => void
}) => void
/** Refreshes `lemonsqueezy-button` listeners on the page. */
Refresh: () => void
Url: {
/**
* Opens a given Lemon Squeezy URL, typically these are Checkout or
* Payment Details Update overlays.
*
* @param url - The URL to open.
*/
Open: (url: string) => void
/** Closes the current opened Lemon Squeezy overlay checkout window. */
Close: () => void
}
Affiliate: {
/** Retrieve the affiliate tracking ID */
GetID: () => string
/**
* Append the affiliate tracking parameter to the given URL
*
* @param url - The URL to append the affiliate tracking parameter to.
*/
Build: (url: string) => string
}
}
}

View File

@ -3,26 +3,26 @@ import { createGenericSyncEvent } from '$util/events'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { keys, map } from '@s-libs/micro-dash'
import {
AuthModel,
type AuthModel,
BaseAuthStore,
ClientResponseError,
CreateInstancePayloadSchema,
DeleteInstancePayload,
DeleteInstancePayloadSchema,
DeleteInstanceResult,
PocketBase,
RestCommands,
RestMethods,
UpdateInstancePayload,
UpdateInstancePayloadSchema,
UpdateInstanceResult,
assertExists,
createRestHelper,
type CreateInstancePayload,
CreateInstancePayloadSchema,
type CreateInstanceResult,
type DeleteInstancePayload,
DeleteInstancePayloadSchema,
type DeleteInstanceResult,
type InstanceFields,
type InstanceId,
type InstanceLogFields,
PocketBase,
RestCommands,
RestMethods,
type UpdateInstancePayload,
UpdateInstancePayloadSchema,
type UpdateInstanceResult,
assertExists,
createRestHelper,
} from 'pockethost/common'
export type AuthToken = string

View File

@ -0,0 +1,16 @@
<script>
import UserLoggedIn from '$components/guards/UserLoggedIn.svelte'
import UserLoggedOut from '$components/guards/UserLoggedOut.svelte'
</script>
<div class="m-4">
<UserLoggedIn>
<slot />
</UserLoggedIn>
<UserLoggedOut>
<p>
You must be <a href="/get-started" class="link">logged in</a> to access this
area.
</p>
</UserLoggedOut>
</div>

View File

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

View File

@ -0,0 +1,26 @@
<script lang="ts">
import { userSubscriptionType } from '$util/stores'
import { PLAN_NAMES, SubscriptionType } from 'pockethost'
import { userStore } from '$util/stores'
import { onMount } from 'svelte'
import FlounderCard from './FlounderCard.svelte'
import Avatar from '$src/routes/Navbar/Avatar.svelte'
</script>
<div class="text-xl">My Account</div>
<div>
<Avatar class="w-5 h-5" />
Change your avatar on
<a href="https://gravatar.com/profile" class="link">Gravatar</a>
</div>
<div>
Your plan: <span class="text-success font-bold"
>{PLAN_NAMES[$userSubscriptionType]}</span
>
</div>
<div>
Need to change or cancel? <a href="/support" class="link">Contact support</a>
</div>
<div class="w-[300px] m-4">
<FlounderCard />
</div>

View File

@ -65,8 +65,8 @@
{/each}
</div>
{#if !upgradable}
To change to this plan, contact @noaxis on <a
href={`"${DISCORD_URL}"`}>Discord</a
To change to this plan, <a href="/support" class="link"
>contact support</a
>
{/if}
{/if}

View File

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 364 B

View File

@ -1,6 +1,6 @@
<script>
import { DISCORD_URL } from '$src/env'
import FAQItem from '$src/routes/account/FAQItem.svelte'
import FAQItem from '$src/routes/(app)/account/FAQItem.svelte'
</script>
<div

View File

@ -0,0 +1,46 @@
<script lang="ts">
import { PLAN_NAMES, SubscriptionType } from 'pockethost'
import { userStore } from '$util/stores'
import { onMount } from 'svelte'
import PricingCard from '$src/routes/(static)/pricing/PricingCard.svelte'
export let priceMonthly: [number, string?, number?] = [
359,
'once, use forever',
299,
]
export let priceAnnually: [number, string?, number?] = [
159,
'year (save 55%)',
99,
]
export let comingSoonText = ''
export let comingSoon = false
export let startDate: Date | null = null
</script>
<PricingCard
name={`${PLAN_NAMES[SubscriptionType.Flounder]}`}
qtyRemaining={999}
qtyMax={1000}
{comingSoonText}
{comingSoon}
{startDate}
availableText="Super Secret Stealth Mode Pre Black Friday, Pre Cyber Monday Earlybird Presale 20% Off Discount Code Special"
description="Epic elite! The Flounder's Edition is almost as good as the Founder's edition, and you'll help PocketHost go global"
{priceMonthly}
{priceAnnually}
checkoutMonthURL="https://store.pockethost.io/buy/9ff8775b-6b9e-4aa8-a0ab-dc5e58e25408?checkout[custom][user_id]={$userStore?.id}&checkout[email]={$userStore?.email}&checkout[discount_code]=G0MTI0OQ"
checkoutYearURL="https://store.pockethost.io/buy/82d79f7c-64f6-4c2b-9f58-dcc8951f1cdd?checkout[custom][user_id]={$userStore?.id}&checkout[email]={$userStore?.email}&checkout[discount_code]=G0MTI0OQ"
features={[
`Everything in the ${PLAN_NAMES[SubscriptionType.Premium]} tier`,
`Commemorative Flounder's badge`,
`PocketHost t-shirt`,
`#onlyflounders private discord channel`,
`-Girlfriend`,
]}
fundingGoals={[
`Global regions (approx 40)`,
`Global low latency from anywhere`,
]}
/>

View File

@ -0,0 +1,42 @@
<script lang="ts">
import { MAX_INSTANCE_COUNTS } from '$src/env'
import { globalInstancesStore, userSubscriptionType } from '$util/stores'
import { values } from '@s-libs/micro-dash'
import InstanceList from './InstanceList.svelte'
import { SubscriptionType } from 'pockethost'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
$: instanceCount = values($globalInstancesStore).length
</script>
<svelte:head>
<title>Dashboard - PocketHost</title>
</svelte:head>
<div class="flex flex-row items-center justify-between mb-6 gap-4 pl-4 sm:pl-6 lg:pl-8 pr-4">
<h2 class="text-4xl text-base-content font-bold capitalize">Dashboard</h2>
<a href="/instances/new" class="m-3 btn btn-primary">
<Fa icon={faPlus} /> New Instance</a
>
</div>
<div class="flex flex-row space-x-4 items-center justify-center">
<div>Instances</div>
<progress
class="progress progress-primary w-48 md:w-80"
value={instanceCount}
max={MAX_INSTANCE_COUNTS[$userSubscriptionType]}
></progress>
<div>
{instanceCount}/{MAX_INSTANCE_COUNTS[$userSubscriptionType]}
{#if $userSubscriptionType === SubscriptionType.Free}
<a href="/pricing" class="link text-xs text-success">Upgrade</a>
{/if}
</div>
</div>
<div class="flex flex-wrap gap-2 items-center justify-center">
<InstanceList />
</div>

View File

@ -1,9 +1,12 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { INSTANCE_ADMIN_URL } from '$src/env'
import { client } from '$src/pocketbase-client'
import { globalInstancesStore } from '$util/stores'
import { faCircleInfo } from '@fortawesome/free-solid-svg-icons'
import { values } from '@s-libs/micro-dash'
import { InstanceId } from 'pockethost'
import { type InstanceId } from 'pockethost'
import Fa from 'svelte-fa'
const { updateInstance } = client()
@ -21,30 +24,37 @@
</script>
{#each values($globalInstancesStore).sort( (a, b) => a.subdomain.localeCompare(b.subdomain), ) as instance, index}
<div class="card w-80 bg-neutral m-4">
<div class="card-body">
<button
class={`card min-w-80 lg:max-w-80 flex-1 m-4 transition hover:bg-base-300 ${instance.maintenance ? 'bg-base-200' : 'bg-neutral'}`}
on:click={_=>goto(`/instances/${instance.id}`)}
>
<div class="card-body w-full">
<div class="card-title">
<div class="flex justify-between items-center w-full">
<span
>{instance.subdomain}
<span class="text-xs text-gray-400">
v{instance.version}
</span>
</span>
<span>{instance.subdomain}</span>
<input
type="checkbox"
class="toggle {instance.maintenance
? 'bg-red-500 hover:bg-red-500'
: 'toggle-success'}"
checked={!instance.maintenance}
on:click={e=>e.stopPropagation()}
on:change={handleMaintenanceChange(instance.id)}
/>
</div>
</div>
<p class="text-left">
<span class="text-gray-400"
>Version {instance.version}
<span class={!instance.maintenance ? 'hidden' : ''}
>- Powered Off</span
></span
>
</p>
<div class="card-actions flex justify-between mt-5">
<a href={`/app/instances/${instance.id}`} class="btn btn-primary">
<i class="fa-regular fa-circle-info"></i>
<a href={`/instances/${instance.id}`} class="btn btn-primary">
<Fa icon={faCircleInfo} />
<span>Details</span>
</a>
@ -62,5 +72,5 @@
</a>
</div>
</div>
</div>
</button>
{/each}

View File

@ -6,8 +6,13 @@
import { assert } from 'pockethost/common'
import { instance } from './store'
import { client } from '$src/pocketbase-client'
import { InstanceId } from 'pockethost/common'
import { type InstanceId } from 'pockethost/common'
import Toggle from './Toggle.svelte'
import Fa from 'svelte-fa'
import {
faExternalLinkAlt,
faTriangleExclamation,
} from '@fortawesome/free-solid-svg-icons'
let isReady = false
$: {
@ -46,53 +51,53 @@
</svelte:head>
{#if isReady}
<div class="flex flex-row items-center justify-between mb-6 gap-4">
<div class="flex flex-row items-center justify-between mb-6 gap-4 pl-4 sm:pl-6 lg:pl-8 pr-2">
<div>
<h2
class="text-4xl md:text-left text-center text-base-content font-bold mb-3 break-words"
class="text-4xl md:text-left text-base-content font-bold mb-3 break-words"
>
{$instance.subdomain}<span class="text-xs text-gray-400"
>.pockethost.io</span
>
<span class="text-xs text-gray-400">v{$instance.version}</span>
{$instance.subdomain}
</h2>
<span class="text-gray-400">
Version {$instance.version} - <span class="capitalize">{$instance.status}</span>
</span>
</div>
<div>
<Toggle
title="Power"
checked={!$instance.maintenance}
onChange={handleMaintenanceChange($instance.id)}
/>
</div>
</div>
{#if $instance.maintenance}
<AlertBar
message="This instance is turned off and will not respond to requests"
type="warning"
/>
<div class="px-4 mb-8">
<AlertBar
message="This instance is turned off and will not respond to requests"
type="warning"
/>
</div>
{/if}
<div class="flex gap-4">
<div class="w-48">
<ul>
<!-- Consistency is key -->
<div class="flex gap-4 mr-4">
<div class="flex flex-col w-56">
<ul class="menu text-base-content mb-6">
<li>
<a href={`/app/instances/${id}`} class={activeClass(id)}>Overview</a>
<a href={`/instances/${id}`} class={activeClass(id)}>Overview</a>
</li>
<li>
<a
href={`/app/instances/${id}/secrets`}
class={activeClass(`secrets`)}>Secrets</a
>
href={`/instances/${id}/secrets`}
class={activeClass(`secrets`)}>Secrets</a>
</li>
<li>
<a href={`/app/instances/${id}/logs`} class={activeClass(`logs`)}
>Logs</a
>
<a href={`/instances/${id}/logs`} class={activeClass(`logs`)}>Logs</a>
</li>
<li>
<a href={`/app/instances/${id}/ftp`} class={activeClass(`ftp`)}
<a href={`/instances/${id}/ftp`} class={activeClass(`ftp`)}
>FTP Access</a
>
</li>
@ -108,24 +113,23 @@
class="w-6 inline-block"
/>
Admin
<i class="fa-solid fa-external-link-alt text-xs"></i>
<Fa icon={faExternalLinkAlt} class="ml-2 text-xs" />
</a>
</li>
</ul>
<div class="divider"></div>
<div class="mt-2 mb-2">
<i class="fa-solid fa-siren-on text-error"></i>
</ul>
<div class="pl-6">
<Fa icon={faTriangleExclamation} class="text-error inline" />
<span class=" font-bold text-error">Danger Zone</span>
<i class="fa-solid fa-siren-on text-error"></i>
<Fa icon={faTriangleExclamation} class="text-error inline" />
</div>
<ul>
<li><a href={`/app/instances/${id}/version`}>Change Version</a></li>
<li><a href={`/app/instances/${id}/domain`}>Custom Domain</a></li>
<li><a href={`/app/instances/${id}/admin-sync`}>Admin Sync</a></li>
<li><a href={`/app/instances/${id}/dev`}>Dev Mode</a></li>
<li><a href={`/app/instances/${id}/rename`}>Rename</a></li>
<ul class="menu text-base-content">
<li><a href={`/instances/${id}/version`} class={activeClass(`version`)}>Change Version</a></li>
<li><a href={`/instances/${id}/domain`} class={activeClass(`domain`)}>Custom Domain</a></li>
<li><a href={`/instances/${id}/admin-sync`} class={activeClass(`admin-sync`)}>Admin Sync</a></li>
<li><a href={`/instances/${id}/dev`} class={activeClass(`dev`)}>Dev Mode</a></li>
<li><a href={`/instances/${id}/rename`} class={activeClass(`rename`)}>Rename</a></li>
<li>
<a href={`/app/instances/${id}/delete`} class="btn btn-error btn-xs"
<a href={`/instances/${id}/delete`} class={`text-error ${activeClass(`delete`)}`}
>Delete</a
>
</li>
@ -134,7 +138,7 @@
<div class="w-full">
{#key $page.url.pathname}
<article class="prose">
<article class="flex flex-col gap-4">
<slot />
</article>
{/key}

View File

@ -1,13 +1,9 @@
<script lang="ts">
import { assertExists } from 'pockethost/common'
import Code from './Overview.svelte'
import Ftp from './ftp/+page.svelte'
import { instance } from './store'
$: ({ status, version, id } = $instance)
assertExists($instance, `Expected instance here`)
const { subdomain } = $instance
</script>
<Code />

View File

@ -1,7 +1,7 @@
<script lang="ts">
import CodeSample from '$components/CodeSample.svelte'
import CardHeader from '$src/components/cards/CardHeader.svelte'
import { DISCORD_URL, DOCS_URL, INSTANCE_URL } from '$src/env'
import { DISCORD_URL, INSTANCE_URL } from '$src/env'
import { instance } from './store'
let installSnippet = `npm i pocketbase`
@ -18,22 +18,21 @@
});`
</script>
<CardHeader documentation={DOCS_URL(`/usage/accessing-instance/`)}
>Overview</CardHeader
>
<CardHeader documentation={`/docs/accessing-instance/`}>Overview</CardHeader>
<div class="mb-4">
<p>Your PocketBase URL is</p>
<!-- These should be p but the inside already has p -->
<div>
<p class="mb-2">Your PocketBase URL is</p>
<CodeSample code={url} />
</div>
<div class="mb-4">
<p>Installing PocketBase</p>
<div>
<p class="mb-2">Installing PocketBase</p>
<CodeSample code={installSnippet} />
</div>
<div class="mb-4">
<p>Connecting to Your Instance</p>
<div>
<p class="mb-2">Connecting to Your Instance</p>
{#if $instance.cname}
{#if $instance.cname_active}
<div class="text-accent">Notice: You are in Custom Domain mode</div>
@ -49,25 +48,27 @@
<CodeSample code={connectionSnippet} />
</div>
<div class="mb-4">
<p>Making Your First Query</p>
<div>
<p class="mb-2">Making Your First Query</p>
<CodeSample code={firstQuerySnippet} />
</div>
<p>Additional Resources:</p>
<ul class="list-disc pl-4">
<li>
<a
href={`https://pocketbase.io/docs/api-records/`}
target="_blank"
class="link">PocketBase Web APIs</a
>
</li>
<li>
<a
href="https://www.npmjs.com/package/pocketbase"
target="_blank"
class="link">PocketBase NPM Package</a
>
</li>
</ul>
<div>
<p>Additional Resources:</p>
<ul class="list-disc pl-4">
<li>
<a
href={`https://pocketbase.io/docs/api-records/`}
target="_blank"
class="link">PocketBase Web APIs</a
>
</li>
<li>
<a
href="https://www.npmjs.com/package/pocketbase"
target="_blank"
class="link">PocketBase NPM Package</a
>
</li>
</ul>
</div>

View File

@ -1,10 +1,9 @@
<script lang="ts">
export let checked = false
export let title = ''
export let onClass = 'success'
export let offClass = 'red-500'
export let onText = 'on'
export let offText = 'off'
export let onText = 'ON'
export let offText = 'OFF'
export let onChange = (isChecked: boolean) => {}
const handleChange = (e: Event) => {
@ -17,9 +16,9 @@
<div class="form-control w-fit">
<label class="label cursor-pointer">
<span class="label-text mr-2"
>{title} is
<span class="text-{checked ? onClass : offClass}"
<span class="label-text text-lg mr-2"
>
<span class="font-bold text-{checked ? onClass : offClass}"
>{checked ? onText : offText}</span
></span
>

View File

@ -1,7 +1,6 @@
<script lang="ts">
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { DOCS_URL } from '$src/env'
import { client } from '$src/pocketbase-client'
import { instance } from '../store'
import ErrorMessage from '../settings/ErrorMessage.svelte'
@ -23,14 +22,12 @@
}
</script>
<CardHeader documentation={DOCS_URL(`/usage/admin-sync`)}>
Admin Sync
</CardHeader>
<CardHeader documentation={`/docs/admin-sync`}>Admin Sync</CardHeader>
<p class="mb-8">
Your instance will have an admin login that matches your pockethost.io login.
Admin Sync ensures that your instance always has an admin account that matches the login credentials of your pockethost.io account.
</p>
<ErrorMessage message={errorMessage} />
<Toggle title="Admin Sync" checked={!!syncAdmin} onChange={handleChange} />
<Toggle checked={!!syncAdmin} onChange={handleChange} />

View File

@ -2,7 +2,6 @@
import { goto } from '$app/navigation'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { DOCS_URL } from '$src/env'
import { client } from '$src/pocketbase-client'
import { globalInstancesStore } from '$util/stores'
import { instance } from '../store'
@ -48,7 +47,7 @@
delete newInstances[id]
return newInstances
})
goto('/')
goto('/dashboard')
})
.catch((error) => {
console.error(error)
@ -64,9 +63,7 @@
}
</script>
<CardHeader documentation={DOCS_URL(`/usage/delete`)}>
Delete Instance
</CardHeader>
<CardHeader documentation={`/docs/delete`}>Delete Instance</CardHeader>
{#if !maintenance}
<AlertBar

View File

@ -1,7 +1,5 @@
<script lang="ts">
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { DOCS_URL } from '$src/env'
import { client } from '$src/pocketbase-client'
import { instance } from '../store'
import ErrorMessage from '../settings/ErrorMessage.svelte'
@ -22,7 +20,7 @@
}
</script>
<CardHeader documentation={DOCS_URL(`/usage/dev-mode`)}>Dev Mode</CardHeader>
<CardHeader documentation={`/docs/dev-mode`}>Dev Mode</CardHeader>
<p class="mb-8">
Starting with PocketBase v0.20.1, your instance will show debugging output in

View File

@ -1,9 +1,8 @@
<script lang="ts">
import AlertBar from '$components/AlertBar.svelte'
import CodeSample from '$components/CodeSample.svelte'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { DOCS_URL, INSTANCE_BARE_HOST } from '$src/env'
import { INSTANCE_BARE_HOST } from '$src/env'
import { client } from '$src/pocketbase-client'
import { isUserPaid, userSubscriptionType } from '$util/stores'
import { SubscriptionType } from 'pockethost/common'
@ -73,52 +72,58 @@
}
</script>
<CardHeader documentation={DOCS_URL(`/usage/custom-domain`)}>
Custom Domain (CNAME)
</CardHeader>
<div class="max-w-2xl">
<CardHeader documentation={`/docs/custom-domains`}>
Custom Domain (CNAME)
</CardHeader>
<div class="mb-8">
Use a custom domain (CNAME) with your PocketHost instance.
</div>
{#if formCname && regex.test(formCname.trim())}
<div class="mb-8">Go to your DNS provider and add a CNAME entry.</div>
<div class="mb-4">
<CodeSample
code={`${formCname} CNAME ${INSTANCE_BARE_HOST($instance)}`}
language={dns}
/>
<div class="mb-8">
Use a custom domain (CNAME) with your PocketHost instance.
</div>
{/if}
<AlertBar message={successMessage} type="success" />
<AlertBar message={errorMessage} type="error" />
{#if cname}
{#if !cname_active}
<AlertBar
message={`Your custom domain name is pending. Go find <a class='btn btn-primary' target='_blank' href="https://discord.com/channels/1128192380500193370/1189948945967882250">@noaxis on Discord</a> to complete setup.`}
type="warning"
/>
{:else}
<AlertBar message={`Your custom domain name is active.`} type="success" />
{#if cname && regex.test(formCname.trim())}
<div class="mb-8">Go to your DNS provider and add a CNAME entry.</div>
<div class="mb-4">
<CodeSample
code={`${formCname} CNAME ${INSTANCE_BARE_HOST($instance)}`}
language={dns}
/>
</div>
{/if}
{/if}
<form
class="flex rename-instance-form-container-query gap-4"
on:submit={onRename}
>
<input
title="Only valid domain name patterns are allowed"
type="text"
bind:value={formCname}
class="input input-bordered w-full"
/>
<AlertBar message={successMessage} type="success" flash />
<AlertBar message={errorMessage} type="error" />
<button type="submit" class="btn btn-error" disabled={isButtonDisabled}
>Update Custom Domain</button
{#if cname}
{#if !cname_active}
<AlertBar
message={`Your custom domain name is pending. Go to <a href="/support" class="link text-warning-content">Support</a> to complete the setup.`}
type="warning"
/>
{:else}
<AlertBar
message={`Your custom domain name is active.`}
type="success"
flash
/>
{/if}
{/if}
<form
class="flex rename-instance-form-container-query gap-4"
on:submit={onRename}
>
</form>
<input
title="Only valid domain name patterns are allowed"
type="text"
bind:value={formCname}
class="input input-bordered w-full"
/>
<button type="submit" class="btn btn-error" disabled={isButtonDisabled}
>Update Custom Domain</button
>
</form>
</div>
<style>
.rename-instance-form-container-query {

View File

@ -1,8 +1,7 @@
<script lang="ts">
import CodeSample from '$components/CodeSample.svelte'
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { DOCS_URL, FTP_URL } from '$src/env'
import { FTP_URL } from '$src/env'
import { client } from '$src/pocketbase-client'
import { bash } from 'svelte-highlight/languages'
@ -16,7 +15,7 @@
const ftpUrl = FTP_URL(email)
</script>
<CardHeader documentation={DOCS_URL(`/usage/ftp`)}>FTP Access</CardHeader>
<CardHeader documentation={`/docs/ftp`}>FTP Access</CardHeader>
<div class="mb-8">
Securely access your instance files via FTPS. Use your PocketHost account
login and password.

View File

@ -3,12 +3,19 @@
import { mkCleanup } from '$util/componentCleanup'
import {
StreamNames,
Unsubscribe,
type Unsubscribe,
type InstanceLogFields,
} from 'pockethost/common'
import { onMount, tick } from 'svelte'
import { derived, writable } from 'svelte/store'
import { instance } from '../store'
import CardHeader from '$src/components/cards/CardHeader.svelte'
import Fa from 'svelte-fa'
import {
faArrowDown,
faClose,
faUpRightAndDownLeftFromCenter,
} from '@fortawesome/free-solid-svg-icons'
$: ({ id } = $instance)
@ -82,7 +89,8 @@
})
</script>
<h2>Logs</h2>
<!-- Consistency is key -->
<CardHeader>Logs</CardHeader>
<div class="mb-4">
Instance logs appear here in realtime, including <code>console.log</code> from
@ -95,11 +103,7 @@
class="btn btn-sm absolute top-[6px] right-[6px]"
on:click={() => (autoScroll = !autoScroll)}
>AutoScroll
<i
class="fa-regular"
class:fa-close={!autoScroll}
class:fa-arrow-down={autoScroll}
/>
<Fa icon={autoScroll ? faArrowDown : faClose} />
</button>
<h3 class="font-bold text-lg">Instance Logging</h3>
@ -109,7 +113,7 @@
>
{#each $logs as log}
<div
class="px-4 text-[11px] font-mono flex align-center"
class="px-4 text-[16px] font-mono flex align-center"
data-prefix=">"
>
<div>
@ -132,19 +136,15 @@
<div class="flex flex-row absolute top-[6px] right-[6px] gap-1">
<button class="btn btn-sm" on:click={() => (autoScroll = !autoScroll)}
>AutoScroll
<i
class="fa-regular"
class:fa-close={!autoScroll}
class:fa-arrow-down={autoScroll}
/>
<Fa icon={autoScroll ? faArrowDown : faClose} />
</button>
<button class="btn btn-sm" on:click={handleFullScreenModal}
>Fullscreen <i class="fa-regular fa-arrows-maximize" /></button
>Fullscreen <Fa icon={faUpRightAndDownLeftFromCenter} /></button
>
</div>
<div class="h-[450px] flex flex-col overflow-y-scroll" bind:this={logElement}>
<div class="h-[50vh] flex flex-col overflow-y-scroll" bind:this={logElement}>
{#each $logs as log}
<div class="px-4 text-[11px] font-mono flex align-center" data-prefix=">">
<div class="px-4 text-[16px] font-mono flex align-center" data-prefix=">">
<div>
<span class="mr-1 text-accent">{log.time}</span>

View File

@ -1,7 +1,5 @@
<script lang="ts">
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { DOCS_URL } from '$src/env'
import { client } from '$src/pocketbase-client'
import { instance } from '../store'
import AlertBar from '$components/AlertBar.svelte'
@ -56,9 +54,7 @@
}
</script>
<CardHeader documentation={DOCS_URL(`/usage/rename-instance`)}>
Rename Instance
</CardHeader>
<CardHeader documentation={`/docs/rename-instance`}>Rename Instance</CardHeader>
<p class="mb-8">
Renaming your instance will cause it to become <strong class="text-error"

View File

@ -2,6 +2,7 @@
import { assertExists } from 'pockethost/common'
import { instance } from '../store'
import SecretsInner from './SecretsInner.svelte'
import CardHeader from '$src/components/cards/CardHeader.svelte';
$: ({ status, version, id } = $instance)
@ -13,5 +14,6 @@
<title>{subdomain} secrets - PocketHost</title>
</svelte:head>
<h2>Secrets</h2>
<!-- Consistency is key -->
<CardHeader>Secrets</CardHeader>
<SecretsInner />

View File

@ -2,9 +2,14 @@
import AlertBar from '$components/AlertBar.svelte'
import { client } from '$src/pocketbase-client/index.js'
import { reduce } from '@s-libs/micro-dash'
import { SECRET_KEY_REGEX, UpdateInstancePayload } from 'pockethost/common'
import {
SECRET_KEY_REGEX,
type UpdateInstancePayload,
} from 'pockethost/common'
import { instance } from '../store.js'
import { items } from './stores.js'
import { faFloppyDisk } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
// Keep track of the new key and value to be added
let secretKey: string = ''
@ -78,10 +83,12 @@
}
</script>
<h3 class="text-xl">Add New Secret</h3>
<div class="mb-8">
{#if successfulSave}
<AlertBar
message="Your new environment variable has been saved."
message="Your new secret has been saved."
type="success"
/>
{/if}
@ -89,39 +96,50 @@
<AlertBar message={errorMessage} type="error" />
<form on:submit={handleSubmit} class="mb-4">
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<div class="flex flex-row gap-4 mb-4">
<label class="flex-1 form-control">
<input
id="secret-key"
type="text"
bind:value={secretKey}
placeholder="KEY"
class="input input-bordered w-full max-w-xs"
placeholder="Key"
class={`input input-bordered ${!isKeyValid && secretKey.length > 0 ? "input-error text-error" : ""}`}
/>
</div>
{#if !isKeyValid && secretKey.length > 0}
<div class="label">
<span class="text-error">
All key names must be upper case, alphanumeric, and may include underscore (_).
</span>
</div>
{/if}
<div>
</label>
<div class="flex-1 form-control">
<input
id="secret-value"
type="text"
bind:value={secretValue}
placeholder="VALUE"
class="input input-bordered w-full max-w-xs"
placeholder="Value"
class="input input-bordered"
/>
</div>
<div class="flex-none text-right">
<button type="submit" class="btn btn-primary" disabled={!isFormValid}>
Add <Fa icon={faFloppyDisk} />
</button>
</div>
</div>
{#if !isKeyValid && secretKey.length > 0}
<!-- {#if !isKeyValid && secretKey.length > 0}
<AlertBar
message="All key names must be upper case, alphanumeric, and may include underscore (_)."
type="error"
/>
{/if}
{/if} -->
<div class="text-right">
<button type="submit" class="btn btn-primary" disabled={!isFormValid}
>Add <i class="fa-regular fa-floppy-disk"></i></button
>
</div>
</form>
</div>

View File

@ -4,7 +4,9 @@
import { client } from '$src/pocketbase-client'
import { instance } from '../store'
import { reduce } from '@s-libs/micro-dash'
import { logger, UpdateInstancePayload } from 'pockethost'
import { logger, type UpdateInstancePayload } from 'pockethost'
import Fa from 'svelte-fa'
import { faTrash } from '@fortawesome/free-solid-svg-icons'
const handleDelete = (name: string) => async (e: Event) => {
e.preventDefault()
@ -27,7 +29,7 @@
}
</script>
<table class="table">
<table class="table mb-8">
<thead>
<tr>
<th class="w-2/5 border-b-2 border-neutral">Key</th>
@ -50,7 +52,7 @@
on:click={handleDelete(item.name)}
type="button"
class="btn btn-sm btn-square btn-outline btn-warning"
><i class="fa-regular fa-trash"></i></button
><Fa icon={faTrash} /></button
>
</td>
</tr>

View File

@ -5,6 +5,8 @@
import Form from './Form.svelte'
import List from './List.svelte'
import { items } from './stores'
import Fa from 'svelte-fa'
import { faUserSecret } from '@fortawesome/free-solid-svg-icons'
$: {
const { id, secrets } = $instance
@ -40,11 +42,11 @@
{/if}
{#if $items.length === 0}
<div class="alert border-2 border-neutral mb-8">
<i class="fa-regular fa-shield-keyhole"></i>
<span>No Environment Variables Found</span>
<div class="alert border-2 border-primary mb-8">
<Fa icon={faUserSecret} />
<span>No secrets yet. Create your first secret to get started.</span>
</div>
{:else}
<List />
{/if}
<List />
<Form />

View File

@ -1,4 +1,6 @@
<script lang="ts">
import { faCircleExclamation } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
import { slide } from 'svelte/transition'
export let message = ''
@ -6,7 +8,7 @@
{#if message}
<div in:slide class="alert alert-error mb-4">
<i class="fa-regular fa-circle-exclamation"></i>
<Fa icon={faCircleExclamation} />
{message}
</div>
{/if}

View File

@ -1,7 +1,5 @@
<script lang="ts">
import Card from '$components/cards/Card.svelte'
import CardHeader from '$components/cards/CardHeader.svelte'
import { DOCS_URL } from '$src/env'
import { client } from '$src/pocketbase-client'
import { instance } from '../store'
import VersionPicker from './VersionPicker.svelte'
@ -58,9 +56,7 @@
}
</script>
<CardHeader documentation={DOCS_URL(`/usage/upgrading`)}>
Version Change
</CardHeader>
<CardHeader documentation={`/docs/upgrading`}>Version Change</CardHeader>
{#if !maintenance}
<AlertBar

View File

@ -40,7 +40,7 @@
</script>
<select
class="select w-full max-w-xs"
class="select select-bordered w-full"
bind:value={selectedVersion}
on:change={handleSelect}
{disabled}

View File

@ -8,12 +8,14 @@
import { SubscriptionType } from 'pockethost/common'
import Creator from './Creator.svelte'
import Paywall from './Paywall.svelte'
import { MAX_INSTANCE_COUNTS } from '$src/env'
let instanceCount = 0
let canCreate = false
$: {
console.log(MAX_INSTANCE_COUNTS[$userSubscriptionType])
instanceCount = values($globalInstancesStore).length
canCreate = $isUserPaid || instanceCount === 0
canCreate = instanceCount < MAX_INSTANCE_COUNTS[$userSubscriptionType]
}
</script>
@ -21,12 +23,16 @@
<title>New Instance - PocketHost</title>
</svelte:head>
<h2 class="text-4xl text-base-content font-bold capitalize mb-6">
Create A New App
</h2>
<div class="flex items-center justify-center">
<div class="max-w-md">
<h2 class="text-4xl text-base-content font-bold capitalize mb-6">
Create A New Instance
</h2>
{#if canCreate}
<Creator />
{:else}
<Paywall />
{/if}
{#if canCreate}
<Creator />
{:else}
<Paywall />
{/if}
</div>
</div>

View File

@ -3,6 +3,11 @@
import CardHeader from '$components/cards/CardHeader.svelte'
import { client } from '$src/pocketbase-client'
import { handleCreateNewInstance } from '$util/database'
import {
faArrowsRotate,
faCircleExclamation,
} from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
import { writable } from 'svelte/store'
import { slide } from 'svelte/transition'
@ -85,63 +90,61 @@
}
</script>
<div class="grid lg:grid-cols-2 grid-cols-1">
<Card>
<form on:submit={handleSubmit}>
<CardHeader>Choose a name for your PocketBase app.</CardHeader>
<Card>
<form on:submit={handleSubmit}>
<CardHeader>Choose a name for your PocketBase instance.</CardHeader>
<div class="flex rename-instance-form-container-query gap-4">
<input
type="text"
bind:value={$instanceNameField}
class="input input-bordered w-full"
/>
<div class="flex rename-instance-form-container-query gap-4">
<input
type="text"
bind:value={$instanceNameField}
class="input input-bordered w-full"
/>
<button
type="button"
class="btn btn-outline btn-secondary"
aria-label="Regenerate Instance Name"
on:click={handleInstanceNameRegeneration}
><i class="fa-regular fa-arrows-rotate"></i></button
<button
type="button"
class="btn btn-outline btn-secondary"
aria-label="Regenerate Instance Name"
on:click={handleInstanceNameRegeneration}
><Fa icon={faArrowsRotate} /></button
>
</div>
<div style="font-size: 15px;" class="p-2 mb-8">
{#if $instanceInfo.fetching}
Verifying...
{:else if $instanceInfo.available}
<span class="text-success">
https://{$instanceInfo.name}.pockethost.io ✔︎</span
>
{:else}
<span class="text-error">
https://{$instanceInfo.name}.pockethost.io ❌</span
>
</div>
<div style="font-size: 15px;" class="p-2 mb-8">
{#if $instanceInfo.fetching}
Verifying...
{:else if $instanceInfo.available}
<span class="text-success">
https://{$instanceInfo.name}.pockethost.io ✔︎</span
>
{:else}
<span class="text-error">
https://{$instanceInfo.name}.pockethost.io ❌</span
>
{/if}
</div>
{#if formError}
<div transition:slide class="alert alert-error mb-5">
<i class="fa-solid fa-circle-exclamation"></i>
<span>{formError}</span>
</div>
{/if}
</div>
<div class="flex items-center justify-center gap-4">
<a href="/" class="btn">Cancel</a>
<button
type="submit"
class="btn btn-primary"
disabled={isFormButtonDisabled}
>
{#if isSubmitting}
<span class="loading loading-spinner loading-md"></span>
{:else}
Create <i class="bi bi-arrow-right-short" />
{/if}
</button>
{#if formError}
<div transition:slide class="alert alert-error mb-5">
<Fa icon={faCircleExclamation} />
<span>{formError}</span>
</div>
</form>
</Card>
</div>
{/if}
<div class="flex items-center justify-center gap-4">
<a href="/" class="btn">Cancel</a>
<button
type="submit"
class="btn btn-primary"
disabled={isFormButtonDisabled}
>
{#if isSubmitting}
<span class="loading loading-spinner loading-md"></span>
{:else}
Create <i class="bi bi-arrow-right-short" />
{/if}
</button>
</div>
</form>
</Card>

View File

@ -0,0 +1,23 @@
<script lang="ts">
import { MAX_INSTANCE_COUNTS } from '$src/env'
import { userSubscriptionType } from '$util/stores'
import { PLAN_NAMES, SubscriptionType } from 'pockethost/common'
</script>
<div class="card max-w-sm bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">Paywall!</h2>
<p>Oof. You hit a paywall.</p>
<p class="text-error">
You're only allowed {MAX_INSTANCE_COUNTS[$userSubscriptionType]} projects on
the {PLAN_NAMES[$userSubscriptionType]} plan.
</p>
<p>
But that's okay, we know you want to support PocketHost if you love it
this much!
</p>
<div class="card-actions justify-end">
<a href="/pricing" class="btn btn-primary">Unlock More Projects</a>
</div>
</div>
</div>

View File

@ -22,8 +22,14 @@
<title>Stats - PocketHost</title>
</svelte:head>
<div>
{#each Object.entries($stats) as [key, idx]}
<div>{key}: {idx}</div>
{/each}
<div class="m-10">
<div class="text-xl">Stats</div>
<table>
{#each Object.entries($stats) as [key, idx]}
<tr>
<td>{key}</td>
<td>{idx}</td>
</tr>
{/each}
</table>
</div>

View File

@ -0,0 +1,3 @@
<div class="prose m-20">
<slot />
</div>

View File

@ -0,0 +1,26 @@
# About PocketHost
## Our Journey
PocketHost was founded in 2021 by [Ben Allfree](https://github.com/benallfree), a passionate developer seeking a more efficient way to manage **PocketBase** instances. Frustrated with the repetitive task of setting up PocketBase every time he started a new project, Ben created PocketHost to simplify this process not only for himself but for developers everywhere.
## Open Source Commitment
We believe in the power of collaboration and transparency. PocketHost is a completely open-source project licensed under the MIT License. This means you can inspect, modify, and contribute to the codebase, fostering a community-driven platform that continually evolves to meet the needs of its users.
## Growth and Community
Since its inception, PocketHost has grown organically to serve over **10,000 users** worldwide. This incredible growth reflects our commitment to providing a simple, reliable, and efficient hosting solution for indie hackers, makers, and small businesses.
## Technology and Innovation
- **Docker Containerization**: We utilize a Docker containerized architecture to ensure isolation and security for each project hosted on our platform.
- **Dynamic Resource Allocation**: Our innovative approach allows us to run thousands of instances on a single Virtual Private Server (VPS). This efficient use of resources ensures optimal performance and scalability for all users.
## Special Thanks
We extend our heartfelt gratitude to **Gani** at [PocketBase](https://pocketbase.io/) for his unwavering support and invaluable advice. His contributions have been instrumental in shaping PocketHost into the robust platform it is today.
## Join Us
PocketHost is more than just a hosting service; it's a community of creators and innovators. Whether you're developing your next big idea or need a reliable hosting solution for your business, we're here to support you every step of the way.

View File

@ -0,0 +1,120 @@
<div class="prose">
# Privacy Policy
**Last Updated: October 5, 2024**
At PocketHost, we are committed to protecting your privacy and ensuring the security of your data. This Privacy Policy outlines how we collect, use, and safeguard your information when you use our services.
## 1. Introduction
PocketHost provides hosting services for PocketBase applications. By using our services, you agree to the collection and use of information in accordance with this policy.
## 2. Data Collection and Usage
### 2.1 Information We Collect
- **Personal Information**: We collect only the personal information that you voluntarily provide to us, such as your email address. This information is necessary for account creation, authentication, and communication purposes.
### 2.2 How We Use Your Information
- **Communication**: Your email address is used solely for transactional communications (like account confirmations, password resets, and service notifications) and for sending you updates about our services.
- **No Third-Party Sales**: We do not sell or rent your personal information to any third parties.
- **Authorized Third Parties**: We may share your information with third-party service providers who are authorized to communicate on our behalf, solely for the purpose of providing our services to you.
### 2.3 Aggregate Data
- **Public Statistics**: We may publish aggregate data such as user counts, instance counts, and other platform statistics. This information does not include any personally identifiable information and is used to inform the public about our platform's usage.
### 2.4 Anonymized Troubleshooting
- **Community Support**: Occasionally, we may discuss particular user behaviors on platforms like Discord for troubleshooting purposes. These discussions are always conducted anonymously to protect your identity.
## 3. Data Storage and Security
### 3.1 Hosting Environment
We utilize trusted third-party providers, **DigitalOcean** and **Fly.io**, to host our infrastructure.
#### DigitalOcean and Fly.io Security Measures
- **At-Rest Encryption**: Both providers offer encryption of volume storage at rest, protecting data against unauthorized physical access.
- **Network Security**: Advanced network security measures like firewalls and intrusion detection systems are employed.
- **Compliance Standards**: They comply with industry-leading security certifications and regulations such as GDPR, ISO 27001, and SOC 2 Type II.
- **Access Controls**: Strict access controls with multi-factor authentication and role-based permissions are in place.
- **Regular Backups and Redundancy**: Regular backups and redundant systems prevent data loss and ensure high availability.
### 3.2 PocketBase Instances
Each PocketBase instance runs in a secure Docker container with access only to its own data, enhancing security through isolation.
#### Data at Rest
- **Unencrypted Data Storage**: While VPS volumes are encrypted, data within each PocketBase instance—including SQLite databases and uploaded files—is stored unencrypted at rest due to PocketBase's lack of at-rest encryption support.
- **Potential Risks**: A breach of administrative access to the VPS could potentially expose unencrypted user data within your PocketBase instance.
### 3.3 SSH Security and Data Encryption
- **SSH Access**: Our servers are secured using 2048-bit SSH keys, ensuring that only authorized personnel can access them.
- **In-Flight Data Encryption**: All data transmitted between our servers is encrypted using industry-standard protocols, safeguarding against interception.
### 3.4 Use of Cloudflare
We utilize **Cloudflare** services to enhance security and performance.
- **Caching**: Cloudflare provides intelligent caching to improve load times.
- **Origin Security**: Acts as a reverse proxy, offering DDoS mitigation, Web Application Firewall (WAF) protection, and SSL/TLS encryption.
- **SSL/TLS Encryption**: All connections between end-users and Cloudflare are encrypted, ensuring secure data transmission.
### 3.5 Recommendations for Enhanced Security
- **Use of S3-Compatible Storage**: We strongly encourage users to configure PocketBase to use an S3-compatible storage service (like Amazon S3, Backblaze B2, or Wasabi) for file storage and backups, which offer encrypted at-rest storage.
- **Encrypted Backups**: Utilizing S3 storage for backups ensures that your data is encrypted during transit and at rest.
## 4. Cookies and Tracking Technologies
- **Authentication and Analytics**: We use cookies to manage user authentication and gather analytics to improve our services.
- **No Ad Tracking**: We do not use cookies or scripts for ad tracking or any similar purposes.
- **Opt-Out**: You can configure your browser settings to refuse cookies, but this may affect the functionality of our services.
## 5. Data Sharing and Disclosure
- **No Selling of Personal Data**: We do not sell or rent your personal information to anyone.
- **Third-Party Service Providers**: We may share your information with third-party service providers who assist us in operating our services, under strict confidentiality agreements.
- **Legal Requirements**: We may disclose your information if required to do so by law or in response to valid requests by public authorities.
## 6. User Rights
- **Access and Correction**: You have the right to access and correct your personal data at any time.
- **Data Portability**: You can request a copy of your data in a structured, machine-readable format.
- **Deletion**: You may request the deletion of your personal data, subject to certain legal obligations.
## 7. Changes to This Privacy Policy
We may update our Privacy Policy periodically. We will notify you of any significant changes by posting the new Privacy Policy on this page with an updated effective date.
## 8. Contact Us
If you have any questions or concerns about this Privacy Policy, please contact us at:
- **Email**: ben@pockethost.io
- **Address**: PO Box 871, Reno NV 89501.
</div>

View File

@ -0,0 +1,137 @@
<div class="prose">
# Terms of Service
**Effective Date: October 5, 2024**
Welcome to PocketHost! These Terms of Service ("Terms") govern your access to and use of PocketHost's services, so please read them carefully.
By accessing or using our services, you agree to be bound by these Terms. If you do not agree to these Terms, please do not use our services.
## 1. Introduction
- **Company Name**: PocketHost
- **Address**: PO Box 871, Reno, NV 89501, USA
- **Contact Information**: [ben@pockethost.io](mailto:ben@pockethost.io)
## 2. Eligibility
There are no age restrictions for using our services. However, by using PocketHost, you represent and warrant that you have the legal capacity to enter into these Terms.
## 3. Account Registration and Security
- **Account Creation**: To use our services, you must create an account by providing a valid email address.
- **Account Security**: You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account.
- **Unauthorized Access**: If we detect unauthorized access to your account, we may suspend it to protect your security.
## 4. Services and Subscriptions
### 4.1 Paid Membership Services
- **Expanded Capacity and Access**: We offer paid membership services for users requiring expanded capacity and access.
- **Payment Processing**: All payments are handled through third-party providers **Stripe** and **LemonSqueezy**.
- **Subscription Plans**: We offer both annual and monthly subscription plans with no additional fees.
### 4.2 Refund Policy
- **No-Questions-Asked Refunds**: If you're not satisfied with our services, you may request a refund within **30 days** of purchase, and it will be granted without questions.
- **Extended Refunds**: We may provide refunds beyond the 30-day window at our discretion. Our goal is to ensure customer satisfaction.
## 5. Fair Use Policy
PocketHost offers generous free projects, storage, bandwidth, and CPU on a Fair Use basis.
### 5.1 What is 'Fair'?
We consider 'fair' use to mean that you are using approximately the same amount of bandwidth, storage, and CPU resources as the average active app on our platform.
PocketHost achieves economies of scale through dynamic management of resources. Relatively low-traffic apps do not require many resources, allowing high-traffic apps to utilize our ample resources. Your app will scale up and down depending on its needs.
If we notice that your app is consistently using so many resources that it is starting to affect the experience of other users, or if it is starting to cost significantly more than you are paying, we will reach out to you to discuss the situation. Obviously, anything taken to the extreme is impractical—for example, creating one billion projects will prompt a conversation with us.
Our ethos is centered around supporting indie developers and hackers. We actively avoid the utility hosting provider model with metered usage. We encourage you to keep this in mind and be a good citizen of our platform, and we will reciprocate the courtesy.
## 6. User Content and Acceptable Use
### 6.1 User Content
- **Ownership**: You retain all rights to the content and source code you upload to our platform.
- **Content Responsibility**: You are responsible for all content you upload and must ensure it complies with these Terms and applicable laws.
### 6.2 Acceptable Use Policy
- **Prohibited Content**: Content that is illegal in the United States or prohibited by our partners (such as payment or hosting providers) is not allowed.
- **Prohibited Activities**:
- Spamming
- Crypto mining
- Any use other than hosting PocketBase for web and mobile applications
- **System Integrity**: Misuse of resources or activities that severely affect system performance or user experience are prohibited.
## 7. Misuse and Termination
- **Emergency Actions**: In cases of severe misuse affecting system performance, we may suspend your account and delete data without notice or backups. We recommend keeping your own backups.
- **Fair Use Violations**: Violations of PocketHost's Fair Use policy may result in account suspension or termination. In particularly egregious cases, data may be deleted without notice.
- **Notification**: In most cases, we will notify you to vacate the platform at your earliest convenience before taking action.
## 8. Third-Party Services
- **Compliance with Third-Party Terms**: We use third-party services for payment and hosting. By using our services, you agree to comply with the terms of these providers.
- **Prohibited by Partners**: Any activity or content not allowed by our third-party providers is also prohibited on our platform.
## 9. Intellectual Property
- **Open Source Platform**: Our platform is open-source under the MIT License.
- **User Rights**: You retain all rights to your content. We do not claim ownership over any content you upload.
## 10. Limitation of Liability
- **Maximum Liability**: Our total liability to you is limited to the amount you've paid us in hosting fees over the last **three months**, with a maximum of **$500**.
- **No Liability for Damages**: We are not liable for any indirect, incidental, special, consequential, or punitive damages, including loss of profits, data, or other intangibles.
- **Service Availability**: We do not guarantee uninterrupted or error-free service and are not liable for any downtime or service interruptions.
## 11. Disclaimers
- **"As Is" Basis**: Our services are provided on an "as is" and "as available" basis.
- **No Warranties**: We disclaim all warranties, express or implied, including warranties of merchantability, fitness for a particular purpose, and non-infringement.
## 12. Dispute Resolution
- **Governing Law**: These Terms are governed by the laws of the State of Nevada, USA.
- **Venue**: Any disputes arising under or in connection with these Terms shall be resolved through litigation in the courts of Nevada.
## 13. Changes to the Terms
- **Modification of Terms**: We may update these Terms from time to time.
- **Notification**: Changes will be effective immediately upon posting the updated Terms on this page.
- **Acceptance of Changes**: Your continued use of the platform after any changes indicates your acceptance of the new Terms.
## 14. Miscellaneous
### 14.1 Force Majeure
We shall not be liable for any failure or delay in performing our obligations under these Terms due to events beyond our reasonable control, including natural disasters, war, terrorism, riots, embargoes, acts of civil or military authorities, fire, floods, accidents, network infrastructure failures, strikes, or shortages of transportation facilities, fuel, energy, labor, or materials.
### 14.2 Entire Agreement
These Terms constitute the entire agreement between you and PocketHost regarding your use of our services and supersede any prior agreements.
### 14.3 Severability
If any provision of these Terms is found to be unenforceable or invalid, that provision will be limited or eliminated to the minimum extent necessary so that the remaining Terms will remain in full force and effect.
### 14.4 Assignment
You may not assign or transfer your rights or obligations under these Terms without our prior written consent. We may assign our rights and obligations without restriction.
## 15. Contact Information
If you have any questions or concerns about these Terms, please contact us at:
- **Email**: [ben@pockethost.io](mailto:ben@pockethost.io)
- **Address**: PocketHost, PO Box 871, Reno, NV 89501, USA
---
By using PocketHost services, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service.
</div>

View File

@ -0,0 +1,9 @@
<script lang="ts">
import Splash from './Splash/Splash.svelte'
</script>
<svelte:head>
<title>Home - PocketHost</title>
</svelte:head>
<Splash />

View File

@ -0,0 +1,12 @@
<script lang="ts">
import Fa from 'svelte-fa'
import { type IconDefinition } from '@fortawesome/free-solid-svg-icons'
export let icon: IconDefinition
</script>
<div
class="text-[32px] bg-primary w-[60px] h-[60px] flex items-center justify-center rounded-full text-white shadow-xl"
>
<Fa {icon} />
</div>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import FeatureIcon from './FeatureIcon.svelte'
import PrimaryButton from './PrimaryButton.svelte'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
export let icon: IconDefinition
export let title
export let tagline
export let content
export let linkText
export let linkURL
</script>
<div class="px-[75px] pt-[75px] pb-[75px]">
<div class="mb-12">
<FeatureIcon {icon} />
</div>
<h3 class="text-green-400 uppercase font-bold mb-2">{title}</h3>
<p class="text-white font-bold text-3xl mb-4">{tagline}</p>
<p class="mb-12">{content}</p>
<PrimaryButton text={linkText} url={linkURL} icon={faArrowRight} />
</div>

View File

@ -0,0 +1,27 @@
<script lang="ts">
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
export let text: string
export let url: string
export let icon: IconDefinition
export let target = ''
</script>
<div class="relative inline-flex group">
<div
class="absolute transition-all duration-1000 opacity-70 -inset-px bg-gradient-to-r from-[#44BCFF] via-[#FF44EC] to-[#FF675E] rounded-xl blur-lg group-hover:opacity-100 group-hover:-inset-1 group-hover:duration-200 animate-tilt"
></div>
<a
href={url}
class="relative inline-flex gap-4 items-center justify-center px-8 py-4 text-lg font-bold text-white transition-all duration-200 bg-gray-900 font-pj rounded-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900"
{target}
role="button"
>
{text}
{#if icon}
<Fa {icon} />
{/if}
</a>
</div>

Some files were not shown because too many files have changed in this diff Show More