Add docs for review
53
opensaas-sh/README.md
Normal file
@ -0,0 +1,53 @@
|
||||
# OpenSaas.sh
|
||||
|
||||
This is the https://opensaas.sh page and demo app, built with the Open Saas template!
|
||||
|
||||
It consists of a Wasp app for showcasing the Open Saas template (+ landing page), while the Astro blog is blog and docs for the Open Saas template, found at https://docs.opensaas.sh.
|
||||
|
||||
Inception :)!
|
||||
|
||||
## Development
|
||||
|
||||
### Demo app (app_diff/)
|
||||
|
||||
Since the demo app is just the open saas template with some small tweaks, and we want to be able to easily keep it up to date as the template changes, we don't version (in git) the actual demo app code, instead we version the diffs between it and the template: `app_diff/`.
|
||||
|
||||
So because we don't version the actual demo app (`app/`) but its diffs instead (`app_diff`), the typical workflow is as follows:
|
||||
|
||||
1. Run `./tools/patch.sh` to generate `app/` from `../template/` and `app_diff/`.
|
||||
2. If there are any conflicts (normally due to updates to the template), modify `app/` till you resolve them. Do any additional changes also if you wish.
|
||||
3. Generate new `app_diff/`, based on the current updated `app/`, by running `./tools/diff.sh`.
|
||||
|
||||
**Running on MacOS**
|
||||
|
||||
If you're running the `patch.sh` or `diff.sh` scripts on Mac, you need to install:
|
||||
|
||||
- `grealpath` (packaged within `coreutils`),
|
||||
- `gpatch`,
|
||||
- and `diffutils`.
|
||||
|
||||
You should also create aliases for `realpath` and `patch`:
|
||||
|
||||
```sh
|
||||
brew install coreutils # contains grealpath
|
||||
brew install gpatch
|
||||
brew install diffutils
|
||||
|
||||
echo 'alias realpath="grealpath"' >> ~/.zshrc
|
||||
echo 'alias patch="gpatch"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
Make sure not to commit `app/` to git. It is currently (until we resolve this) not added to .gitignore because that messes up diffing for us.
|
||||
|
||||
### Blog (blog/)
|
||||
|
||||
Blog (and docs in it) is currently tracked in whole, as it has quite some content, so updating it to the latest version of Open Saas is done manually, but it might be interesting to also move it to the `diff` approach, as we use for the demo app, if it turns out to be a good match.
|
||||
|
||||
For more info on authoring content for the docs and blog, including information on custom components, see the [blog/README.md](blog/README.md).
|
||||
|
||||
## Deployment
|
||||
|
||||
App: check its README.md (after you generate it with `.tools/patch.sh`) .
|
||||
|
||||
Blog (docs): hosted on Netlify.
|
5
opensaas-sh/app_diff/.env.client.diff
Normal file
@ -0,0 +1,5 @@
|
||||
--- 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
|
28
opensaas-sh/app_diff/.env.vault.diff
Normal file
@ -0,0 +1,28 @@
|
||||
--- 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"
|
20
opensaas-sh/app_diff/.gitignore.diff
Normal file
@ -0,0 +1,20 @@
|
||||
--- 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
|
30
opensaas-sh/app_diff/README.md.diff
Normal file
@ -0,0 +1,30 @@
|
||||
--- 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.
|
9
opensaas-sh/app_diff/deletions
Normal file
@ -0,0 +1,9 @@
|
||||
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
|
28
opensaas-sh/app_diff/fly-client.toml.diff
Normal file
@ -0,0 +1,28 @@
|
||||
--- 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
|
26
opensaas-sh/app_diff/fly-server.toml.diff
Normal file
@ -0,0 +1,26 @@
|
||||
--- 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
|
118
opensaas-sh/app_diff/main.wasp.diff
Normal file
@ -0,0 +1,118 @@
|
||||
--- 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
|
@ -0,0 +1,121 @@
|
||||
--- 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;
|
@ -0,0 +1,17 @@
|
||||
--- 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;
|
@ -0,0 +1,18 @@
|
||||
--- 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;
|
@ -0,0 +1,47 @@
|
||||
--- 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;
|
@ -0,0 +1,24 @@
|
||||
--- 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";
|
@ -0,0 +1,8 @@
|
||||
--- 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";
|
@ -0,0 +1,11 @@
|
||||
--- 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";
|
@ -0,0 +1,68 @@
|
||||
--- 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;
|
@ -0,0 +1,19 @@
|
||||
--- 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");
|
@ -0,0 +1,11 @@
|
||||
--- 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";
|
7
opensaas-sh/app_diff/migrations/migration_lock.toml.diff
Normal file
@ -0,0 +1,7 @@
|
||||
--- 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
|
11740
opensaas-sh/app_diff/package-lock.json.diff
Normal file
21
opensaas-sh/app_diff/package.json.diff
Normal file
@ -0,0 +1,21 @@
|
||||
--- 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",
|
18
opensaas-sh/app_diff/schema.prisma.diff
Normal file
@ -0,0 +1,18 @@
|
||||
--- 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)
|
@ -0,0 +1,91 @@
|
||||
--- 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>
|
@ -0,0 +1,54 @@
|
||||
--- 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 />
|
@ -0,0 +1,19 @@
|
||||
--- 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'>
|
@ -0,0 +1,29 @@
|
||||
--- 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,
|
73
opensaas-sh/app_diff/src/analytics/stats.ts.diff
Normal file
@ -0,0 +1,73 @@
|
||||
--- 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
|
19
opensaas-sh/app_diff/src/auth/LoginPage.tsx.diff
Normal file
@ -0,0 +1,19 @@
|
||||
--- 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 />
|
47
opensaas-sh/app_diff/src/auth/userSignupFields.ts.diff
Normal file
@ -0,0 +1,47 @@
|
||||
--- 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() {
|
@ -0,0 +1,85 @@
|
||||
--- 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
|
@ -0,0 +1,13 @@
|
||||
--- 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
|
21
opensaas-sh/app_diff/src/file-upload/operations.ts.diff
Normal file
@ -0,0 +1,21 @@
|
||||
--- 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 });
|
@ -0,0 +1,78 @@
|
||||
--- 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>
|
||||
)
|
@ -0,0 +1,28 @@
|
||||
--- 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>
|
||||
)}
|
@ -0,0 +1,43 @@
|
||||
--- 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>
|
@ -0,0 +1,11 @@
|
||||
--- 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
|
@ -0,0 +1,93 @@
|
||||
--- 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>
|
171
opensaas-sh/app_diff/src/landing-page/contentSections.ts.diff
Normal file
@ -0,0 +1,171 @@
|
||||
--- 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 },
|
||||
],
|
52
opensaas-sh/app_diff/src/payment/PricingPage.tsx.diff
Normal file
@ -0,0 +1,52 @@
|
||||
--- 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>
|
20
opensaas-sh/app_diff/src/payment/paymentProcessor.ts.diff
Normal file
@ -0,0 +1,20 @@
|
||||
--- 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;
|
11
opensaas-sh/app_diff/src/payment/plans.ts.diff
Normal file
@ -0,0 +1,11 @@
|
||||
--- 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;
|
@ -0,0 +1,15 @@
|
||||
--- 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,
|
@ -0,0 +1,11 @@
|
||||
--- 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');
|
16
opensaas-sh/app_diff/src/server/scripts/dbSeeds.ts.diff
Normal file
@ -0,0 +1,16 @@
|
||||
--- 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,
|
||||
};
|
||||
}
|
7
opensaas-sh/app_diff/src/shared/common.ts.diff
Normal file
@ -0,0 +1,7 @@
|
||||
--- 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
|
52
opensaas-sh/app_diff/src/user/operations.ts.diff
Normal file
@ -0,0 +1,52 @@
|
||||
--- 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: [
|
22
opensaas-sh/app_diff/tailwind.config.cjs.diff
Normal file
@ -0,0 +1,22 @@
|
||||
--- 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')],
|
24
opensaas-sh/blog/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# build output
|
||||
dist/
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
4
opensaas-sh/blog/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
opensaas-sh/blog/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
172
opensaas-sh/blog/README.md
Normal file
@ -0,0 +1,172 @@
|
||||
# OpenSaaS Docs and Blog
|
||||
|
||||
This is the docs and blog for the OpenSaaS.sh website, [](https://starlight.astro.build)
|
||||
|
||||
|
||||
## 🚀 Project Structure
|
||||
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── components/
|
||||
│ ├── content/
|
||||
│ │ ├── docs/
|
||||
│ │ │ ├── blog/
|
||||
│ │ │ ├── guides/
|
||||
│ │ │ └── ...
|
||||
│ │ └── config.ts
|
||||
│ └── env.d.ts
|
||||
├── astro.config.mjs
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
||||
|
||||
Blog posts are in the `src/content/docs/blog/` directory. Use `.mdx` files for blog posts.
|
||||
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
Static assets, like favicons and banner images, can be placed in the `public/` directory.
|
||||
|
||||
We have a number of custom components in `src/components/` that you can use in your blog posts and docs.
|
||||
|
||||
## Custom Components
|
||||
|
||||
Custom components in the `src/components/` that replace default Starlight components are imported into the `astro.config.mjs` file:
|
||||
|
||||
```js
|
||||
components: {
|
||||
SiteTitle: './src/components/MyHeader.astro',
|
||||
ThemeSelect: './src/components/MyThemeSelect.astro',
|
||||
Head: './src/components/HeadWithOGImage.astro',
|
||||
PageTitle: './src/components/TitleWithBannerImage.astro',
|
||||
},
|
||||
```
|
||||
|
||||
Other components can be imported into your blog posts and docs using the `import` statement:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Open SaaS Tutorial"
|
||||
date: 2024-12-10
|
||||
//...
|
||||
---
|
||||
import VideoPlayer from '../../../components/VideoPlayer.astro';
|
||||
```
|
||||
|
||||
### HeadWithOGImage
|
||||
|
||||
This component is used to generate the Open Graph (OG) meta tags for the social media preview images for each doc and blog post.
|
||||
|
||||
It checks if a banner image exists in `./public/banner-images` with the same name as the blog post but with a `.webp` extension, e.g. if the blog post is `2024-12-10-open-saas-tutorial.mdx`, it checks for `./public/banner-images/2024-12-10-open-saas-tutorial.webp`. If it does, it uses that image. If it doesn't, it uses the default banner image.
|
||||
|
||||
Generally, the default banner image is used for docs, and blog posts use a custom banner image.
|
||||
|
||||
### TitleWithBannerImage
|
||||
|
||||
This component uses the same image as the `HeadWithOGImage` component to display a banner image above the title of the blog post.
|
||||
|
||||
You can use the `hideBannerImage` prop to hide the banner image on the page:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Open SaaS Tutorial"
|
||||
date: 2024-12-10
|
||||
hideBannerImage: true
|
||||
---
|
||||
```
|
||||
|
||||
Because the same image in `./public/banner-images` is used for social media preview images and the banner image on the doc/blog page, `hideBannerImage: true` will hide the banner image on the doc/blog page, but still use that image for the social media preview image.
|
||||
|
||||
### VideoPlayer
|
||||
|
||||
This component is a wrapper around the `video` element that adds some default styles.
|
||||
|
||||
You can pass three props to the component:
|
||||
|
||||
- `src` (required): the path to the video file
|
||||
- `lgWidth` (optional): the width of the video player on large screens greater than 425px. If no prop is passed the default is `55%`.
|
||||
- `smWidth` (optional): the width of the video player on small screens less than 425px. If no prop is passed the default is `100%`.
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Open SaaS Tutorial"
|
||||
date: 2024-12-10
|
||||
//...
|
||||
---
|
||||
import VideoPlayer from '../../../components/VideoPlayer.astro';
|
||||
|
||||
<VideoPlayer src="/videos/open-saas-tutorial.mp4" lgWidth="75%" smWidth="80%" />
|
||||
```
|
||||
|
||||
### MyHeader
|
||||
|
||||
This component is a wrapper around the `Header` component from the `@astrojs/starlight` package.
|
||||
|
||||
It repositions the docs and blog links to the left, and adds a logo and a link to the home page, https://opensaas.sh.
|
||||
|
||||
|
||||
## Authoring Content
|
||||
|
||||
The docs and blog are written in Markdown or MDX with some additional metadata:
|
||||
|
||||
```mdx
|
||||
title: We Made the Most Annoying Cookie Banners Ever
|
||||
date: 2024-11-26
|
||||
tags:
|
||||
- cookie consent
|
||||
- saas
|
||||
- sideproject
|
||||
- hackathon
|
||||
subtitle: and it was totally worth it
|
||||
hideBannerImage: true
|
||||
authors: vince
|
||||
```
|
||||
|
||||
Most posts are written in MDX, which allows you to use jsx components in your blog posts. It's recommended to use the MDX extension for your editor, such as this one for [VSCode](https://marketplace.cursorapi.com/items?itemName=unifiedjs.vscode-mdx).
|
||||
|
||||
### Blog Post Metadata
|
||||
`authors` is required and will display the authors of the blog post. To configure a new author, add the proper metadata to `astro.config.mjs` under plugins > starlightBlog > authors:
|
||||
|
||||
```js
|
||||
authors: {
|
||||
vince: {
|
||||
name: 'Vince',
|
||||
title: 'Dev Rel @ Wasp',
|
||||
picture: '/CRAIG_ROCK.png', // Put author images in the `public` directory.
|
||||
url: 'https://wasp.sh',
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
`subtitle` is optional and will display a subtitle below the title of the blog post.
|
||||
|
||||
`hideBannerImage` is optional and will hide the banner image in `./public/banner-images` on the blog post page if you only want it to be displayed as the social media preview image (remember, the same image is used for both the social media preview image and the banner image on the blog post page).
|
||||
|
||||
### Images
|
||||
|
||||
Images to be used in guides and posts are stored in `./src/assets` and are referenced in the blog posts with a relative path.
|
||||
|
||||
Banner images used for social media preview images, as well as cover images for blog posts, are stored in `./public/banner-images` and must always use the `.webp` extension. If a banner image is not found, the default banner image is used. (Note: banner images for docs are used only for social media preview images, where for blog posts the are used as social media preview images and as cover images on the blog post page unless the `hideBannerImage` metadata is set to `true`.)
|
||||
|
||||
See the [HeadWithOGImage](#headwithogimage) and [TitleWithBannerImage](#titlewithbannerimage) sections for more information.
|
||||
|
||||
Always use astro's `Image` component to embed images in your blog posts and docs as Astro will automatically optimize the images for the web.
|
||||
|
||||
```mdx
|
||||
import { Image } from 'astro:assets';
|
||||
import myImage from '../../../assets/my-image.jpg';
|
||||
|
||||
<Image src={myImage} alt="My Image" />
|
||||
```
|
||||
|
||||
## Video
|
||||
|
||||
Videos to be used in blog posts are stored in `./src/assets/` and are referenced in the blog posts with a relative path, just like images.
|
||||
|
||||
Always use the `VideoPlayer` component to embed videos in your blog posts. See the [VideoPlayer component](#videoplayer) section for more information.
|
108
opensaas-sh/blog/astro.config.mjs
Normal file
@ -0,0 +1,108 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
import starlightBlog from 'starlight-blog';
|
||||
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://docs.opensaas.sh',
|
||||
trailingSlash: 'always',
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'OpenSaaS.sh',
|
||||
description: 'Open SaaS is a free, open-source, full-stack SaaS starter kit for React + NodeJS.',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
logo: {
|
||||
src: '/src/assets/logo.webp',
|
||||
alt: 'Open SaaS',
|
||||
},
|
||||
head: [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
defer: true,
|
||||
'data-domain': 'docs.opensaas.sh',
|
||||
'data-api': 'https://opensaas.sh/wasparadocs/wasp/event',
|
||||
src: 'https://opensaas.sh/wasparadocs/wasp/script.js',
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
src: 'https://www.googletagmanager.com/gtag/js?id=G-8QGM76GR3Q',
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: 'script',
|
||||
content: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-8QGM76GR3Q');
|
||||
`,
|
||||
},
|
||||
],
|
||||
editLink: {
|
||||
baseUrl: 'https://github.com/wasp-lang/open-saas/edit/main/opensaas-sh/blog',
|
||||
},
|
||||
components: {
|
||||
SiteTitle: './src/components/MyHeader.astro',
|
||||
ThemeSelect: './src/components/MyThemeSelect.astro',
|
||||
Head: './src/components/HeadWithOGImage.astro',
|
||||
PageTitle: './src/components/TitleWithBannerImage.astro',
|
||||
},
|
||||
social: {
|
||||
github: 'https://github.com/wasp-lang/open-saas',
|
||||
twitter: 'https://twitter.com/wasplang',
|
||||
discord: 'https://discord.gg/aCamt5wCpS',
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Start Here',
|
||||
items: [
|
||||
{ label: 'Introduction', link: '/' },
|
||||
{ label: 'Getting Started', link: '/start/getting-started/' },
|
||||
{ label: 'Guided Tour', link: '/start/guided-tour/' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Guides',
|
||||
autogenerate: { directory: '/guides/' },
|
||||
},
|
||||
{
|
||||
label: 'General',
|
||||
autogenerate: { directory: '/general/' },
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
starlightBlog({
|
||||
title: 'Blog',
|
||||
customCss: ['./src/styles/tailwind.css'],
|
||||
authors: {
|
||||
vince: {
|
||||
name: 'Vince',
|
||||
title: 'Dev Rel @ Wasp',
|
||||
picture: '/CRAIG_ROCK.png', // Images in the `public` directory are supported.
|
||||
url: 'https://wasp.sh',
|
||||
},
|
||||
matija: {
|
||||
name: 'Matija',
|
||||
title: 'CEO @ Wasp',
|
||||
picture: '/matija.jpeg', // Images in the `public` directory are supported.
|
||||
url: 'https://wasp.sh',
|
||||
},
|
||||
milica: {
|
||||
name: 'Milica',
|
||||
title: 'Growth @ Wasp',
|
||||
picture: '/milica.jpg', // Images in the `public` directory are supported.
|
||||
url: 'https://wasp.sh',
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
tailwind({ applyBaseStyles: false }),
|
||||
],
|
||||
});
|
15209
opensaas-sh/blog/package-lock.json
generated
Normal file
23
opensaas-sh/blog/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/starlight": "^0.29.2",
|
||||
"@astrojs/starlight-tailwind": "^2.0.3",
|
||||
"@astrojs/tailwind": "^5.1.2",
|
||||
"astro": "^4.16.15",
|
||||
"prettier": "^3.4.2",
|
||||
"sharp": "^0.32.5",
|
||||
"starlight-blog": "^0.15.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
1
opensaas-sh/blog/postcss.config.cjs
Normal file
@ -0,0 +1 @@
|
||||
module.exports = {};
|
BIN
opensaas-sh/blog/public/CRAIG_ROCK.png
Normal file
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 243 KiB |
After Width: | Height: | Size: 108 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 41 KiB |
14
opensaas-sh/blog/public/banner-images/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# OG Images & Banner Images
|
||||
|
||||
When images are stored in this directory, they are automatically used as Open Graph (social media preview) Images and Cover/Banner Images for each blog post.
|
||||
|
||||
Images stored here must follow the naming convention `<post-slug>.webp` and must always be .webp files, e.g. `2023-11-21-coverlettergpt.webp`.
|
||||
|
||||
This is because OG Image URLs and Banner Images are automatically generated for each blog post based on the logic in the custom Title and Head components, e.g. `src/components/HeadWithOGImage.astro`:
|
||||
|
||||
```tsx
|
||||
const ogImageUrl = new URL(
|
||||
`/banner-images/${Astro.props.id.replace(/blog\//, '').replace(/\.\w+$/, '.webp')}`,
|
||||
Astro.site,
|
||||
)
|
||||
```
|
BIN
opensaas-sh/blog/public/banner-images/opensaas.webp
Normal file
After Width: | Height: | Size: 3.5 KiB |
1
opensaas-sh/blog/public/favicon.svg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
opensaas-sh/blog/public/matija.jpeg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
opensaas-sh/blog/public/milica.jpg
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
opensaas-sh/blog/src/assets/admin/admin-dashboard.png
Normal file
After Width: | Height: | Size: 342 KiB |
After Width: | Height: | Size: 99 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 65 KiB |
BIN
opensaas-sh/blog/src/assets/boilerplate-starters/marc1.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
opensaas-sh/blog/src/assets/boilerplate-starters/marc2.png
Normal file
After Width: | Height: | Size: 249 KiB |
BIN
opensaas-sh/blog/src/assets/boilerplate-starters/opensaas.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
opensaas-sh/blog/src/assets/boilerplate-starters/os-commits.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
opensaas-sh/blog/src/assets/boilerplate-starters/os-gh-stats.png
Normal file
After Width: | Height: | Size: 208 KiB |
After Width: | Height: | Size: 72 KiB |
BIN
opensaas-sh/blog/src/assets/cookie-consent/cookiebanner.png
Normal file
After Width: | Height: | Size: 248 KiB |
BIN
opensaas-sh/blog/src/assets/cookie-consent/enter.gif
Normal file
After Width: | Height: | Size: 2.1 MiB |
BIN
opensaas-sh/blog/src/assets/cookie-consent/image.png
Normal file
After Width: | Height: | Size: 324 KiB |
BIN
opensaas-sh/blog/src/assets/cookie-consent/keyboard.jpg
Normal file
After Width: | Height: | Size: 170 KiB |
BIN
opensaas-sh/blog/src/assets/cookie-consent/preferences.png
Normal file
After Width: | Height: | Size: 1003 KiB |
BIN
opensaas-sh/blog/src/assets/cookie-consent/wheel.gif
Normal file
After Width: | Height: | Size: 6.4 MiB |
After Width: | Height: | Size: 798 KiB |
BIN
opensaas-sh/blog/src/assets/cover-letter-gpt/coverlettergpt.webp
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
opensaas-sh/blog/src/assets/cover-letter-gpt/mrr-graph.webp
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
opensaas-sh/blog/src/assets/file-uploads/cors.png
Normal file
After Width: | Height: | Size: 728 KiB |
BIN
opensaas-sh/blog/src/assets/file-uploads/create-bucket.png
Normal file
After Width: | Height: | Size: 772 KiB |
BIN
opensaas-sh/blog/src/assets/file-uploads/default-settings.png
Normal file
After Width: | Height: | Size: 974 KiB |
BIN
opensaas-sh/blog/src/assets/file-uploads/find-s3.png
Normal file
After Width: | Height: | Size: 196 KiB |
BIN
opensaas-sh/blog/src/assets/file-uploads/keys.png
Normal file
After Width: | Height: | Size: 192 KiB |