Remove app diff

This commit is contained in:
Filip Sodić 2025-02-17 13:55:52 +01:00
parent 808c459534
commit c13abc7536
47 changed files with 0 additions and 13475 deletions

View File

@ -1,5 +0,0 @@
--- template/app/.env.client
+++ opensaas-sh/app/.env.client
@@ -0,0 +1 @@
+REACT_APP_GOOGLE_ANALYTICS_ID=G-H3LSJCK95H
\ No newline at end of file

View File

@ -1,28 +0,0 @@
--- template/app/.env.vault
+++ opensaas-sh/app/.env.vault
@@ -0,0 +1,25 @@
+#/-------------------.env.vault---------------------/
+#/ cloud-agnostic vaulting standard /
+#/ [how it works](https://dotenv.org/env-vault) /
+#/--------------------------------------------------/
+
+# development
+DOTENV_VAULT_DEVELOPMENT="STJRGW84NkbUqenj6Eyt98WiAcvzWsWhOKwyktUfAJ0dR8TlNnSdCDRsCRisCzKuDTTZtgCf6FyTgT48HsYuwFZqydg/aFeM4skfLpyUy0wz0K90hq2sNEB85HCAwZaTTf9ye5fEBq9P1PAEoYlHEFtPXO1exSkXyxQHzkv5/j2xLJVkg89cZ+MBaQQtzmgK/A9ya5eVSX2zIhum+ipZKIE04pjc4H8fu5YgxY91rOVTFHBQc1dlbuCr+vvmvgHxNS2gO+eM5UcmZk6FY4yY+GUZmf+xGw5ZPwFjiS3awC+jNhdebWnkrk/PopAqBC3i7eH3fdaPvJW2RWkMMrY4+gtN8Hs9lPNopWkl3uavAva9qmvrCE1KLu94IaQh34Cy6dRurPQDF/MmnugqYzcEQvvtIV1sgZig6Id4c9P67gs/Q9NvKXvbeNxzXYTwkF3SvnMRXk33yBKbXHxE1vIn9k9mbMnX3DVeVdT5TDR9BKs9xGMuvP7rQv5RA/xSnzMFg8LeJ+oZ2zNUWWN8tOyx6K4WwiIJN39AOLYElXAFa3UTSk12GtzaySM5rQdcWn0YLN3uncye26zRCA+BpR231fgrSpfJJ2VkNBG0pTh1ELySOFug6mItLOb7CQGXjZrrwTSfEBhhpMZn/c2Ec6p/nBN2UcHAmB55N6GDFuAkagv9l/4C5fIbS6iGHYAOwwYKczSBryi3+5y7sDYkdVhOTzuKTfkpY1FpkEgUY8FFnW2GjYodH2T+l23Q84a7A9VjxZAyZJkgwGnezO+M8fWcwtqt3iywEVhTd5Hi7Nxe+Q53oQeUrwnTKSkx5Ig5uR1RJIybW+gWvpSOEcUZN6AVf67/UPu9RHpwHDQ5YCGGqJwDzR/f5QxR0kqiW3B0ZDWpJX7zFk2QiLGLzDZ6xmdaGenbJ0jWMA116Xm+ot0GCEqim+RVk768zSAxIQFOCcllXWhmxwTO2/SzY6j7Hxf1Ps8e0uCVc7FPemh7nBYLRlSOI/Pjk9gLUkyKhV7IEiaL2BbPFLGOFkOk0AGNc+m/y1BH+CnJYYKCK1AXNquKM9+MV8Wmure2iBIm2oNlDOcJl48YpUQ0BcKzmFw06OJHVULPVn7da7XZ2/34SvyFcsYARE6NZLLMMg810S1cok0gQ3FpxKv3wxP1jRXntEC1ZxUqJQE/hZ5IxoOxhAJGQfqYbJIQSE5oE8Ve7E0X2lTYVEPiwziYOeovKidJX1vccZh4ggxgYsRzT/qAiS/FcvgsWz9kY6n0e1iKLEmsGR96SDpzNSWV/57iSuOd6nKtcU8HWhekTMnTQK4wrhXuYQHZhLeRnGQIjHzNyzJ8lV6HVcot6NZPPHjOfohQ9eIhQi7qUC7JPcLFkWixjrPTAEJVlQelbAOPHPOUyYEoejcPi/0LMTrQPYN/To/lhDHn/5N9vhlk8flj0zuJHpCFq8laDsn9wdvymuK/0MQ7DrPPU00nNzQjXDtT9xbb6I9ZX7XtgTVHVnWtEwxCQRYkmbR0T+0TzDLXlQObITanuFVa19ZSeATGc+PhxFAva5yxPih2L+j3BTN43ap/X3j54DDylyNHDQlpC21wOa+HFGftYUAWx8/w8i5gr7ZU66aVQWZspSLP7BIFzMaVVjAM5gVqaCLTpcnCE3oMMYddsqsXUbkJrmdlh4cP8ao2Y9YHzv3KRTyAU4wDxhkwWdDqnUSRVXkcDeZUuCQJsHFRvXVzgCNDSktMewTC4xMNniW6KNUwbqzWxGBCz6g/0WUJEaJYG+teu1jBMPDhNFFlw6BjIoNX7iButmm81XCG8D1uEgRvo+H4lxAL4rPyPNCQieT34XHk/QxcQsqij2wnTenGUwWQt++kzMLvL/69RtvaskA8I3aLXDLn3ATMU4JlMvD1MqRHrGb3tzoz+lXKWVgrJR+BSYUBXZc9EAUsj4G4+NsRyMPbBWE2ADgM4ksDflXCccbelu/4S50eS9dEXf7ms0CeYh+dFmqjhCXI8JrgL+bUdOwDbEJVPjM2yOVM1wATGAa8+0nLaJbIwI41JVXBx/uLZ8pt2pyvQB9cyLQR00hcjbLymHBfLP51B6nlaNhUq4yZsI/JM59XNmEYAY8GZJFuvIBvSimC5plUCc1bfGhK694k7Fv1bKtlIzXd4kCy5bKNiwWMTVbOy3Uv87FQTa61z5mnz/XGmRwU/22v4VOWy11BKWGYkYJGN1KrAVkUenCKQw6FjgVXBbfJ6m2k9tl5dfQ48ppToorUZkFiKsPuc8V6DhcUt2stxEETnUnU9K2p8QyyhfPykBK6YWbShDobeA0TPF+vo2ypkVOvhNo7F4cQnvvVKl9MJmmAxFOisB9yHXREk2GSlivNJeTqIz8Vv4K42/wNMAO2ZQqSPlE4wt2ueFQqLMkG+UDlD6gujSx8D/OzSTfCx+vLTzdqsbubPxjcPx+pQA=="
+DOTENV_VAULT_DEVELOPMENT_VERSION=12
+
+# ci
+DOTENV_VAULT_CI="pveNDc605iqf5ZOh9mvYpFOc925852DGrvz7y+vs82ub3bZvTiLhJMuJKZEDtyjn7yBUdfMRsAP6wAL8/73te+4y7K56lT48MuvFlpvwM9ERkCzJY1yZ6ib3eBzTax/jon2H5kfEHBM9TRIa5B8JrCHbYqhcrnxj0VyLEa4x1I4pMTdesRZ+907FTdExaRSFFujdkUaScytOV93DTr2lQZpUGBga5CbKr3KyeSS1+ONGSWc+BQiAkdhmnvPsyKHnWRKHSFVhInne2W00LFZLBZ56I4+G7D+x+ze3/WNb5GSq7eNlVwnTJgj5AXJBjtAV/uD7cmOnJ3h+lOkvz3Wry10BaICTAx5SVfOuD5Pr/FLgR2zJau+pgqhRdcp7gG4y2thWN5ZtlLGnJlL7xgnRaD+oIYBn6fg8CCZsnsX2XZHAzJcgntph2pUd0At4UawSijDVZuylcneNdF3CJ68/fcqqpR94LYU61w4WasrAWj1hFiRV1MTpLkdQWTMT6K5k5TDouuljI3nt+Upf9mDK7efjy7OTiiArNxKpUxUy31Kl0TgWTsttDFbcOGwbsxq1t/Tu9Ph0eEhwHc2dTUuSuhV+PHxE8GwUvGiIDBbAZZq7ZyeLc5btdP++8zdXBtYM7bIa9qubaCWakruk31w6Gm5ySjGY+R1HGURldXJmypsAEcu5kK3IX4Z8IubF79T8ZvutLdoXJPSaH9x8ij7EcamTVfZPmekj6FpI3sqy7rmaUup73MBld9a3r9AQTzXbEmR+2xbK488D1il8PUqu9J3QkUeF2rpB97Uy2mgl1zF8VXiGkdP1Hpqeck8FpS89GWFF4FA3oBE/Keqvqvvs/f+KzDn2AvHHwrT6fnVBdeuSp4hK+sSkig1rh+1P69zYjIDqpph0kT+Ri7uNMkbrXX7iT5he9J1LrXtiM/DLxXqi8M3FfeN7aaSxQ2nzac1mlN+vwsIqJbzOSm+ABGHNJNjPjBZpMbeBZsFwogaCbFd2oorp02h6yC6741qwH7gFEr1EJVcu+TJspHyKpq4TFIk+srRMhuCqwUAkdzY4skjEGVhxxYMiKAtRg5IKYICqbfNsestMipAcf22teriGnmqBe9Yuzq9IXZXRcl/etagr0/B3SQhNlVj2u9VMBfhj5ZY+QcQDfSjqOcW/DyphyBU4T1/rHn1x5RznCvtbOPZ+XXFApZaeoKzj/s8JxHzM8aegITx3jVRwXnr0/0FnHTnC34b124K6HM3oKxdPdrtACgW8gH+mgXCaStBqwBdk1aV62+YQxWdHXjy9FAvTF8BktcxwE0TXC5Qu0JsoS8uYbbQmaQCNWIJWF/KptW+g9gkKDBV2Iq5mXwMLHzCtzxGarj5jqznVEX4OdbYbgSOZ5mB5JEU2OwSn2QtzqUVquAN+DygbDVrh1+KA9bk4HMt38rMKHV/i9VBHkuApYT7OUIexCu4jxUDBnid/Hnm3OfMvXrUesoqFv2cYEHX8R/jvhLx36jEWeaTYL+e9XFpSLlWJTGc7wAg0Zj+XJlNtLhFlB6pw"
+DOTENV_VAULT_CI_VERSION=8
+
+# staging
+DOTENV_VAULT_STAGING="nAXVAHQaoF1R6J23PDrfp6kH0L+nXmZaI0OUfcCBU7QSfP+K3mbkdO92cbKVUN6ycc1wxRue6BFLEUgBLuCJQwUdKYevK81fs/ACrHYxuW6Px85FG3NJVLGvgF/3qiqu0RRfcpeB6lYJCAPFsYcoA9Cx8QDkYmvSlWAGmLJQ7cqzHUNlveOqtrXsLFV2OjlWjlYD1l7VQkaXnsY+Mij60DbRIadusoDuAB3z5m7N8/ogMOGENlUl9HRJJ4ppcJiHoBm7pxrLpK0oubx3KEDTSkPVlpNb1YYMeU2VKiGYCOMyXX8IguISrrgU4hHEvbLM8F3gnKHyx59qbwU/32yX7ofIIPsTLLTwD+mHWElPoLs9WWq249RYEmRT7w5iuCdjl9Qlr36EJKPYc7UG+P0DZROb7pXcbtYg8/gfaCXOQG5Ue2FMon4bEphkmY1CKaJKtPTXNuecDVTa8O4hEM65xrEClieVUgexEaM0g7fEUo8wOljZx96gG8Mf4RqHX3xy4M0d4/2UluE053kavUaTULhRPG/oTHIGiKFRnG0BfgQFcGRXkFgLG9zerVAoWEGAcBGHn5NECIUWqaD2Z7sdEZd9R0SZmtwUXoPV1jqHNeKEvt7fUN8xcPsV1MFAUi2cAyGLkrl541KbWpU0IpHK1pIkbfUVyDOL4htSD/SajhtlyJ79q7Xd5sKsb273ZQsmtk3ZREBjQUFA2NP+fPH7ifttVeWi3EFUi8+8cWwgNHM6Cn7LaFitah6+DcCQ6+l7SneFTEbzMlprzazBBL/Gu4kr6pNXGX0RZnR8uhs6aZZgV2h9V14s0uZx3EcAFqC3g+I5X08Fi+2ERpcfWUvFdjx5akmDQllSB+oqy6SxiJvqFnfWvVPQNhx1xh2x8OplZbzykEJ+Tfl0mlc5UHHoLQqje0jagBRSHWQbxC0jIGGyqdhnz3uG672krM/zTTc2wKWMk4EZFDG7Yc7oz3DcJXjFBgxEagd9jYxqLIN0RAH9pq+0aUP7daz0CbX+xhTd988dl+LblcnEN59XEsBOhWCd5bm51KJZcKQ1UpsVVPGHnTT7MPE2+6BheEr1dgu/LDUPDW8NT0QwBUwXwxH1ROuTOu2+LWNdFrV11sEpKGfW2bjqrkpbx/xovahbFyCSq12T0EBInAFF5bq2pB0Ik2vfEH8gNL0Z+FoqH0/aDrSuw6j8xqmd2rKL3XwL5bYVZDCc1isAWXESA9CHHTSx1NGoa1kpunJkGJY//2rn5srK9RlOOHxxUrtqVBMfEGsj7qIv/Ebsgr3dvFpdVXiRZ5s3KZZs6YlSXyukklehkjtYoMM2TaZ478ioFOtD8yx12PjI3Pfoapo2La2boM48RFcipmVNpWNsLjGXaYyhYVV3RBsZAMvkOnYRVFUiZ50+ETdfizX0H23+mmkBt8FjNqHhvg9BIRaYnKTar1FPAd840lo4w2e7n2mKufHwykI5ILrJV0ozhPy7Yr8kltPiQYjpy1ua2scUdnKCngdX0KjqNQz072e6uYlhRpOib6CEwZXqMbr/"
+DOTENV_VAULT_STAGING_VERSION=8
+
+# production
+DOTENV_VAULT_PRODUCTION="zGpYx7O6vRv3vYt0aW1H6UF3TSi6DUFgqjeKzkNcJukFWQWvTgX5HYtLKQnBAGCDpuWY+KWfiFGNu/zVouo5LTlQb6apzBLzj0dr+PxxDpjXe8s+q7OSPH7Ist4UiVurC2nAMjgFsUpDA2LUkfPOuWPRvg1UGYjTaxEADTYCI/vk0Uegv4R11K1/HcCGXlliJk2QAVaT5ZYSo0VKvcRu5HYHSX4aAwJgnucm3xvNmPn9Cjkxx1jZL5jYfzLS1etDEPn6slDj88NzZ8BvXHeSJUftpR352azOJpL0GbSKrwoPNYU9F2JCthilLqPZPC6LJzg0/4p5vUKA83gqnDnUxEHR+ZlNK0MRTQPEwrZitruOaP1ggWIsEJA/DZFMLA5oQrq4kp+p2E99YbsPNmV0LfvsA6LtqKv393jsuzSdJ94zeLhQlRRECiTQRVEee3ug5tJ0f34N7acRSv4EdgriKp8poIsE4miVK2xXwrek9jRxeXLHzu7Oo8yBkYaX2DqtXyxf+8Qq2WgvdRm9QgfGdiwftYsjXO52jhHI7z7bJ3R/W5BR+Z0QbR83LR3ZWdI+AYY9F2CkYF62nW5TVxiWU0M4DZ5KUgVo+iNgm+AV0nefgFvud6Ln3VtNcb2bT0RFAw4MJK0LyYlNewZBJQhlfRiN+3xJKqLbDKMlhGmSHF8RRNYN9MfKwOfF62jNFlwPjgyGCjuQL96L97z50Q63TMbpEciR0uNcMavIADPs1qDhCSkYO4rQO1IgOUF34trra9KKRfoP2UdDJhiqvlTQ2Ygu2uc164Vho0E003jlBBZBI+UUJhPz4cYTtFJjpinRCp/Q0mUeiTLbo1V7T32IQ+ZkUqO7FRuwxR282OzKQVRZiUxuOAa7WxdQzrcXbCOcCvJWX8hTVrSim9z+NAFxNyTCwVBhek9EfwCfxJSCuFuI9axlb6xA1sZ/61G5+wuKeEpUvlTbQC/ZT1QJqpKMmujEePAfW++dzJJgW/nX9o0CIt3KTzwVzImAe+/oNhzKn0TbzsvMqeOaTgymo2re0Rdw3P/Fk5SUQTM7+2GIFC+iRC5a3LN7UOFP+UdQ21J+WGkkl+yvJyyjY3IRSoFWqSCU7BAjULmJmtkl1Y/60vIj+GET5hu7VGaTlIgaWiVZc4eGAu6UAeGzasTjYWNQh50p44VxQlYBijJC+JUhzPkFOLjaLx6rRZzTQVl9NUxIxdkaOn+BImJQadaC2Ad3YOMpsLVJF/p25fi2L2e2MtsDdcx2ZNlpV1GIrYGqMIiujPdpoMzibj4VmObVMB6dazpyqgAf/QXdGguyv/4JJBcNUNPbqHqMl0yY+DzGzxPCKx6WViR63HZJeC5hJE39wNEgeYGJcXrL5K2siwrFENns8Os93aoE/8xgiPdj3vLQmcL0Z30XEqPl6/V4mTgc2Oa1SAi1NGLXnVsYJ/h1Rr4a9T11zkKhRCIvT4Jd5WpvGjG3f9t7YUigb6BqlMc0BTJ6ufQP/xBBOHqzoUs3gVk4HTMDfpYwO9k9SJNkfWBn1iV102BJ"
+DOTENV_VAULT_PRODUCTION_VERSION=8
+
+#/----------------settings/metadata-----------------/
+DOTENV_VAULT="vlt_47e3eeb0730e831e688049600e59f8975260a1f00302ae08684ed87ba67872d0"
+DOTENV_API_URL="https://vault.dotenv.org"
+DOTENV_CLI="npx dotenv-vault@latest"

View File

@ -1,20 +0,0 @@
--- template/app/.gitignore
+++ opensaas-sh/app/.gitignore
@@ -6,6 +6,17 @@
.env
.env.*
+# These two we added only because dotenv-vault keeps adding them if it doesn't find them,
+# even though we don't need them. Remove them once dotenv-vault stops doing that.
+.env*
+.flaskenv*
+
# Don't ignore example dotenv files.
!.env.example
!.env.*.example
+
+# We don't want to ignore .env.client as it doesn't have any secrets.
+!.env.client
+# These are config files for dotenv-vault, so we don't want to ignore them.
+!.env.project
+!.env.vault

View File

@ -1,30 +0,0 @@
--- template/app/README.md
+++ opensaas-sh/app/README.md
@@ -1,12 +1,25 @@
-# <YOUR_APP_NAME>
+# opensaas.sh (demo) app
-Built with [Wasp](https://wasp.sh), based on the [Open Saas](https://opensaas.sh) template.
+This is a Wasp app based on Open Saas template with minimal modifications that make it into a demo app that showcases Open Saas's abilities.
+
+It is deployed to https://opensaas.sh and serves both as a landing page for Open Saas and as a demo app.
## Development
+### .env files
+`.env.client` file is versioned, but `.env.server` file you have to obtain by running `npm run env:pull`, since it has secrets in it.
+This will generate `.env.server` based on the `.env.vault`.
+We are using https://vault.dotenv.org to power this and have an account/organization up there.
+If you modify .env.server and want to persist the changes (for yourself and for the other team members), do `npm run env:push`.
+
### Running locally
- Make sure you have the `.env.client` and `.env.server` files with correct dev values in the root of the project.
- Run the database with `wasp start db` and leave it running.
- Run `wasp start` and leave it running.
- [OPTIONAL]: If this is the first time starting the app, or you've just made changes to your entities/prisma schema, also run `wasp db migrate-dev`.
+## Deployment
+
+This app is deployed to fly.io, Wasp org, via `wasp deploy fly deploy`.
+
+You can run `npm run deploy` to deploy it via `wasp deploy fly deploy` with required client side env vars correctly set.

View File

@ -1,9 +0,0 @@
src/client/static/avatar-placeholder.webp
src/client/static/da-boi.webp
src/client/static/open-saas-banner.webp
src/landing-page/logos/SalesforceLogo.tsx
src/payment/lemonSqueezy/checkoutUtils.ts
src/payment/lemonSqueezy/paymentDetails.ts
src/payment/lemonSqueezy/paymentProcessor.ts
src/payment/lemonSqueezy/webhook.ts
src/payment/webhook.ts

View File

@ -1,28 +0,0 @@
--- template/app/fly-client.toml
+++ opensaas-sh/app/fly-client.toml
@@ -0,0 +1,25 @@
+# fly.toml app configuration file generated for open-saas-wasp-sh-client on 2023-12-04T12:34:07+01:00
+#
+# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
+#
+
+app = "open-saas-wasp-sh-client"
+primary_region = "ams"
+
+[build]
+
+[http_service]
+ internal_port = 8043
+ force_https = true
+ auto_stop_machines = true
+ auto_start_machines = true
+ min_machines_running = 0
+ processes = ["app"]
+
+[http_service.http_options.response]
+ pristine = true
+
+[[vm]]
+ cpu_kind = "shared"
+ cpus = 1
+ memory_mb = 1024

View File

@ -1,26 +0,0 @@
--- template/app/fly-server.toml
+++ opensaas-sh/app/fly-server.toml
@@ -0,0 +1,22 @@
+# fly.toml app configuration file generated for open-saas-wasp-sh-server on 2023-12-04T12:33:59+01:00
+#
+# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
+#
+
+app = "open-saas-wasp-sh-server"
+primary_region = "ams"
+
+[build]
+
+[http_service]
+ internal_port = 8080
+ force_https = true
+ auto_stop_machines = true
+ auto_start_machines = true
+ min_machines_running = 1
+ processes = ["app"]
+
+[[vm]]
+ cpu_kind = "shared"
+ cpus = 1
+ memory_mb = 1024
\ No newline at end of file

View File

@ -1,118 +0,0 @@
--- template/app/main.wasp
+++ opensaas-sh/app/main.wasp
@@ -3,30 +3,29 @@
version: "^0.16.0"
},
- title: "My Open SaaS App",
+ title: "Open SaaS",
head: [
"<meta charset='utf-8' />",
- "<meta name='description' content='Your apps main description and features.' />",
- "<meta name='author' content='Your (App) Name' />",
- "<meta name='keywords' content='saas, solution, product, app, service' />",
-
+ "<meta name='description' content='Build and launch your SaaS application faster with our free, open-source starter kit. Features include auth, payments, AI example app, and admin dashboard.' />",
+ "<meta name='author' content='Open SaaS' />",
+ "<meta name='keywords' content='saas, starter, boilerplate, free, open source, authentication, payments' />",
+
+ "<meta property='og:site_name' content='Open SaaS' />",
"<meta property='og:type' content='website' />",
- "<meta property='og:title' content='Your Open SaaS App' />",
- "<meta property='og:site_name' content='Your Open SaaS App' />",
- "<meta property='og:url' content='https://your-saas-app.com' />",
- "<meta property='og:description' content='Your apps main description and features.' />",
- "<meta property='og:image' content='https://your-saas-app.com/public-banner.webp' />",
- "<meta name='twitter:image' content='https://your-saas-app.com/public-banner.webp' />",
+ "<meta property='og:title' content='Open SaaS' />",
+ "<meta property='og:url' content='https://opensaas.sh' />",
+ "<meta property='og:description' content='Free, open-source SaaS boilerplate starter for React & NodeJS.' />",
+ "<meta property='og:image' content='https://opensaas.sh/public-banner.webp' />",
+
+ "<meta name=\"twitter:title\" content=\"Open SaaS\" />",
+ "<meta name=\"twitter:text:title\" content=\"Open SaaS\" />",
+ "<meta name='twitter:image' content='https://opensaas.sh/public-banner.webp' />",
+ "<meta name=\"twitter:image:alt\" content=\"Open SaaS\" />",
"<meta name='twitter:image:width' content='800' />",
"<meta name='twitter:image:height' content='400' />",
"<meta name='twitter:card' content='summary_large_image' />",
- // TODO: You can put your Plausible analytics scripts below (https://docs.opensaas.sh/guides/analytics/):
- // NOTE: Plausible does not use Cookies, so you can simply add the scripts here.
- // Google, on the other hand, does, so you must instead add the script dynamically
- // via the Cookie Consent component after the user clicks the "Accept" cookies button.
- "<script defer data-domain='<your-site-id>' src='https://plausible.io/js/script.js'></script>", // for production
- "<script defer data-domain='<your-site-id>' src='https://plausible.io/js/script.local.js'></script>", // for development
+ "<script defer data-domain='opensaas.sh' data-api='/waspara/wasp/event' src='/waspara/wasp/script.js'></script>",
],
// 🔐 Auth out of the box! https://wasp.sh/docs/auth/overview
@@ -38,7 +37,7 @@
email: {
fromField: {
name: "Open SaaS App",
- email: "me@example.com"
+ email: "vince@wasp-lang.dev"
},
emailVerification: {
clientRoute: EmailVerificationRoute,
@@ -50,21 +49,18 @@
},
userSignupFields: import { getEmailUserFields } from "@src/auth/userSignupFields",
},
- // Uncomment to enable Google Auth (check https://wasp.sh/docs/auth/social-auth/google for setup instructions):
- // google: { // Guide for setting up Auth via Google
- // userSignupFields: import { getGoogleUserFields } from "@src/auth/userSignupFields",
- // configFn: import { getGoogleAuthConfig } from "@src/auth/userSignupFields",
- // },
- // Uncomment to enable GitHub Auth (check https://wasp.sh/docs/auth/social-auth/github for setup instructions):
- // gitHub: {
- // userSignupFields: import { getGitHubUserFields } from "@src/auth/userSignupFields",
- // configFn: import { getGitHubAuthConfig } from "@src/auth/userSignupFields",
- // },
- // Uncomment to enable Discord Auth (check https://wasp.sh/docs/auth/social-auth/discord for setup instructions):
- // discord: {
- // userSignupFields: import { getDiscordUserFields } from "@src/auth/userSignupFields",
- // configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
- // }
+ google: {
+ userSignupFields: import { getGoogleUserFields } from "@src/auth/userSignupFields",
+ configFn: import { getGoogleAuthConfig } from "@src/auth/userSignupFields",
+ },
+ gitHub: {
+ userSignupFields: import { getGitHubUserFields } from "@src/auth/userSignupFields",
+ configFn: import { getGitHubAuthConfig } from "@src/auth/userSignupFields",
+ },
+ discord: {
+ userSignupFields: import { getDiscordUserFields } from "@src/auth/userSignupFields",
+ configFn: import { getDiscordAuthConfig } from "@src/auth/userSignupFields"
+ }
},
onAfterSignup: import { onAfterSignup } from "@src/auth/hooks",
onAuthFailedRedirectTo: "/login",
@@ -87,11 +83,11 @@
// NOTE: "Dummy" provider is just for local development purposes.
// Make sure to check the server logs for the email confirmation url (it will not be sent to an address)!
// Once you are ready for production, switch to e.g. "SendGrid" or "Mailgun" providers. Check out https://docs.opensaas.sh/guides/email-sending/ .
- provider: Dummy,
+ provider: SendGrid,
defaultFrom: {
name: "Open SaaS App",
// When using a real provider, e.g. SendGrid, you must use the same email address that you configured your account to send out emails with!
- email: "me@example.com"
+ email: "vince@wasp-lang.dev"
},
},
}
@@ -212,9 +208,9 @@
}
api paymentsWebhook {
- fn: import { paymentsWebhook } from "@src/payment/webhook",
+ fn: import { stripeWebhook } from "@src/payment/stripe/webhook",
entities: [User],
- middlewareConfigFn: import { paymentsMiddlewareConfigFn } from "@src/payment/webhook",
+ middlewareConfigFn: import { stripeMiddlewareConfigFn } from "@src/payment/stripe/webhook",
httpRoute: (POST, "/payments-webhook")
}
//#endregion

View File

@ -1,121 +0,0 @@
--- template/app/migrations/20231213174854_init/migration.sql
+++ opensaas-sh/app/migrations/20231213174854_init/migration.sql
@@ -0,0 +1,118 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" SERIAL NOT NULL,
+ "email" TEXT,
+ "username" TEXT,
+ "password" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "lastActiveTimestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isEmailVerified" BOOLEAN NOT NULL DEFAULT false,
+ "isMockUser" BOOLEAN NOT NULL DEFAULT false,
+ "isAdmin" BOOLEAN NOT NULL DEFAULT true,
+ "emailVerificationSentAt" TIMESTAMP(3),
+ "passwordResetSentAt" TIMESTAMP(3),
+ "stripeId" TEXT,
+ "checkoutSessionId" TEXT,
+ "hasPaid" BOOLEAN NOT NULL DEFAULT false,
+ "subscriptionTier" TEXT,
+ "subscriptionStatus" TEXT,
+ "sendEmail" BOOLEAN NOT NULL DEFAULT false,
+ "datePaid" TIMESTAMP(3),
+ "credits" INTEGER NOT NULL DEFAULT 3,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SocialLogin" (
+ "id" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "providerId" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "SocialLogin_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "GptResponse" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "GptResponse_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ContactFormMessage" (
+ "id" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "isRead" BOOLEAN NOT NULL DEFAULT false,
+ "repliedAt" TIMESTAMP(3),
+
+ CONSTRAINT "ContactFormMessage_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DailyStats" (
+ "id" SERIAL NOT NULL,
+ "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "totalViews" INTEGER NOT NULL DEFAULT 0,
+ "prevDayViewsChangePercent" TEXT NOT NULL DEFAULT '0',
+ "userCount" INTEGER NOT NULL DEFAULT 0,
+ "paidUserCount" INTEGER NOT NULL DEFAULT 0,
+ "userDelta" INTEGER NOT NULL DEFAULT 0,
+ "paidUserDelta" INTEGER NOT NULL DEFAULT 0,
+ "totalRevenue" DOUBLE PRECISION NOT NULL DEFAULT 0,
+ "totalProfit" DOUBLE PRECISION NOT NULL DEFAULT 0,
+
+ CONSTRAINT "DailyStats_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PageViewSource" (
+ "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" TEXT NOT NULL,
+ "visitors" INTEGER NOT NULL,
+ "dailyStatsId" INTEGER,
+
+ CONSTRAINT "PageViewSource_pkey" PRIMARY KEY ("date","name")
+);
+
+-- CreateTable
+CREATE TABLE "Logs" (
+ "id" SERIAL NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "message" TEXT NOT NULL,
+ "level" TEXT NOT NULL,
+
+ CONSTRAINT "Logs_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "SocialLogin_provider_providerId_userId_key" ON "SocialLogin"("provider", "providerId", "userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DailyStats_date_key" ON "DailyStats"("date");
+
+-- AddForeignKey
+ALTER TABLE "SocialLogin" ADD CONSTRAINT "SocialLogin_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "GptResponse" ADD CONSTRAINT "GptResponse_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ContactFormMessage" ADD CONSTRAINT "ContactFormMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PageViewSource" ADD CONSTRAINT "PageViewSource_dailyStatsId_fkey" FOREIGN KEY ("dailyStatsId") REFERENCES "DailyStats"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -1,17 +0,0 @@
--- template/app/migrations/20240105224550_tasks/migration.sql
+++ opensaas-sh/app/migrations/20240105224550_tasks/migration.sql
@@ -0,0 +1,14 @@
+-- CreateTable
+CREATE TABLE "Task" (
+ "id" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "time" TEXT NOT NULL DEFAULT '1',
+ "isDone" BOOLEAN NOT NULL DEFAULT false,
+ "userId" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,18 +0,0 @@
--- template/app/migrations/20240207164719_files/migration.sql
+++ opensaas-sh/app/migrations/20240207164719_files/migration.sql
@@ -0,0 +1,15 @@
+-- CreateTable
+CREATE TABLE "File" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "key" TEXT NOT NULL,
+ "uploadUrl" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "File_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -1,47 +0,0 @@
--- template/app/migrations/20240226123357_new_auth_structure/migration.sql
+++ opensaas-sh/app/migrations/20240226123357_new_auth_structure/migration.sql
@@ -0,0 +1,44 @@
+-- CreateTable
+CREATE TABLE "Auth" (
+ "id" TEXT NOT NULL,
+ "userId" INTEGER,
+
+ CONSTRAINT "Auth_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AuthIdentity" (
+ "providerName" TEXT NOT NULL,
+ "providerUserId" TEXT NOT NULL,
+ "providerData" TEXT NOT NULL DEFAULT '{}',
+ "authId" TEXT NOT NULL,
+
+ CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId")
+);
+
+-- CreateTable
+CREATE TABLE "Session" (
+ "id" TEXT NOT NULL,
+ "expiresAt" TIMESTAMP(3) NOT NULL,
+ "userId" TEXT NOT NULL,
+
+ CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id");
+
+-- CreateIndex
+CREATE INDEX "Session_userId_idx" ON "Session"("userId");
+
+-- AddForeignKey
+ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,24 +0,0 @@
--- template/app/migrations/20240226130234_remove_old_auth_structure/migration.sql
+++ opensaas-sh/app/migrations/20240226130234_remove_old_auth_structure/migration.sql
@@ -0,0 +1,21 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `emailVerificationSentAt` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `isEmailVerified` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `passwordResetSentAt` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the `SocialLogin` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "SocialLogin" DROP CONSTRAINT "SocialLogin_userId_fkey";
+
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "emailVerificationSentAt",
+DROP COLUMN "isEmailVerified",
+DROP COLUMN "password",
+DROP COLUMN "passwordResetSentAt";
+
+-- DropTable
+DROP TABLE "SocialLogin";

View File

@ -1,8 +0,0 @@
--- template/app/migrations/20240226145340_remove_unique_email_username/migration.sql
+++ opensaas-sh/app/migrations/20240226145340_remove_unique_email_username/migration.sql
@@ -0,0 +1,5 @@
+-- DropIndex
+DROP INDEX "User_email_key";
+
+-- DropIndex
+DROP INDEX "User_username_key";

View File

@ -1,11 +0,0 @@
--- template/app/migrations/20240605151848_remove_has_paid/migration.sql
+++ opensaas-sh/app/migrations/20240605151848_remove_has_paid/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `hasPaid` on the `User` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "hasPaid";

View File

@ -1,68 +0,0 @@
--- template/app/migrations/20240702143707_update_user_entity/migration.sql
+++ opensaas-sh/app/migrations/20240702143707_update_user_entity/migration.sql
@@ -0,0 +1,65 @@
+/*
+ Warnings:
+
+ - The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint.
+ - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
+ - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- DropForeignKey
+ALTER TABLE "Auth" DROP CONSTRAINT "Auth_userId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "ContactFormMessage" DROP CONSTRAINT "ContactFormMessage_userId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "File" DROP CONSTRAINT "File_userId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "GptResponse" DROP CONSTRAINT "GptResponse_userId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "Task" DROP CONSTRAINT "Task_userId_fkey";
+
+-- AlterTable
+ALTER TABLE "Auth" ALTER COLUMN "userId" SET DATA TYPE TEXT;
+
+-- AlterTable
+ALTER TABLE "ContactFormMessage" ALTER COLUMN "userId" SET DATA TYPE TEXT;
+
+-- AlterTable
+ALTER TABLE "File" ALTER COLUMN "userId" SET DATA TYPE TEXT;
+
+-- AlterTable
+ALTER TABLE "GptResponse" ALTER COLUMN "userId" SET DATA TYPE TEXT;
+
+-- AlterTable
+ALTER TABLE "Task" ALTER COLUMN "userId" SET DATA TYPE TEXT;
+
+-- AlterTable
+ALTER TABLE "User" DROP CONSTRAINT "User_pkey",
+ALTER COLUMN "id" DROP DEFAULT,
+ALTER COLUMN "id" SET DATA TYPE TEXT,
+ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id");
+DROP SEQUENCE "User_id_seq";
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- AddForeignKey
+ALTER TABLE "GptResponse" ADD CONSTRAINT "GptResponse_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ContactFormMessage" ADD CONSTRAINT "ContactFormMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,19 +0,0 @@
--- template/app/migrations/20240715142249_version_14/migration.sql
+++ opensaas-sh/app/migrations/20240715142249_version_14/migration.sql
@@ -0,0 +1,16 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `sendEmail` on the `User` table. All the data in the column will be lost.
+ - You are about to drop the column `subscriptionTier` on the `User` table. All the data in the column will be lost.
+ - A unique constraint covering the columns `[stripeId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN "sendEmail",
+DROP COLUMN "subscriptionTier",
+ADD COLUMN "sendNewsletter" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "subscriptionPlan" TEXT;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_stripeId_key" ON "User"("stripeId");

View File

@ -1,11 +0,0 @@
--- template/app/migrations/20241126132514_remove_checkout_session_id/migration.sql
+++ opensaas-sh/app/migrations/20241126132514_remove_checkout_session_id/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `checkoutSessionId` on the `User` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "User" DROP COLUMN IF EXISTS "checkoutSessionId";

View File

@ -1,7 +0,0 @@
--- template/app/migrations/migration_lock.toml
+++ opensaas-sh/app/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "postgresql"
\ No newline at end of file

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
--- template/app/package.json
+++ opensaas-sh/app/package.json
@@ -1,13 +1,17 @@
{
"name": "opensaas",
"type": "module",
+ "scripts": {
+ "env:pull": "npx dotenv-vault@latest pull development .env.server",
+ "env:push": "npx dotenv-vault@latest push development .env.server",
+ "deploy": "REACT_APP_GOOGLE_ANALYTICS_ID=G-H3LSJCK95H wasp deploy fly deploy"
+ },
"dependencies": {
"@aws-sdk/client-s3": "^3.523.0",
"@aws-sdk/s3-request-presigner": "^3.523.0",
"@faker-js/faker": "8.3.1",
"@google-analytics/data": "4.1.0",
"@headlessui/react": "1.7.13",
- "@lemonsqueezy/lemonsqueezy.js": "^3.2.0",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.7",
"apexcharts": "3.41.0",

View File

@ -1,18 +0,0 @@
--- template/app/schema.prisma
+++ opensaas-sh/app/schema.prisma
@@ -14,10 +14,12 @@
email String? @unique
username String? @unique
lastActiveTimestamp DateTime @default(now())
- isAdmin Boolean @default(false)
+ isAdmin Boolean @default(true)
+ // isMockUser is an extra property for the demo app ensuring that all users can access
+ // the admin dashboard but won't be able to see the other users' data, only mock user data.
+ isMockUser Boolean @default(false)
- paymentProcessorUserId String? @unique
- lemonSqueezyCustomerPortalUrl String? // You can delete this if you're not using Lemon Squeezy as your payments processor.
+ stripeId String? @unique
subscriptionStatus String? // 'active', 'cancel_at_period_end', 'past_due', 'deleted'
subscriptionPlan String? // 'hobby', 'pro'
sendNewsletter Boolean @default(false)

View File

@ -1,91 +0,0 @@
--- template/app/src/admin/dashboards/analytics/AnalyticsDashboardPage.tsx
+++ opensaas-sh/app/src/admin/dashboards/analytics/AnalyticsDashboardPage.tsx
@@ -1,4 +1,6 @@
+import { Link } from 'wasp/client/router';
import { type AuthUser } from 'wasp/auth';
+import { useState, useEffect, useMemo } from 'react';
import { useQuery, getDailyStats } from 'wasp/client/operations';
import TotalSignupsCard from './TotalSignupsCard';
import TotalPageViewsCard from './TotalPageViewsCard';
@@ -11,16 +13,58 @@
import { cn } from '../../../client/cn';
const Dashboard = ({ user }: { user: AuthUser }) => {
+ const [isDemoInfoVisible, setIsDemoInfoVisible] = useState(false);
useRedirectHomeUnlessUserIsAdmin({ user });
const { data: stats, isLoading, error } = useQuery(getDailyStats);
+ const didUserCloseDemoInfo = localStorage.getItem('didUserCloseDemoInfo') === 'true';
+
+ useEffect(() => {
+ if (didUserCloseDemoInfo || !stats) {
+ setIsDemoInfoVisible(false);
+ } else if (!didUserCloseDemoInfo && stats) {
+ setIsDemoInfoVisible(true);
+ }
+ }, [stats]);
+
+ const handleDemoInfoClose = () => {
+ try {
+ localStorage.setItem('didUserCloseDemoInfo', 'true');
+ setIsDemoInfoVisible(false);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const sortedSources = useMemo(() => {
+ return stats?.dailyStats?.sources?.slice().sort((a, b) => b.visitors - a.visitors);
+ }, [stats?.dailyStats?.sources]);
+
return (
<DefaultLayout user={user}>
+ {isDemoInfoVisible && (
+ <div className='fixed z-999 bottom-0 mb-2 left-1/2 -translate-x-1/2 lg:mb-4 bg-gray-700 rounded-full px-3.5 py-2 text-sm text-white duration-300 ease-in-out hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-600'>
+ <div className='px-4 flex flex-row gap-2 items-center my-1'>
+ <span className='text-gray-100 text-center'>
+ This is actual data from Stripe test purchases. <br /> Try out purchasing a{' '}
+ <Link to='/pricing' className='underline text-yellow-400'>
+ test product
+ </Link>
+ !
+ </span>
+ <button className=' pl-2.5 text-gray-400 text-xl font-bold' onClick={() => handleDemoInfoClose()}>
+ X
+ </button>
+ </div>
+ </div>
+ )}
<div className='relative'>
- <div className={cn({
- 'opacity-25': !stats,
- })}>
+ <div
+ className={cn({
+ 'opacity-25': !stats,
+ })}
+ >
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4 2xl:gap-7.5'>
<TotalPageViewsCard
totalPageViews={stats?.dailyStats.totalViews}
@@ -39,7 +83,7 @@
<RevenueAndProfitChart weeklyStats={stats?.weeklyStats} isLoading={isLoading} />
<div className='col-span-12 xl:col-span-8'>
- <SourcesTable sources={stats?.dailyStats?.sources} />
+ <SourcesTable sources={sortedSources} />
</div>
</div>
</div>
@@ -47,9 +91,7 @@
{!stats && (
<div className='absolute inset-0 flex items-start justify-center bg-white/50 dark:bg-boxdark-2/50'>
<div className='rounded-lg bg-white p-8 shadow-lg dark:bg-boxdark'>
- <p className='text-2xl font-bold text-boxdark dark:text-white'>
- No daily stats generated yet
- </p>
+ <p className='text-2xl font-bold text-boxdark dark:text-white'>No daily stats generated yet</p>
<p className='mt-2 text-sm text-bodydark2'>
Stats will appear here once the daily stats job has run
</p>

View File

@ -1,54 +0,0 @@
--- template/app/src/admin/dashboards/users/UsersDashboardPage.tsx
+++ opensaas-sh/app/src/admin/dashboards/users/UsersDashboardPage.tsx
@@ -1,14 +1,50 @@
import { type AuthUser } from 'wasp/auth';
+import { useState, useEffect } from 'react';
import UsersTable from './UsersTable';
import Breadcrumb from '../../layout/Breadcrumb';
import DefaultLayout from '../../layout/DefaultLayout';
import { useRedirectHomeUnlessUserIsAdmin } from '../../useRedirectHomeUnlessUserIsAdmin';
const Users = ({ user }: { user: AuthUser }) => {
- useRedirectHomeUnlessUserIsAdmin({user})
+ const [isDemoInfoVisible, setIsDemoInfoVisible] = useState(false);
+ useRedirectHomeUnlessUserIsAdmin({user});
+
+ useEffect(() => {
+ try {
+ if (localStorage.getItem('isDemoInfoVisible') === 'false') {
+ // do nothing
+ } else {
+ setIsDemoInfoVisible(true);
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }, []);
+
+ const handleDemoInfoClose = () => {
+ try {
+ localStorage.setItem('isDemoInfoVisible', 'false');
+ setIsDemoInfoVisible(false);
+ } catch (error) {
+ console.error(error);
+ }
+ };
return (
<DefaultLayout user={user}>
+ {/* Floating Demo Announcement */}
+ {isDemoInfoVisible && (
+ <div className='fixed z-999 bottom-0 mb-2 left-1/2 -translate-x-1/2 lg:mb-4 bg-gray-700 rounded-full px-3.5 py-2 text-sm text-white duration-300 ease-in-out hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-600'>
+ <div className='px-4 flex flex-row gap-2 items-center my-1'>
+ <span className='text-gray-100'>
+ You are viewing mock user data only ;)
+ </span>
+ <button className=' pl-2.5 text-gray-400 text-xl font-bold' onClick={() => handleDemoInfoClose()}>
+ X
+ </button>
+ </div>
+ </div>
+ )}
<Breadcrumb pageName='Users' />
<div className='flex flex-col gap-10'>
<UsersTable />

View File

@ -1,19 +0,0 @@
--- template/app/src/admin/dashboards/users/UsersTable.tsx
+++ opensaas-sh/app/src/admin/dashboards/users/UsersTable.tsx
@@ -9,6 +9,7 @@
const [skip, setskip] = useState(0);
const [page, setPage] = useState(1);
const [email, setEmail] = useState<string | undefined>(undefined);
+
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
const [statusOptions, setStatusOptions] = useState<SubscriptionStatus[]>([]);
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
@@ -222,7 +223,7 @@
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>
</div>
<div className='col-span-2 flex items-center'>
- <p className='text-sm text-meta-3'>{user.paymentProcessorUserId}</p>
+ <p className='text-sm text-meta-3'>{user.stripeId}</p>
</div>
<div className='col-span-1 flex items-center'>
<div className='text-sm text-black dark:text-white'>

View File

@ -1,29 +0,0 @@
--- template/app/src/analytics/providers/plausibleAnalyticsUtils.ts
+++ opensaas-sh/app/src/analytics/providers/plausibleAnalyticsUtils.ts
@@ -36,7 +36,7 @@
async function getTotalPageViews() {
const response = await fetch(
- `${PLAUSIBLE_BASE_URL}/v1/stats/aggregate?site_id=${PLAUSIBLE_SITE_ID}&metrics=pageviews`,
+ `${PLAUSIBLE_BASE_URL}/v1/stats/aggregate?site_id=${PLAUSIBLE_SITE_ID}&metrics=pageviews&with_imported=true`,
{
method: 'GET',
headers: {
@@ -80,7 +80,7 @@
}
async function getPageviewsForDate(date: string) {
- const url = `${PLAUSIBLE_BASE_URL}/v1/stats/aggregate?site_id=${PLAUSIBLE_SITE_ID}&period=day&date=${date}&metrics=pageviews`;
+ const url = `${PLAUSIBLE_BASE_URL}/v1/stats/aggregate?site_id=${PLAUSIBLE_SITE_ID}&period=day&date=${date}&metrics=pageviews&with_imported=true`;
const response = await fetch(url, {
method: 'GET',
headers: headers,
@@ -93,7 +93,7 @@
}
export async function getSources() {
- const url = `${PLAUSIBLE_BASE_URL}/v1/stats/breakdown?site_id=${PLAUSIBLE_SITE_ID}&property=visit:source&metrics=visitors`;
+ const url = `${PLAUSIBLE_BASE_URL}/v1/stats/breakdown?site_id=${PLAUSIBLE_SITE_ID}&property=visit:source&metrics=visitors&with_imported=true`;
const response = await fetch(url, {
method: 'GET',
headers: headers,

View File

@ -1,73 +0,0 @@
--- template/app/src/analytics/stats.ts
+++ opensaas-sh/app/src/analytics/stats.ts
@@ -2,10 +2,8 @@
import { type DailyStatsJob } from 'wasp/server/jobs';
import Stripe from 'stripe';
import { stripe } from '../payment/stripe/stripeClient'
-import { listOrders } from '@lemonsqueezy/lemonsqueezy.js';
import { getDailyPageViews, getSources } from './providers/plausibleAnalyticsUtils';
-// import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils';
-import { paymentProcessor } from '../payment/paymentProcessor';
+// import { getDailyPageViews, getSources } from './providers/googleAnalyticsUtils;
export type DailyStatsProps = { dailyStats?: DailyStats; weeklyStats?: DailyStats[]; isLoading?: boolean };
@@ -41,17 +39,7 @@
paidUserDelta -= yesterdaysStats.paidUserCount;
}
- let totalRevenue;
- switch (paymentProcessor.id) {
- case 'stripe':
- totalRevenue = await fetchTotalStripeRevenue();
- break;
- case 'lemonsqueezy':
- totalRevenue = await fetchTotalLemonSqueezyRevenue();
- break;
- default:
- throw new Error(`Unsupported payment processor: ${paymentProcessor.id}`);
- }
+ let totalRevenue = await fetchTotalStripeRevenue()
const { totalViews, prevDayViewsChangePercent } = await getDailyPageViews();
@@ -162,38 +150,3 @@
// Revenue is in cents so we convert to dollars (or your main currency unit)
return totalRevenue / 100;
}
-
-async function fetchTotalLemonSqueezyRevenue() {
- try {
- let totalRevenue = 0;
- let hasNextPage = true;
- let currentPage = 1;
-
- while (hasNextPage) {
- const { data: response } = await listOrders({
- filter: {
- storeId: process.env.LEMONSQUEEZY_STORE_ID,
- },
- page: {
- number: currentPage,
- size: 100,
- },
- });
-
- if (response?.data) {
- for (const order of response.data) {
- totalRevenue += order.attributes.total;
- }
- }
-
- hasNextPage = !response?.meta?.page.lastPage;
- currentPage++;
- }
-
- // Revenue is in cents so we convert to dollars (or your main currency unit)
- return totalRevenue / 100;
- } catch (error) {
- console.error('Error fetching Lemon Squeezy revenue:', error);
- throw error;
- }
-}
\ No newline at end of file

View File

@ -1,19 +0,0 @@
--- template/app/src/auth/LoginPage.tsx
+++ opensaas-sh/app/src/auth/LoginPage.tsx
@@ -1,8 +1,15 @@
+import { Navigate } from 'react-router-dom';
import { Link as WaspRouterLink, routes } from 'wasp/client/router';
-import { LoginForm } from 'wasp/client/auth';
+import { LoginForm, useAuth } from 'wasp/client/auth';
import { AuthPageLayout } from './AuthPageLayout';
export default function Login() {
+ const { data: user } = useAuth();
+
+ if (user) {
+ return <Navigate to='/demo-app' />;
+ }
+
return (
<AuthPageLayout>
<LoginForm />

View File

@ -1,47 +0,0 @@
--- template/app/src/auth/userSignupFields.ts
+++ opensaas-sh/app/src/auth/userSignupFields.ts
@@ -1,11 +1,8 @@
import { z } from 'zod';
import { defineUserSignupFields } from 'wasp/auth/providers/types';
-const adminEmails = process.env.ADMIN_EMAILS?.split(',') || [];
-
export const getEmailUserFields = defineUserSignupFields({
username: (data: any) => data.email,
- isAdmin: (data: any) => adminEmails.includes(data.email),
email: (data: any) => data.email,
});
@@ -29,10 +26,6 @@
const githubData = githubDataSchema.parse(data);
return githubData.profile.login;
},
- isAdmin: (data) => {
- const githubData = githubDataSchema.parse(data);
- return adminEmails.includes(githubData.profile.emails[0].email);
- },
});
// NOTE: if we don't want to access users' emails, we can use scope ["user:read"]
@@ -58,10 +51,6 @@
const googleData = googleDataSchema.parse(data);
return googleData.profile.email;
},
- isAdmin: (data) => {
- const googleData = googleDataSchema.parse(data);
- return adminEmails.includes(googleData.profile.email);
- },
});
export function getGoogleAuthConfig() {
@@ -86,10 +75,6 @@
const discordData = discordDataSchema.parse(data);
return discordData.profile.username;
},
- isAdmin: (data) => {
- const email = discordDataSchema.parse(data).profile.email;
- return !!email && adminEmails.includes(email);
- },
});
export function getDiscordAuthConfig() {

View File

@ -1,85 +0,0 @@
--- template/app/src/client/components/NavBar/NavBar.tsx
+++ opensaas-sh/app/src/client/components/NavBar/NavBar.tsx
@@ -32,6 +32,7 @@
!isLandingPage,
})}
>
+ {/* {isLandingPage && <Announcement />} */}
<nav className='flex items-center justify-between p-6 lg:px-8' aria-label='Global'>
<div className='flex items-center lg:flex-1'>
<WaspRouterLink
@@ -39,9 +40,7 @@
className='flex items-center -m-1.5 p-1.5 text-gray-900 duration-300 ease-in-out hover:text-yellow-500'
>
<NavLogo />
- {isLandingPage && (
- <span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Your Saas</span>
- )}
+ {isLandingPage && <span className='ml-2 text-sm font-semibold leading-6 dark:text-white'>Open Saas</span>}
</WaspRouterLink>
</div>
<div className='flex lg:hidden'>
@@ -56,13 +55,13 @@
</div>
<div className='hidden lg:flex lg:gap-x-12'>{renderNavigationItems(navigationItems)}</div>
<div className='hidden lg:flex lg:flex-1 gap-3 justify-end items-center'>
- <ul className='flex justify-center items-center gap-2 sm:gap-4'>
+ <ul className='ml-4 flex justify-center items-center gap-2 sm:gap-4'>
<DarkModeSwitcher />
</ul>
{isUserLoading ? null : !user ? (
<WaspRouterLink to={routes.LoginRoute.to} className='text-sm font-semibold leading-6 ml-3'>
- <div className='flex items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
- Log in <BiLogIn size='1.1rem' className='ml-1 mt-[0.1rem]' />
+ <div className='flex justify-end items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white test-sm'>
+ Try the demo App <BiLogIn size='1.1rem' className='ml-1' />
</div>
</WaspRouterLink>
) : (
@@ -77,7 +76,7 @@
<Dialog.Panel className='fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-white dark:text-white dark:bg-boxdark px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-gray-900/10'>
<div className='flex items-center justify-between'>
<WaspRouterLink to={routes.LandingPageRoute.to} className='-m-1.5 p-1.5'>
- <span className='sr-only'>Your SaaS</span>
+ <span className='sr-only'>Open SaaS</span>
<NavLogo />
</WaspRouterLink>
<button
@@ -96,7 +95,7 @@
{isUserLoading ? null : !user ? (
<WaspRouterLink to={routes.LoginRoute.to}>
<div className='flex justify-end items-center duration-300 ease-in-out text-gray-900 hover:text-yellow-500 dark:text-white'>
- Log in <BiLogIn size='1.1rem' className='ml-1' />
+ Try the Demo App{' '} <BiLogIn size='1.1rem' className='ml-1' />
</div>
</WaspRouterLink>
) : (
@@ -138,3 +137,27 @@
);
});
}
+
+const ContestURL =
+ 'https://docs.opensaas.sh/blog/';
+
+function Announcement() {
+ return (
+ <div className='flex justify-center items-center gap-3 p-3 w-full bg-gradient-to-r from-[#d946ef] to-[#fc0] font-semibold text-white text-center z-49'>
+ <p onClick={() => window.open(ContestURL, '_blank')} className='hidden lg:block cursor-pointer hover:opacity-90 hover:drop-shadow'>🍪 THE MOST ANNOYING COOKIE BANNER EVER HACKATHON 🤬</p>
+ <div className='hidden lg:block self-stretch w-0.5 bg-white'></div>
+ <div
+ onClick={() => window.open(ContestURL, '_blank')}
+ className='hidden lg:block cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
+ >
+ Enter here and win prizes! →
+ </div>
+ <div
+ onClick={() => window.open(ContestURL, '_blank')}
+ className='lg:hidden cursor-pointer rounded-full bg-neutral-700 px-2.5 py-1 text-xs hover:bg-neutral-600 tracking-wider'
+ >
+ 🍪 The Most Annoying Cookie Banner Contest! 🤬 →
+ </div>
+ </div>
+ );
+}
\ No newline at end of file

View File

@ -1,13 +0,0 @@
--- template/app/src/client/components/cookie-consent/Config.ts
+++ opensaas-sh/app/src/client/components/cookie-consent/Config.ts
@@ -97,8 +97,8 @@
// showPreferencesBtn: 'Manage Individual preferences', // (OPTIONAL) Activates the preferences modal
// TODO: Add your own privacy policy and terms and conditions links below.
footer: `
- <a href="<your-url-here>" target="_blank">Privacy Policy</a>
- <a href="<your-url-here>" target="_blank">Terms and Conditions</a>
+ <a>Privacy Policy</a>
+ <a>Terms and Conditions</a>
`,
},
// The showPreferencesBtn activates this modal to manage individual preferences https://cookieconsent.orestbida.com/reference/configuration-reference.html#translation-preferencesmodal

View File

@ -1,21 +0,0 @@
--- template/app/src/file-upload/operations.ts
+++ opensaas-sh/app/src/file-upload/operations.ts
@@ -18,6 +18,18 @@
throw new HttpError(401);
}
+ const numberOfFilesByUser = await context.entities.File.count({
+ where: {
+ user: {
+ id: context.user.id,
+ },
+ },
+ });
+
+ if (numberOfFilesByUser >= 2) {
+ throw new HttpError(403, 'Thanks for trying Open SaaS. This demo only allows 2 file uploads per user.');
+ }
+
const userInfo = context.user.id;
const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo });

View File

@ -1,78 +0,0 @@
--- template/app/src/landing-page/components/Clients.tsx
+++ opensaas-sh/app/src/landing-page/components/Clients.tsx
@@ -1,23 +1,64 @@
+import logo from '../../client/static/logo.webp';
import AstroLogo from "../logos/AstroLogo";
-import OpenAILogo from "../logos/OpenAILogo";
import PrismaLogo from "../logos/PrismaLogo";
-import SalesforceLogo from "../logos/SalesforceLogo";
+import OpenAILogo from "../logos/OpenAILogo";
export default function Clients() {
return (
<div className='mt-12 mx-auto max-w-7xl px-6 lg:px-8 flex flex-col items-between gap-y-6'>
- <h2 className='mb-6 text-center font-semibold tracking-wide text-gray-500 dark:text-white'>
- Built with / Used by:
+ <h2 className='mb-6 text-center font-semibold tracking-wide text-gray-500'>
+ Built and Ships with
</h2>
<div className='mx-auto grid max-w-lg grid-cols-2 items-center gap-x-8 gap-y-12 sm:max-w-xl md:grid-cols-4 sm:gap-x-10 sm:gap-y-14 lg:mx-0 lg:max-w-none'>
- {
- [<SalesforceLogo />, <PrismaLogo />, <AstroLogo />, <OpenAILogo />].map((logo, index) => (
- <div key={index} className='flex justify-center col-span-1 max-h-12 w-full object-contain dark:opacity-80'>
- {logo}
- </div>
- ))
- }
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale opacity-100'
+ src='https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/512px-React-icon.svg.png'
+ alt='React'
+ height={48}
+ />
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale opacity-60 dark:filter dark:brightness-0 dark:invert'
+ src='https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Node.js_logo.svg/590px-Node.js_logo.svg.png'
+ alt='NodeJS'
+ height={48}
+ />
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale opacity-80'
+ src={logo}
+ alt='Wasp'
+ height={48}
+ />
+
+ <div className='flex justify-center col-span-1 max-h-12 w-full object-contain grayscale opacity-80'>
+ <PrismaLogo />
+ </div>
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale '
+ src='https://upload.wikimedia.org/wikipedia/commons/thumb/d/d5/Tailwind_CSS_Logo.svg/512px-Tailwind_CSS_Logo.svg.png'
+ alt='Tailwind CSS'
+ height={48}
+ />
+
+ <img
+ className='col-span-1 max-h-12 w-full object-contain grayscale opacity-80 dark:opacity-60 dark:filter dark:brightness-0 dark:invert'
+ src='https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Stripe_Logo%2C_revised_2016.svg/512px-Stripe_Logo%2C_revised_2016.svg.png'
+ alt='Stripe'
+ height={48}
+ />
+
+ <div className='flex justify-center col-span-1 w-full max-h-12 object-contain grayscale opacity-75'>
+ <AstroLogo />
+ </div>
+
+ <div className='flex justify-center col-span-1 w-full max-h-12 object-contain grayscale opacity-80'>
+ <OpenAILogo />
+ </div>
+
</div>
</div>
)

View File

@ -1,28 +0,0 @@
--- template/app/src/landing-page/components/FAQ.tsx
+++ opensaas-sh/app/src/landing-page/components/FAQ.tsx
@@ -7,20 +7,20 @@
export default function FAQ({ faqs }: { faqs: FAQ[] }) {
return (
- <div className='mt-32 mx-auto max-w-2xl divide-y divide-gray-900/10 dark:divide-gray-200/10 px-6 pb-8 sm:pb-24 sm:pt-12 lg:max-w-7xl lg:px-8 lg:py-32'>
+ <div className='mt-32 mx-auto max-w-2xl divide-y divide-gray-900/10 px-6 pb-8 sm:pb-24 sm:pt-12 lg:max-w-7xl lg:px-8 lg:py-32'>
<h2 className='text-2xl font-bold leading-10 tracking-tight text-gray-900 dark:text-white'>
Frequently asked questions
</h2>
- <dl className='mt-10 space-y-8 divide-y divide-gray-900/10'>
+ <dl className='mt-10 space-y-8 divide-y divide-gray-900/10 dark:divide-gray-100/10'>
{faqs.map((faq) => (
<div key={faq.id} className='pt-8 lg:grid lg:grid-cols-12 lg:gap-8'>
<dt className='text-base font-semibold leading-7 text-gray-900 lg:col-span-5 dark:text-white'>
{faq.question}
</dt>
- <dd className='flex items-center justify-start gap-2 mt-4 lg:col-span-7 lg:mt-0'>
- <p className='text-base leading-7 text-gray-600 dark:text-white'>{faq.answer}</p>
+ <dd className='mt-4 lg:col-span-7 lg:mt-0'>
+ <p className='text-base leading-7 text-gray-600 dark:text-gray-400'>{faq.answer}</p>
{faq.href && (
- <a href={faq.href} className='text-base leading-7 text-yellow-500 hover:text-yellow-600'>
+ <a href={faq.href} className='mt-4 text-base leading-7 text-yellow-500 hover:text-yellow-600'>
Learn more →
</a>
)}

View File

@ -1,43 +0,0 @@
--- template/app/src/landing-page/components/Features.tsx
+++ opensaas-sh/app/src/landing-page/components/Features.tsx
@@ -10,25 +10,27 @@
<div id='features' className='mx-auto mt-48 max-w-7xl px-6 lg:px-8'>
<div className='mx-auto max-w-2xl text-center'>
<p className='mt-2 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl dark:text-white'>
- The <span className='text-yellow-500'>Best</span> Features
+ <span className='text-yellow-500'>100%</span> Open-Source
</p>
- <p className='mt-6 text-lg leading-8 text-gray-600 dark:text-white'>
- Don't work harder.
- <br /> Work smarter.
+ <p className='mt-6 text-lg leading-8 text-gray-600 dark:text-gray-400'>
+ No vendor lock-in.
+ <br /> Deploy anywhere.
</p>
</div>
<div className='mx-auto mt-16 max-w-2xl sm:mt-20 lg:mt-24 lg:max-w-4xl'>
<dl className='grid max-w-xl grid-cols-1 gap-x-8 gap-y-10 lg:max-w-none lg:grid-cols-2 lg:gap-y-16'>
{features.map((feature) => (
- <div key={feature.name} className='relative pl-16'>
- <dt className='text-base font-semibold leading-7 text-gray-900 dark:text-white'>
- <div className='absolute left-0 top-0 flex h-10 w-10 items-center justify-center border border-yellow-400 bg-yellow-100/50 dark:bg-boxdark rounded-lg'>
- <div className='text-2xl'>{feature.icon}</div>
- </div>
- {feature.name}
- </dt>
- <dd className='mt-2 text-base leading-7 text-gray-600 dark:text-white'>{feature.description}</dd>
- </div>
+ <a href={feature.href} className='group'>
+ <div key={feature.name} className='relative pl-16'>
+ <dt className='text-base font-semibold leading-7 text-gray-900 dark:text-white group-hover:underline'>
+ <div className='absolute left-0 top-0 flex h-10 w-10 items-center justify-center border border-yellow-400 bg-yellow-100/50 dark:bg-boxdark rounded-lg group-hover:border-yellow-500'>
+ <div className='text-2xl group-hover:opacity-80 '>{feature.icon}</div>
+ </div>
+ {feature.name}
+ </dt>
+ <dd className='mt-2 text-base leading-7 text-gray-600 dark:text-gray-400'>{feature.description}</dd>
+ </div>
+ </a>
))}
</dl>
</div>

View File

@ -1,11 +0,0 @@
--- template/app/src/landing-page/components/Footer.tsx
+++ opensaas-sh/app/src/landing-page/components/Footer.tsx
@@ -13,7 +13,7 @@
<div className='mx-auto mt-6 max-w-7xl px-6 lg:px-8 dark:bg-boxdark-2'>
<footer
aria-labelledby='footer-heading'
- className='relative border-t border-gray-900/10 dark:border-gray-200/10 py-24 sm:mt-32'
+ className='relative border-t border-gray-900/10 dark:border-gray-100/10 py-24 sm:mt-32'
>
<h2 id='footer-heading' className='sr-only'>
Footer

View File

@ -1,93 +0,0 @@
--- template/app/src/landing-page/components/Hero.tsx
+++ opensaas-sh/app/src/landing-page/components/Hero.tsx
@@ -1,7 +1,25 @@
-import openSaasBannerWebp from '../../client/static/open-saas-banner.webp';
-import { DocsUrl } from '../../shared/common';
+import { useState, useEffect } from 'react';
+import { AiFillGithub } from 'react-icons/ai';
+import { DocsUrl, GithubUrl } from '../../shared/common';
export default function Hero() {
+ const [repoInfo, setRepoInfo] = useState<null | any>(null);
+
+ useEffect(() => {
+ const fetchRepoInfo = async () => {
+ try {
+ const response = await fetch(
+ 'https://api.github.com/repos/wasp-lang/open-saas'
+ );
+ const data = await response.json();
+ setRepoInfo(data);
+ } catch (error) {
+ console.error('Error fetching repo info', error);
+ }
+ };
+ fetchRepoInfo();
+ }, []);
+
return (
<div className='relative pt-14 w-full'>
<TopGradient />
@@ -9,31 +27,47 @@
<div className='py-24 sm:py-32'>
<div className='mx-auto max-w-8xl px-6 lg:px-8'>
<div className='lg:mb-18 mx-auto max-w-3xl text-center'>
- <h1 className='text-4xl font-bold text-gray-900 sm:text-6xl dark:text-white'>
- Some <span className='italic'>cool</span> words about your product
+ <h1 className='text-4xl font-bold text-gray-900 sm:text-6xl dark:text-white tracking-tight'>
+ The <span className='italic'>free</span> SaaS template with
+ superpowers
</h1>
- <p className='mt-6 mx-auto max-w-2xl text-lg leading-8 text-gray-600 dark:text-white'>
- With some more exciting words about your product!
+ <p className='mt-6 mx-auto max-w-2xl text-lg leading-8 text-gray-600 dark:text-gray-400'>
+ An open-source, feature-rich, full-stack React + NodeJS
+ template that manages features for you.
</p>
<div className='mt-10 flex items-center justify-center gap-x-6'>
<a
href={DocsUrl}
- className='rounded-md px-3.5 py-2.5 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-200 hover:ring-2 hover:ring-yellow-300 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:text-white'
+ className='rounded-md px-6 py-4 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-200 hover:ring-2 hover:ring-yellow-300 shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:text-white'
>
Get Started <span aria-hidden='true'>→</span>
</a>
+ <a
+ href={GithubUrl}
+ className='group relative flex items-center justify-center rounded-md bg-gray-100 px-6 py-4 text-sm font-semibold shadow-sm ring-1 ring-inset ring-gray-200 dark:bg-gray-700 hover:ring-2 hover:ring-yellow-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
+ >
+ {/* <AiFillGithub size='1.25rem' className='mr-2' /> */}
+ View the Repo
+ {repoInfo!! && (
+ <>
+ <span className='absolute -top-3 -right-7 inline-flex items-center gap-x-1 rounded-full ring-1 group-hover:ring-2 ring-inset ring-yellow-300 bg-yellow-100 px-2 py-1 text-sm font-medium text-yellow-800'>
+ <AiFillGithub size='1rem' />
+ {repoInfo.stargazers_count}
+ </span>
+ </>
+ )}
+ </a>
</div>
</div>
- <div className='mt-14 flow-root sm:mt-14'>
- <div className='-m-2 flex justify-center rounded-xl lg:-m-4 lg:rounded-2xl lg:p-4'>
- <img
- src={openSaasBannerWebp}
- alt='App screenshot'
- width={1000}
- height={530}
- loading='lazy'
- className='rounded-md shadow-2xl ring-1 ring-gray-900/10'
- />
+ <div className='mt-14 flow-root sm:mt-14 '>
+ <div className='-m-2 mx-auto rounded-xl lg:-m-4 lg:rounded-2xl lg:p-4'>
+ <iframe
+ className=' mx-auto w-full md:w-[85%] aspect-[4/3] shadow-2xl'
+ src='https://cards.producthunt.com/cards/posts/436467?v=1'
+ // width={850}
+ // height={689}
+ allowFullScreen
+ ></iframe>
</div>
</div>
</div>

View File

@ -1,171 +0,0 @@
--- template/app/src/landing-page/contentSections.ts
+++ opensaas-sh/app/src/landing-page/contentSections.ts
@@ -1,75 +1,132 @@
import type { NavigationItem } from '../client/components/NavBar/NavBar';
-import { routes } from 'wasp/client/router';
-import { DocsUrl, BlogUrl } from '../shared/common';
-import daBoiAvatar from '../client/static/da-boi.webp';
-import avatarPlaceholder from '../client/static/avatar-placeholder.webp';
+import { DocsUrl, BlogUrl, GithubUrl } from '../shared/common';
export const landingPageNavigationItems: NavigationItem[] = [
{ name: 'Features', to: '#features' },
- { name: 'Pricing', to: routes.PricingPageRoute.to },
{ name: 'Documentation', to: DocsUrl },
{ name: 'Blog', to: BlogUrl },
];
export const features = [
{
- name: 'Cool Feature #1',
- description: 'Describe your cool feature here.',
+ name: 'Open-Source Philosophy',
+ description:
+ 'The repo and framework are 100% open-source, and so are the services wherever possible. Still missing something? Contribute!',
icon: '🤝',
href: DocsUrl,
},
{
- name: 'Cool Feature #2',
- description: 'Describe your cool feature here.',
+ name: 'DIY Auth, Done For You',
+ description: 'Pre-configured full-stack Auth that you own. No 3rd-party services or hidden fees.',
icon: '🔐',
- href: DocsUrl,
+ href: DocsUrl + '/guides/authentication/',
},
{
- name: 'Cool Feature #3',
- description: 'Describe your cool feature here.',
+ name: 'Full-stack Type Safety',
+ description:
+ 'Full support for TypeScript with auto-generated types that span the whole stack. Nothing to configure!',
icon: '🥞',
href: DocsUrl,
},
{
- name: 'Cool Feature #4',
- description: 'Describe your cool feature here.',
+ name: 'Stripe / Lemon Squeezy Integration',
+ description: "No SaaS is complete without payments. We've pre-configured checkout and webhooks. Just choose a provider and start cashing out.",
icon: '💸',
+ href: DocsUrl + '/guides/payments-integration/',
+ },
+ {
+ name: 'Admin Dashboard',
+ description: 'Graphs! Tables! Analytics w/ Plausible or Google! All in one place. Ooooooooooh.',
+ icon: '📈',
+ href: DocsUrl + '/general/admin-dashboard/',
+ },
+ {
+ name: 'Email Sending',
+ description:
+ 'Email sending built-in. Combine it with the cron jobs feature to easily send emails to your customers.',
+ icon: '📧',
+ href: DocsUrl + '/guides/email-sending/',
+ },
+ {
+ name: 'OpenAI API Implemented',
+ description: 'Have a sweet AI-powered app concept? Get your idea shipped to potential customers in days!',
+ icon: '🤖',
+ href: DocsUrl,
+ },
+ {
+ name: 'File Uploads with AWS',
+ description: 'File upload examples with AWS S3 presigned URLs are included and fully documented!',
+ icon: '📁',
+ href: DocsUrl + '/guides/file-uploading/',
+ },
+ {
+ name: 'Blog w/ Astro',
+ description:
+ 'Built-in blog with the Astro framework. Write your posts in Markdown, and watch your SEO performance take off.',
+ icon: '📝',
+ href: DocsUrl + '/start/guided-tour/',
+ },
+ {
+ name: 'Deploy Anywhere. Easily.',
+ description:
+ 'No vendor lock-in because you own all your code. Deploy yourself, or let Wasp deploy it for you with a single command.',
+ icon: '🚀 ',
+ href: DocsUrl + '/guides/deploying/',
+ },
+ {
+ name: 'E2E Tests w/ Playwright',
+ description: 'Move fast without breaking too much. Tests and a CI pipeline w/ GitHub Actions are set up for you.',
+ icon: '🧪',
+ href: DocsUrl + '/guides/tests/',
+ },
+ {
+ name: 'Complete Documentation & Support',
+ description: "We don't leave you hanging. We have detailed docs and a Discord community to help!",
+ icon: '🫂',
href: DocsUrl,
},
];
export const testimonials = [
{
- name: 'Da Boi',
- role: 'Wasp Mascot',
- avatarSrc: daBoiAvatar,
- socialUrl: 'https://twitter.com/wasplang',
- quote: "I don't even know how to code. I'm just a plushie.",
- },
- {
- name: 'Mr. Foobar',
- role: 'Founder @ Cool Startup',
- avatarSrc: avatarPlaceholder,
- socialUrl: '',
- quote: 'This product makes me cooler than I already am.',
- },
- {
- name: 'Jamie',
- role: 'Happy Customer',
- avatarSrc: avatarPlaceholder,
- socialUrl: '#',
- quote: 'My cats love it!',
+ name: 'Max Khamrovskyi',
+ role: 'Senior Eng @ Red Hat',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1719397191205179392/V_QrGPSO_400x400.jpg',
+ socialUrl: 'https://twitter.com/maksim36ua',
+ quote: 'I used Wasp to build and sell my AI-augmented SaaS app for marketplace vendors within two months!',
+ },
+ {
+ name: 'Tim Skaggs',
+ role: 'Founder @ Antler US',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1802196804236091392/ZG0OE_fO_400x400.jpg',
+ socialUrl: 'https://twitter.com/tskaggs',
+ quote: 'Nearly done with a MVP in 3 days of part-time work... and deployed on Fly.io in 10 minutes.',
+ },
+ {
+ name: 'Jonathan Cocharan',
+ role: 'Entrepreneur',
+ avatarSrc: 'https://pbs.twimg.com/profile_images/926142421653753857/o6Hmcbr7_400x400.jpg',
+ socialUrl: 'https://twitter.com/jonathancocharan',
+ quote:
+ 'In just 6 nights... my SaaS app is live 🎉! Huge thanks to the amazing @wasplang community 🙌 for their guidance along the way. These tools are incredibly efficient 🤯!',
},
];
export const faqs = [
{
id: 1,
- question: 'Whats the meaning of life?',
- answer: '42.',
- href: 'https://en.wikipedia.org/wiki/42_(number)',
+ question: 'Why is this SaaS Template free and open-source?',
+ answer:
+ 'We believe the best product is made when the community puts their heads together. We also believe a quality starting point for a web app should be free and available to everyone. Our hope is that together we can create the best SaaS template out there and bring our ideas to customers quickly.',
+ },
+ {
+ id: 2,
+ question: "What's Wasp?",
+ href: 'https://wasp-lang.dev',
+ answer: "It's the fastest way to develop full-stack React + NodeJS + Prisma apps and it's what gives this template superpowers. Wasp relies on React, NodeJS, and Prisma to define web components and server queries and actions. Wasp's secret sauce is its compiler which takes the client, server code, and config file and outputs the client app, server app and deployment code, supercharging the development experience. Combined with this template, you can build a SaaS app in record time.",
},
];
export const footerNavigation = {
app: [
+ { name: 'Github', href: GithubUrl },
{ name: 'Documentation', href: DocsUrl },
{ name: 'Blog', href: BlogUrl },
],

View File

@ -1,52 +0,0 @@
--- template/app/src/payment/PricingPage.tsx
+++ opensaas-sh/app/src/payment/PricingPage.tsx
@@ -7,6 +7,7 @@
import { cn } from '../client/cn';
const bestDealPaymentPlanId: PaymentPlanId = PaymentPlanId.Pro;
+const PaymentsDocsURL = 'https://docs.opensaas.sh/guides/payments-integration/';
interface PaymentPlanCard {
name: string;
@@ -82,7 +83,7 @@
}
if (!customerPortalUrl) {
- throw new Error(`Customer Portal does not exist for user ${user.id}`)
+ throw new Error(`Customer Portal does not exist for user ${user.id}`);
}
window.open(customerPortalUrl, '_blank');
@@ -96,11 +97,18 @@
Pick your <span className='text-yellow-500'>pricing</span>
</h2>
</div>
- <p className='mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-white'>
- Choose between Stripe and LemonSqueezy as your payment provider. Just add your Product IDs! Try it
- out below with test credit card number <br />
- <span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
- </p>
+ <div className='mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-white flex flex-wrap items-center justify-center space-x-2'>
+ <p>
+ Choose between
+ <a href={PaymentsDocsURL} target='_blank' className='text-purple-400 drop-shadow-sm'> Stripe </a>
+ and
+ <a href={PaymentsDocsURL} target='_blank' className='text-yellow-500 drop-shadow-sm'> Lemon Squeezy </a>
+ as your payment provider. Just add your Product IDs! Try it out below with test credit card number
+ <br />
+ <span className='px-2 py-1 bg-gray-100 rounded-md text-gray-500'>4242 4242 4242 4242 4242</span>
+ </p>
+ </div>
+
<div className='isolate mx-auto mt-16 grid max-w-md grid-cols-1 gap-y-8 lg:gap-x-8 sm:mt-20 lg:mx-0 lg:max-w-none lg:grid-cols-3'>
{Object.values(PaymentPlanId).map((planId) => (
<div
@@ -187,7 +195,7 @@
)}
disabled={isPaymentLoading}
>
- {!!user ? 'Buy plan' : 'Log in to buy plan'}
+ {!!user ? 'Buy Plan' : 'Log in to buy plan'}
</button>
)}
</div>

View File

@ -1,20 +0,0 @@
--- template/app/src/payment/paymentProcessor.ts
+++ opensaas-sh/app/src/payment/paymentProcessor.ts
@@ -3,7 +3,6 @@
import type { MiddlewareConfigFn } from 'wasp/server';
import { PrismaClient } from '@prisma/client';
import { stripePaymentProcessor } from './stripe/paymentProcessor';
-import { lemonSqueezyPaymentProcessor } from './lemonSqueezy/paymentProcessor';
export interface CreateCheckoutSessionArgs {
userId: string;
@@ -24,9 +23,4 @@
webhookMiddlewareConfigFn: MiddlewareConfigFn;
}
-/**
- * Choose which payment processor you'd like to use, then delete the
- * other payment processor code that you're not using from `/src/payment`
- */
-// export const paymentProcessor: PaymentProcessor = lemonSqueezyPaymentProcessor;
export const paymentProcessor: PaymentProcessor = stripePaymentProcessor;

View File

@ -1,11 +0,0 @@
--- template/app/src/payment/plans.ts
+++ opensaas-sh/app/src/payment/plans.ts
@@ -9,7 +9,7 @@
}
export interface PaymentPlan {
- // Returns the id under which this payment plan is identified on your payment processor.
+ // Returns the id under which this payment plan is identified on your payment processor.
// E.g. this might be price id on Stripe, or variant id on LemonSqueezy.
getPaymentProcessorPlanId: () => string;
effect: PaymentPlanEffect;

View File

@ -1,15 +0,0 @@
--- template/app/src/payment/stripe/paymentDetails.ts
+++ opensaas-sh/app/src/payment/stripe/paymentDetails.ts
@@ -14,10 +14,10 @@
) => {
return userDelegate.update({
where: {
- paymentProcessorUserId: userStripeId
+ stripeId: userStripeId
},
data: {
- paymentProcessorUserId: userStripeId,
+ stripeId: userStripeId,
subscriptionPlan,
subscriptionStatus,
datePaid,

View File

@ -1,11 +0,0 @@
--- template/app/src/payment/stripe/paymentProcessor.ts
+++ opensaas-sh/app/src/payment/stripe/paymentProcessor.ts
@@ -21,7 +21,7 @@
id: userId
},
data: {
- paymentProcessorUserId: customer.id
+ stripeId: customer.id
}
})
if (!stripeSession.url) throw new Error('Error creating Stripe Checkout Session');

View File

@ -1,16 +0,0 @@
--- template/app/src/server/scripts/dbSeeds.ts
+++ opensaas-sh/app/src/server/scripts/dbSeeds.ts
@@ -38,9 +38,11 @@
sendNewsletter: false,
credits,
subscriptionStatus,
- lemonSqueezyCustomerPortalUrl: null,
- paymentProcessorUserId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
+ stripeId: hasUserPaidOnStripe ? `cus_test_${faker.string.uuid()}` : null,
datePaid: hasUserPaidOnStripe ? faker.date.between({ from: createdAt, to: lastActiveTimestamp }) : null,
subscriptionPlan: subscriptionStatus ? faker.helpers.arrayElement(getSubscriptionPaymentPlanIds()) : null,
+ // For the demo app, we want to default isMockUser to true so that our admin dash only shows mock users
+ // and not real users signing up to test the app
+ isMockUser: true,
};
}

View File

@ -1,7 +0,0 @@
--- template/app/src/shared/common.ts
+++ opensaas-sh/app/src/shared/common.ts
@@ -1,2 +1,3 @@
export const DocsUrl = 'https://docs.opensaas.sh';
export const BlogUrl = 'https://docs.opensaas.sh/blog';
+export const GithubUrl = 'https://github.com/wasp-lang/open-saas';
\ No newline at end of file

View File

@ -1,52 +0,0 @@
--- template/app/src/user/operations.ts
+++ opensaas-sh/app/src/user/operations.ts
@@ -52,7 +52,10 @@
subscriptionStatus?: SubscriptionStatus[];
};
type GetPaginatedUsersOutput = {
- users: Pick<User, 'id' | 'email' | 'username' | 'lastActiveTimestamp' | 'subscriptionStatus' | 'paymentProcessorUserId'>[];
+ users: Pick<
+ User,
+ 'id' | 'email' | 'username' | 'lastActiveTimestamp' | 'subscriptionStatus' | 'stripeId'
+ >[];
totalPages: number;
};
@@ -65,8 +68,10 @@
}
const allSubscriptionStatusOptions = args.subscriptionStatus as Array<string | null> | undefined;
- const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null)
- let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as string[] | undefined
+ const hasNotSubscribed = allSubscriptionStatusOptions?.find((status) => status === null);
+ let subscriptionStatusStrings = allSubscriptionStatusOptions?.filter((status) => status !== null) as
+ | string[]
+ | undefined;
const queryResults = await context.entities.User.findMany({
skip: args.skip,
@@ -79,6 +84,7 @@
mode: 'insensitive',
},
isAdmin: args.isAdmin,
+ isMockUser: true,
},
{
OR: [
@@ -103,7 +109,7 @@
isAdmin: true,
lastActiveTimestamp: true,
subscriptionStatus: true,
- paymentProcessorUserId: true,
+ stripeId: true,
},
orderBy: {
id: 'desc',
@@ -119,6 +125,7 @@
mode: 'insensitive',
},
isAdmin: args.isAdmin,
+ isMockUser: true,
},
{
OR: [

View File

@ -1,22 +0,0 @@
--- template/app/tailwind.config.cjs
+++ opensaas-sh/app/tailwind.config.cjs
@@ -8,7 +8,8 @@
theme: {
extend: {
fontFamily: {
- satoshi: ['Satoshi', 'system-ui', 'sans-serif'],
+ sans: ['ui-monospace', 'Liberation Mono', 'Menlo', 'monospace'],
+ satoshi: ['Satoshi', 'sans-serif'],
},
colors: {
current: 'currentColor',
@@ -246,6 +247,9 @@
'spin-2': 'spin 2s linear infinite',
'spin-3': 'spin 3s linear infinite',
},
+ aspectRatio: {
+ '4/3': '4 / 3',
+ },
},
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],