├── .github
└── FUNDING.yml
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE.md
├── README.md
├── book
├── .note
├── 1-begin
│ └── .gitignore
├── 1-end
│ ├── .gitignore
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ └── index.tsx
│ │ ├── server
│ │ └── app.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 10-begin
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── Discussion.ts
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ ├── Invitation.ts
│ │ │ │ ├── Post.ts
│ │ │ │ ├── Team.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ ├── sockets.ts
│ │ │ ├── stripe.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ ├── app
│ │ ├── .babelrc
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ │ ├── common
│ │ │ │ ├── Confirmer.tsx
│ │ │ │ ├── LoginButton.tsx
│ │ │ │ ├── MemberChooser.tsx
│ │ │ │ ├── MenuWithLinks.tsx
│ │ │ │ ├── MenuWithMenuItems.tsx
│ │ │ │ └── Notifier.tsx
│ │ │ ├── discussions
│ │ │ │ ├── CreateDiscussionForm.tsx
│ │ │ │ ├── DiscussionActionMenu.tsx
│ │ │ │ ├── DiscussionList.tsx
│ │ │ │ ├── DiscussionListItem.tsx
│ │ │ │ └── EditDiscussionForm.tsx
│ │ │ ├── layout
│ │ │ │ └── index.tsx
│ │ │ ├── posts
│ │ │ │ ├── PostContent.tsx
│ │ │ │ ├── PostDetail.tsx
│ │ │ │ ├── PostEditor.tsx
│ │ │ │ └── PostForm.tsx
│ │ │ └── teams
│ │ │ │ └── InviteMember.tsx
│ │ ├── lib
│ │ │ ├── api
│ │ │ │ ├── makeQueryString.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── confirm.ts
│ │ │ ├── isMobile.ts
│ │ │ ├── notify.ts
│ │ │ ├── resizeImage.ts
│ │ │ ├── sharedStyles.ts
│ │ │ ├── store
│ │ │ │ ├── discussion.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── invitation.ts
│ │ │ │ ├── post.ts
│ │ │ │ ├── team.ts
│ │ │ │ └── user.ts
│ │ │ ├── theme.ts
│ │ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ │ ├── _app.tsx
│ │ │ ├── _document.tsx
│ │ │ ├── billing.tsx
│ │ │ ├── create-team.tsx
│ │ │ ├── discussion.tsx
│ │ │ ├── invitation.tsx
│ │ │ ├── login.tsx
│ │ │ ├── team-settings.tsx
│ │ │ └── your-settings.tsx
│ │ ├── public
│ │ │ └── pepe.jpg
│ │ ├── server
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── lambda
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── handler.ts
│ │ ├── package.json
│ │ ├── serverless.yml
│ │ ├── tsconfig.json
│ │ └── yarn.lock
├── 10-end-functional
│ ├── .gitignore
│ ├── api
│ │ ├── .elasticbeanstalk
│ │ │ └── config.yml
│ │ ├── .env.example
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── logger.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── Discussion.ts
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ ├── Invitation.ts
│ │ │ │ ├── Post.ts
│ │ │ │ ├── Team.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ ├── sockets.ts
│ │ │ ├── stripe.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── static
│ │ │ └── robots.txt
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ └── yarn.lock
│ ├── app
│ │ ├── .babelrc
│ │ ├── .elasticbeanstalk
│ │ │ └── config.yml
│ │ ├── .env.example
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── components
│ │ │ ├── common
│ │ │ │ ├── Confirmer.tsx
│ │ │ │ ├── LoginButton.tsx
│ │ │ │ ├── MemberChooser.tsx
│ │ │ │ ├── MenuWithLinks.tsx
│ │ │ │ ├── MenuWithMenuItems.tsx
│ │ │ │ └── Notifier.tsx
│ │ │ ├── discussions
│ │ │ │ ├── CreateDiscussionForm.tsx
│ │ │ │ ├── DiscussionActionMenu.tsx
│ │ │ │ ├── DiscussionList.tsx
│ │ │ │ ├── DiscussionListItem.tsx
│ │ │ │ └── EditDiscussionForm.tsx
│ │ │ ├── layout
│ │ │ │ └── index.tsx
│ │ │ ├── posts
│ │ │ │ ├── PostContent.tsx
│ │ │ │ ├── PostDetail.tsx
│ │ │ │ ├── PostEditor.tsx
│ │ │ │ └── PostForm.tsx
│ │ │ └── teams
│ │ │ │ └── InviteMember.tsx
│ │ ├── lib
│ │ │ ├── api
│ │ │ │ ├── makeQueryString.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── confirm.ts
│ │ │ ├── gtag.ts
│ │ │ ├── isMobile.ts
│ │ │ ├── notify.ts
│ │ │ ├── resizeImage.ts
│ │ │ ├── sharedStyles.ts
│ │ │ ├── store
│ │ │ │ ├── discussion.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── invitation.ts
│ │ │ │ ├── post.ts
│ │ │ │ ├── team.ts
│ │ │ │ └── user.ts
│ │ │ ├── theme.ts
│ │ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ │ ├── _app.tsx
│ │ │ ├── _document.tsx
│ │ │ ├── billing.tsx
│ │ │ ├── create-team.tsx
│ │ │ ├── discussion.tsx
│ │ │ ├── invitation.tsx
│ │ │ ├── login-cached.tsx
│ │ │ ├── login.tsx
│ │ │ ├── team-settings.tsx
│ │ │ └── your-settings.tsx
│ │ ├── public
│ │ │ ├── fonts
│ │ │ │ ├── IBM-Plex-Mono
│ │ │ │ │ ├── IBMPlexMono-Regular.woff
│ │ │ │ │ └── IBMPlexMono-Regular.woff2
│ │ │ │ ├── Roboto
│ │ │ │ │ ├── Roboto-Regular.woff
│ │ │ │ │ └── Roboto-Regular.woff2
│ │ │ │ ├── cdn.css
│ │ │ │ └── server.css
│ │ │ └── pepe.jpg
│ │ ├── server
│ │ │ ├── robots.txt
│ │ │ ├── routesWithCache.ts
│ │ │ ├── server.ts
│ │ │ └── setupSitemapAndRobots.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── lambda
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── handler.ts
│ │ ├── package.json
│ │ ├── serverless.yml
│ │ ├── symlink
│ │ ├── tsconfig.json
│ │ └── yarn.lock
├── 10-end
│ ├── .gitignore
│ ├── api
│ │ ├── .elasticbeanstalk
│ │ │ └── config.yml
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── logger.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── Discussion.ts
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ ├── Invitation.ts
│ │ │ │ ├── Post.ts
│ │ │ │ ├── Team.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ ├── sockets.ts
│ │ │ ├── stripe.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── static
│ │ │ └── robots.txt
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ └── yarn.lock
│ ├── app
│ │ ├── .babelrc
│ │ ├── .elasticbeanstalk
│ │ │ └── config.yml
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── components
│ │ │ ├── common
│ │ │ │ ├── Confirmer.tsx
│ │ │ │ ├── LoginButton.tsx
│ │ │ │ ├── MemberChooser.tsx
│ │ │ │ ├── MenuWithLinks.tsx
│ │ │ │ ├── MenuWithMenuItems.tsx
│ │ │ │ └── Notifier.tsx
│ │ │ ├── discussions
│ │ │ │ ├── CreateDiscussionForm.tsx
│ │ │ │ ├── DiscussionActionMenu.tsx
│ │ │ │ ├── DiscussionList.tsx
│ │ │ │ ├── DiscussionListItem.tsx
│ │ │ │ └── EditDiscussionForm.tsx
│ │ │ ├── layout
│ │ │ │ └── index.tsx
│ │ │ ├── posts
│ │ │ │ ├── PostContent.tsx
│ │ │ │ ├── PostDetail.tsx
│ │ │ │ ├── PostEditor.tsx
│ │ │ │ └── PostForm.tsx
│ │ │ └── teams
│ │ │ │ └── InviteMember.tsx
│ │ ├── lib
│ │ │ ├── api
│ │ │ │ ├── makeQueryString.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── confirm.ts
│ │ │ ├── gtag.ts
│ │ │ ├── isMobile.ts
│ │ │ ├── notify.ts
│ │ │ ├── resizeImage.ts
│ │ │ ├── sharedStyles.ts
│ │ │ ├── store
│ │ │ │ ├── discussion.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── invitation.ts
│ │ │ │ ├── post.ts
│ │ │ │ ├── team.ts
│ │ │ │ └── user.ts
│ │ │ ├── theme.ts
│ │ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ │ ├── _app.tsx
│ │ │ ├── _document.tsx
│ │ │ ├── billing.tsx
│ │ │ ├── create-team.tsx
│ │ │ ├── discussion-f.tsx
│ │ │ ├── discussion.tsx
│ │ │ ├── invitation.tsx
│ │ │ ├── login-cached.tsx
│ │ │ ├── login.tsx
│ │ │ ├── team-settings.tsx
│ │ │ └── your-settings.tsx
│ │ ├── public
│ │ │ ├── fonts
│ │ │ │ ├── IBM-Plex-Mono
│ │ │ │ │ ├── IBMPlexMono-Regular.woff
│ │ │ │ │ └── IBMPlexMono-Regular.woff2
│ │ │ │ ├── Roboto
│ │ │ │ │ ├── Roboto-Regular.woff
│ │ │ │ │ └── Roboto-Regular.woff2
│ │ │ │ ├── cdn.css
│ │ │ │ └── server.css
│ │ │ └── pepe.jpg
│ │ ├── server
│ │ │ ├── robots.txt
│ │ │ ├── routesWithCache.ts
│ │ │ ├── server.ts
│ │ │ └── setupSitemapAndRobots.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── lambda
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── handler.ts
│ │ ├── package.json
│ │ ├── serverless.yml
│ │ ├── tsconfig.json
│ │ └── yarn.lock
├── 2-begin
│ ├── .gitignore
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ └── index.tsx
│ │ ├── server
│ │ └── app.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 2-end
│ ├── .gitignore
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── sharedStyles.ts
│ │ └── theme.ts
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ └── index.tsx
│ │ ├── server
│ │ └── app.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 3-begin
│ ├── .gitignore
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── sharedStyles.ts
│ │ └── theme.ts
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ └── index.tsx
│ │ ├── server
│ │ └── app.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 3-end
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── public.ts
│ │ │ └── sendRequestAndGetResponse.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── sharedStyles.ts
│ │ └── theme.ts
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ └── index.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 4-begin
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── public.ts
│ │ │ └── sendRequestAndGetResponse.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── sharedStyles.ts
│ │ └── theme.ts
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ └── index.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 4-end
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── models
│ │ │ │ └── User.ts
│ │ │ ├── server.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ └── theme.ts
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ ├── index.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 5-begin
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── models
│ │ │ │ └── User.ts
│ │ │ ├── server.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ └── theme.ts
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ ├── index.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 5-end
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── models
│ │ │ │ └── User.ts
│ │ │ ├── server.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── LoginButton.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ ├── theme.ts
│ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ ├── index.tsx
│ │ ├── login.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 6-begin
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── models
│ │ │ │ └── User.ts
│ │ │ ├── server.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── LoginButton.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ ├── theme.ts
│ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ ├── index.tsx
│ │ ├── login.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 6-end
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── LoginButton.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ ├── theme.ts
│ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ ├── index.tsx
│ │ ├── login.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 7-begin
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── LoginButton.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ └── layout
│ │ │ └── index.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ ├── theme.ts
│ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── csr-page.tsx
│ │ ├── index.tsx
│ │ ├── login.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 7-end
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ ├── Invitation.ts
│ │ │ │ ├── Team.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── Loading.tsx
│ │ │ ├── LoginButton.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ ├── layout
│ │ │ └── index.tsx
│ │ └── teams
│ │ │ └── InviteMember.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── makeQueryString.ts
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ ├── team-leader.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ ├── store
│ │ │ ├── index.ts
│ │ │ ├── invitation.ts
│ │ │ ├── team.ts
│ │ │ └── user.ts
│ │ ├── theme.ts
│ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── create-team.tsx
│ │ ├── invitation.tsx
│ │ ├── login.tsx
│ │ ├── team-settings.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 8-begin
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ ├── Invitation.ts
│ │ │ │ ├── Team.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── Loading.tsx
│ │ │ ├── LoginButton.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ └── Notifier.tsx
│ │ ├── layout
│ │ │ └── index.tsx
│ │ └── teams
│ │ │ └── InviteMember.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── makeQueryString.ts
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ ├── team-leader.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ ├── store
│ │ │ ├── index.ts
│ │ │ ├── invitation.ts
│ │ │ ├── team.ts
│ │ │ └── user.ts
│ │ ├── theme.ts
│ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── create-team.tsx
│ │ ├── invitation.tsx
│ │ ├── login.tsx
│ │ ├── team-settings.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 8-end
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── Discussion.ts
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ ├── Invitation.ts
│ │ │ │ ├── Post.ts
│ │ │ │ ├── Team.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ ├── sockets.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .babelrc
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── LoginButton.tsx
│ │ │ ├── MemberChooser.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ ├── MenuWithMenuItems.tsx
│ │ │ └── Notifier.tsx
│ │ ├── discussions
│ │ │ ├── CreateDiscussionForm.tsx
│ │ │ ├── DiscussionActionMenu.tsx
│ │ │ ├── DiscussionList.tsx
│ │ │ ├── DiscussionListItem.tsx
│ │ │ └── EditDiscussionForm.tsx
│ │ ├── layout
│ │ │ └── index.tsx
│ │ ├── posts
│ │ │ ├── PostContent.tsx
│ │ │ ├── PostDetail.tsx
│ │ │ ├── PostEditor.tsx
│ │ │ └── PostForm.tsx
│ │ └── teams
│ │ │ └── InviteMember.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── makeQueryString.ts
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ ├── team-leader.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ ├── store
│ │ │ ├── discussion.ts
│ │ │ ├── index.ts
│ │ │ ├── invitation.ts
│ │ │ ├── post.ts
│ │ │ ├── team.ts
│ │ │ └── user.ts
│ │ ├── theme.ts
│ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── create-team.tsx
│ │ ├── discussion.tsx
│ │ ├── invitation.tsx
│ │ ├── login.tsx
│ │ ├── team-settings.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 9-begin
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── Discussion.ts
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ ├── Invitation.ts
│ │ │ │ ├── Post.ts
│ │ │ │ ├── Team.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ ├── sockets.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── app
│ │ ├── .babelrc
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ ├── common
│ │ │ ├── Confirmer.tsx
│ │ │ ├── LoginButton.tsx
│ │ │ ├── MemberChooser.tsx
│ │ │ ├── MenuWithLinks.tsx
│ │ │ ├── MenuWithMenuItems.tsx
│ │ │ └── Notifier.tsx
│ │ ├── discussions
│ │ │ ├── CreateDiscussionForm.tsx
│ │ │ ├── DiscussionActionMenu.tsx
│ │ │ ├── DiscussionList.tsx
│ │ │ ├── DiscussionListItem.tsx
│ │ │ └── EditDiscussionForm.tsx
│ │ ├── layout
│ │ │ └── index.tsx
│ │ ├── posts
│ │ │ ├── PostContent.tsx
│ │ │ ├── PostDetail.tsx
│ │ │ ├── PostEditor.tsx
│ │ │ └── PostForm.tsx
│ │ └── teams
│ │ │ └── InviteMember.tsx
│ │ ├── lib
│ │ ├── api
│ │ │ ├── makeQueryString.ts
│ │ │ ├── public.ts
│ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ ├── team-leader.ts
│ │ │ └── team-member.ts
│ │ ├── confirm.ts
│ │ ├── isMobile.ts
│ │ ├── notify.ts
│ │ ├── resizeImage.ts
│ │ ├── sharedStyles.ts
│ │ ├── store
│ │ │ ├── discussion.ts
│ │ │ ├── index.ts
│ │ │ ├── invitation.ts
│ │ │ ├── post.ts
│ │ │ ├── team.ts
│ │ │ └── user.ts
│ │ ├── theme.ts
│ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── create-team.tsx
│ │ ├── discussion.tsx
│ │ ├── invitation.tsx
│ │ ├── login.tsx
│ │ ├── team-settings.tsx
│ │ └── your-settings.tsx
│ │ ├── public
│ │ └── pepe.jpg
│ │ ├── server
│ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
├── 9-end
│ ├── .gitignore
│ ├── api
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── server
│ │ │ ├── api
│ │ │ │ ├── index.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── aws-s3.ts
│ │ │ ├── aws-ses.ts
│ │ │ ├── google-auth.ts
│ │ │ ├── mailchimp.ts
│ │ │ ├── models
│ │ │ │ ├── Discussion.ts
│ │ │ │ ├── EmailTemplate.ts
│ │ │ │ ├── Invitation.ts
│ │ │ │ ├── Post.ts
│ │ │ │ ├── Team.ts
│ │ │ │ └── User.ts
│ │ │ ├── passwordless-auth.ts
│ │ │ ├── passwordless-token-mongostore.ts
│ │ │ ├── server.ts
│ │ │ ├── sockets.ts
│ │ │ ├── stripe.ts
│ │ │ └── utils
│ │ │ │ ├── slugify.ts
│ │ │ │ └── sum.ts
│ │ ├── test
│ │ │ └── server
│ │ │ │ └── utils
│ │ │ │ ├── slugify.test.ts
│ │ │ │ └── sum.test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ ├── app
│ │ ├── .babelrc
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components
│ │ │ ├── common
│ │ │ │ ├── Confirmer.tsx
│ │ │ │ ├── LoginButton.tsx
│ │ │ │ ├── MemberChooser.tsx
│ │ │ │ ├── MenuWithLinks.tsx
│ │ │ │ ├── MenuWithMenuItems.tsx
│ │ │ │ └── Notifier.tsx
│ │ │ ├── discussions
│ │ │ │ ├── CreateDiscussionForm.tsx
│ │ │ │ ├── DiscussionActionMenu.tsx
│ │ │ │ ├── DiscussionList.tsx
│ │ │ │ ├── DiscussionListItem.tsx
│ │ │ │ └── EditDiscussionForm.tsx
│ │ │ ├── layout
│ │ │ │ └── index.tsx
│ │ │ ├── posts
│ │ │ │ ├── PostContent.tsx
│ │ │ │ ├── PostDetail.tsx
│ │ │ │ ├── PostEditor.tsx
│ │ │ │ └── PostForm.tsx
│ │ │ └── teams
│ │ │ │ └── InviteMember.tsx
│ │ ├── lib
│ │ │ ├── api
│ │ │ │ ├── makeQueryString.ts
│ │ │ │ ├── public.ts
│ │ │ │ ├── sendRequestAndGetResponse.ts
│ │ │ │ ├── team-leader.ts
│ │ │ │ └── team-member.ts
│ │ │ ├── confirm.ts
│ │ │ ├── isMobile.ts
│ │ │ ├── notify.ts
│ │ │ ├── resizeImage.ts
│ │ │ ├── sharedStyles.ts
│ │ │ ├── store
│ │ │ │ ├── discussion.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── invitation.ts
│ │ │ │ ├── post.ts
│ │ │ │ ├── team.ts
│ │ │ │ └── user.ts
│ │ │ ├── theme.ts
│ │ │ └── withAuth.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── nodemon.json
│ │ ├── package.json
│ │ ├── pages
│ │ │ ├── _app.tsx
│ │ │ ├── _document.tsx
│ │ │ ├── billing.tsx
│ │ │ ├── create-team.tsx
│ │ │ ├── discussion.tsx
│ │ │ ├── invitation.tsx
│ │ │ ├── login.tsx
│ │ │ ├── team-settings.tsx
│ │ │ └── your-settings.tsx
│ │ ├── public
│ │ │ └── pepe.jpg
│ │ ├── server
│ │ │ └── server.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.server.json
│ │ └── yarn.lock
│ └── lambda
│ │ ├── .eslintignore
│ │ ├── .eslintrc.js
│ │ ├── .gitignore
│ │ ├── handler.ts
│ │ ├── package.json
│ │ ├── serverless.yml
│ │ ├── tsconfig.json
│ │ └── yarn.lock
├── package.json
└── yarn.lock
└── saas
├── .gitignore
├── api
├── .elasticbeanstalk
│ └── config.yml
├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── nodemon.json
├── package.json
├── server
│ ├── api
│ │ ├── index.ts
│ │ ├── public.ts
│ │ ├── team-leader.ts
│ │ └── team-member.ts
│ ├── aws-s3.ts
│ ├── aws-ses.ts
│ ├── google-auth.ts
│ ├── logger.ts
│ ├── mailchimp.ts
│ ├── models
│ │ ├── Discussion.ts
│ │ ├── EmailTemplate.ts
│ │ ├── Invitation.ts
│ │ ├── Post.ts
│ │ ├── Team.ts
│ │ └── User.ts
│ ├── passwordless-auth.ts
│ ├── passwordless-token-mongostore.ts
│ ├── server.ts
│ ├── sockets.ts
│ ├── stripe.ts
│ └── utils
│ │ ├── slugify.ts
│ │ └── sum.ts
├── static
│ └── robots.txt
├── test
│ └── server
│ │ └── utils
│ │ ├── slugify.test.ts
│ │ └── sum.test.ts
├── tsconfig.json
└── yarn.lock
├── app
├── .babelrc
├── .elasticbeanstalk
│ └── config.yml
├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── components
│ ├── common
│ │ ├── Confirmer.tsx
│ │ ├── LoginButton.tsx
│ │ ├── MemberChooser.tsx
│ │ ├── MenuWithLinks.tsx
│ │ ├── MenuWithMenuItems.tsx
│ │ └── Notifier.tsx
│ ├── discussions
│ │ ├── CreateDiscussionForm.tsx
│ │ ├── DiscussionActionMenu.tsx
│ │ ├── DiscussionList.tsx
│ │ ├── DiscussionListItem.tsx
│ │ └── EditDiscussionForm.tsx
│ ├── layout
│ │ └── index.tsx
│ ├── posts
│ │ ├── PostContent.tsx
│ │ ├── PostDetail.tsx
│ │ ├── PostEditor.tsx
│ │ └── PostForm.tsx
│ └── teams
│ │ └── InviteMember.tsx
├── lib
│ ├── api
│ │ ├── makeQueryString.ts
│ │ ├── public.ts
│ │ ├── sendRequestAndGetResponse.ts
│ │ ├── team-leader.ts
│ │ └── team-member.ts
│ ├── confirm.ts
│ ├── gtag.ts
│ ├── isMobile.ts
│ ├── notify.ts
│ ├── resizeImage.ts
│ ├── sharedStyles.ts
│ ├── store
│ │ ├── discussion.ts
│ │ ├── index.ts
│ │ ├── invitation.ts
│ │ ├── post.ts
│ │ ├── team.ts
│ │ └── user.ts
│ ├── theme.ts
│ └── withAuth.tsx
├── next-env.d.ts
├── next.config.js
├── nodemon.json
├── package.json
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── billing.tsx
│ ├── create-team.tsx
│ ├── discussion.tsx
│ ├── invitation.tsx
│ ├── login-cached.tsx
│ ├── login.tsx
│ ├── team-settings.tsx
│ └── your-settings.tsx
├── public
│ ├── fonts
│ │ ├── IBM-Plex-Mono
│ │ │ ├── IBMPlexMono-Regular.woff
│ │ │ └── IBMPlexMono-Regular.woff2
│ │ ├── Roboto
│ │ │ ├── Roboto-Regular.woff
│ │ │ └── Roboto-Regular.woff2
│ │ ├── cdn.css
│ │ └── server.css
│ └── pepe.jpg
├── server
│ ├── robots.txt
│ ├── routesWithCache.ts
│ ├── server.ts
│ └── setupSitemapAndRobots.ts
├── tsconfig.json
├── tsconfig.server.json
└── yarn.lock
└── lambda
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── api
├── handler.ts
├── package.json
├── serverless.yml
├── symlink
├── tsconfig.json
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: ['https://builderbook.org/book']
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
3 | yarn-error.log
4 | production-server/
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint"]
3 | }
4 |
--------------------------------------------------------------------------------
/book/1-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/1-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/1-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/1-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/1-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/1-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/1-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/1-end/app/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import App from 'next/app';
2 | import React from 'react';
3 |
4 | class MyApp extends App {
5 | public render() {
6 | const { Component, pageProps } = this.props;
7 |
8 | return ;
9 | }
10 | }
11 |
12 | export default MyApp;
13 |
--------------------------------------------------------------------------------
/book/1-end/app/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Head, Html, Main, NextScript } from 'next/document';
2 | import React from 'react';
3 |
4 | class MyDocument extends Document {
5 | public render() {
6 | console.log(process.env.NEXT_PUBLIC_URL_APP);
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default MyDocument;
24 |
--------------------------------------------------------------------------------
/book/1-end/app/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Head from 'next/head';
3 |
4 | const Index = () => (
5 |
6 |
7 |
Index page
8 |
9 |
10 |
11 |
Content on Index page
12 |
13 |
14 | );
15 |
16 | export default Index;
17 |
--------------------------------------------------------------------------------
/book/1-end/app/server/app.ts:
--------------------------------------------------------------------------------
1 | const a = 'someString';
2 |
3 | // some comment
4 |
5 | export default a;
6 |
--------------------------------------------------------------------------------
/book/1-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/10-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/10-begin/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-begin/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/10-begin/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-begin/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/book/10-begin/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/10-begin/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/10-begin/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/10-begin/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/10-begin/app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "class-properties": { "loose": true }
7 | }
8 | ]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/book/10-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/10-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/10-begin/app/components/posts/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = { html: string };
4 |
5 | class PostContent extends React.Component {
6 | public render() {
7 | const { html } = this.props;
8 |
9 | return (
10 |
14 | );
15 | }
16 | }
17 |
18 | export default PostContent;
19 |
--------------------------------------------------------------------------------
/book/10-begin/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/book/10-begin/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/10-begin/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-begin/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/10-begin/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/book/10-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/10-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/10-begin/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-begin/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-begin/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/10-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/10-begin/lambda/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-begin/lambda/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | extends: ["plugin:@typescript-eslint/recommended", "prettier"],
4 | env: {
5 | "es6": true,
6 | "node": true,
7 | },
8 | rules: {
9 | 'prettier/prettier': [
10 | 'error',
11 | {
12 | singleQuote: true,
13 | trailingComma: 'all',
14 | arrowParens: 'always',
15 | printWidth: 100,
16 | semi: true,
17 | },
18 | ],
19 | '@typescript-eslint/no-unused-vars': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | 'prefer-arrow-callback': 'error',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | },
24 | plugins: [
25 | "prettier"
26 | ]
27 | }
--------------------------------------------------------------------------------
/book/10-begin/lambda/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .serverless/*
10 | .build/*
11 | .next
12 | .vscode/
13 | node_modules/
14 | .coverage
15 | .env
16 | .env.production
17 | now.json
18 | .note
19 |
20 | compiled/
21 | production-server/
22 |
23 | yarn-error.log
24 |
--------------------------------------------------------------------------------
/book/10-begin/lambda/serverless.yml:
--------------------------------------------------------------------------------
1 | service: saas-boilerplate
2 |
3 | provider:
4 | name: aws
5 | runtime: nodejs16.x
6 | stage: production
7 | region: us-east-1
8 | memorySize: 2048 # optional, in MB, default is 1024
9 | timeout: 30 # optional, in seconds, default is 6
10 |
11 | plugins:
12 | - serverless-plugin-typescript
13 | - serverless-dotenv-plugin
14 |
15 | custom:
16 | dotenv:
17 | include:
18 | - NODE_ENV
19 | - MONGO_URL_TEST
20 | - MONGO_URL
21 | - AWS_ACCESSKEYID
22 | - AWS_SECRETACCESSKEY
23 | - EMAIL_SUPPORT_FROM_ADDRESS
24 | - URL_APP
25 | - PRODUCTION_URL_APP
26 |
27 | functions:
28 | sendEmailForNewPost:
29 | handler: handler.sendEmailForNewPost
30 |
--------------------------------------------------------------------------------
/book/10-begin/lambda/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"],
19 | "module": "commonjs",
20 | "outDir": ".build/",
21 | "rootDir": "./"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/book/10-end-functional/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/10-end-functional/api/.elasticbeanstalk/config.yml:
--------------------------------------------------------------------------------
1 | branch-defaults:
2 | default:
3 | environment: api-saas-boilerplate-env
4 | environment-defaults:
5 | api-saas-boilerplate-env:
6 | branch: null
7 | repository: null
8 | global:
9 | application_name: api-saas-boilerplate-app
10 | default_ec2_keyname: null
11 | default_platform: Node.js 14 running on 64bit Amazon Linux 2
12 | default_region: us-east-1
13 | include_git_submodules: true
14 | instance_profile: null
15 | platform_name: null
16 | platform_version: null
17 | profile: null
18 | sc: null
19 | workspace_type: Application
20 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/server/logger.ts:
--------------------------------------------------------------------------------
1 | import * as winston from 'winston';
2 |
3 | const dev = process.env.NODE_ENV !== 'production';
4 |
5 | const logger = winston.createLogger({
6 | format: winston.format.simple(),
7 | level: dev ? 'debug' : 'info',
8 | transports: [new winston.transports.Console()],
9 | });
10 |
11 | export default logger;
12 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 | Allow: /login
4 | Allow: /signup
5 | Disallow: /*
6 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/10-end-functional/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "target": "es2020",
19 | "lib": ["es2020"],
20 | "module": "commonjs",
21 | "outDir": "production-server/",
22 | "downlevelIteration": true,
23 | },
24 | "include": ["./server/**/*.ts"]
25 | }
26 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "class-properties": { "loose": true }
7 | }
8 | ]
9 | ],
10 | "plugins": [["@babel/plugin-proposal-private-property-in-object", { "loose": true }], ["@babel/plugin-proposal-private-methods", { "loose": true }]]
11 | }
12 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/.elasticbeanstalk/config.yml:
--------------------------------------------------------------------------------
1 | branch-defaults:
2 | default:
3 | environment: app-saas-boilerplate-env
4 | environment-defaults:
5 | app-saas-boilerplate-env:
6 | branch: null
7 | repository: null
8 | global:
9 | application_name: app-saas-boilerplate-app
10 | default_ec2_keyname: null
11 | default_platform: Node.js 14 running on 64bit Amazon Linux 2
12 | default_region: us-east-1
13 | include_git_submodules: true
14 | instance_profile: null
15 | platform_name: null
16 | platform_version: null
17 | profile: null
18 | sc: null
19 | workspace_type: Application
20 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLEKEY="pk_test_xxxxxxxxxxxxxxx"
2 | NEXT_PUBLIC_STRIPE_LIVE_PUBLISHABLEKEY="pk_live_xxxxxxxxxxxxxxx"
3 |
4 | NEXT_PUBLIC_BUCKET_FOR_POSTS=
5 | NEXT_PUBLIC_BUCKET_FOR_TEAM_AVATARS=
6 | NEXT_PUBLIC_BUCKET_FOR_TEAM_LOGOS=
7 |
8 | NEXT_PUBLIC_URL_APP="http://localhost:3000"
9 | NEXT_PUBLIC_URL_API="http://localhost:8000"
10 | NEXT_PUBLIC_PRODUCTION_URL_APP=
11 | NEXT_PUBLIC_PRODUCTION_URL_API=
12 |
13 |
14 | NEXT_PUBLIC_API_GATEWAY_ENDPOINT=
15 | NEXT_PUBLIC_GA_MEASUREMENT_ID=
16 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/components/posts/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = { html: string };
4 |
5 | class PostContent extends React.Component {
6 | public render() {
7 | const { html } = this.props;
8 |
9 | return (
10 |
19 | );
20 | }
21 | }
22 |
23 | export default PostContent;
24 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/lib/gtag.ts:
--------------------------------------------------------------------------------
1 | const { NEXT_PUBLIC_GA_MEASUREMENT_ID } = process.env;
2 |
3 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages
4 | export const pageview = (url) => {
5 | (window as any).gtag('config', NEXT_PUBLIC_GA_MEASUREMENT_ID, {
6 | page_location: url,
7 | });
8 | };
9 |
10 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events
11 | export const event = ({ action, category, label }) => {
12 | (window as any).gtag('event', action, {
13 | event_category: category,
14 | event_label: label,
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | };
10 |
11 | const styleToolbar = {
12 | background: '#FFF',
13 | height: '64px',
14 | paddingRight: '20px',
15 | };
16 |
17 | const styleTextField = {
18 | color: '#222',
19 | fontWeight: '300',
20 | };
21 |
22 | const styleForm = {
23 | margin: '7% auto',
24 | width: '360px',
25 | };
26 |
27 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
28 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-end-functional/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end-functional/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff
--------------------------------------------------------------------------------
/book/10-end-functional/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end-functional/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff2
--------------------------------------------------------------------------------
/book/10-end-functional/app/public/fonts/Roboto/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end-functional/app/public/fonts/Roboto/Roboto-Regular.woff
--------------------------------------------------------------------------------
/book/10-end-functional/app/public/fonts/Roboto/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end-functional/app/public/fonts/Roboto/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/book/10-end-functional/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end-functional/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/10-end-functional/app/server/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 | Allow: /login
4 | Allow: /signup
5 | Disallow: /*
--------------------------------------------------------------------------------
/book/10-end-functional/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/10-end-functional/lambda/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-end-functional/lambda/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .serverless/*
10 | .build/*
11 | .next
12 | .vscode/
13 | node_modules/
14 | .coverage
15 | .env
16 | .env.production
17 | now.json
18 | .note
19 |
20 | compiled/
21 | production-server/
22 |
23 | yarn-error.log
24 |
--------------------------------------------------------------------------------
/book/10-end-functional/lambda/serverless.yml:
--------------------------------------------------------------------------------
1 | service: saas-boilerplate
2 |
3 | useDotenv: true
4 | provider:
5 | name: aws
6 | runtime: nodejs16.x
7 | stage: production
8 | region: us-east-1
9 | memorySize: 2048 # optional, in MB, default is 1024
10 | timeout: 30 # optional, in seconds, default is 6
11 | # profile: saas
12 |
13 |
14 | plugins:
15 | - serverless-plugin-typescript
16 | - serverless-dotenv-plugin
17 |
18 | custom:
19 | dotenv:
20 | include:
21 | - NODE_ENV
22 | - MONGO_URL_TEST
23 | - MONGO_URL
24 | - AWS_ACCESSKEYID
25 | - AWS_SECRETACCESSKEY
26 | - EMAIL_SUPPORT_FROM_ADDRESS
27 | - URL_APP
28 | - PRODUCTION_URL_APP
29 |
30 | functions:
31 | sendEmailForNewPost:
32 | handler: handler.sendEmailForNewPost
33 |
--------------------------------------------------------------------------------
/book/10-end-functional/lambda/symlink:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "Creating symlinks for api:" $1
4 |
5 | echo ln -s $1/$var $var
6 | ln -s $1/$var $var
--------------------------------------------------------------------------------
/book/10-end-functional/lambda/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"],
19 | "module": "commonjs",
20 | "outDir": ".build/",
21 | "rootDir": "./"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/book/10-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/10-end/api/.elasticbeanstalk/config.yml:
--------------------------------------------------------------------------------
1 | branch-defaults:
2 | default:
3 | environment: api-saas-boilerplate-env
4 | environment-defaults:
5 | api-saas-boilerplate-env:
6 | branch: null
7 | repository: null
8 | global:
9 | application_name: api-saas-boilerplate-app
10 | default_ec2_keyname: null
11 | default_platform: Node.js 18 running on 64bit Amazon Linux 2
12 | default_region: us-east-1
13 | include_git_submodules: true
14 | instance_profile: null
15 | platform_name: null
16 | platform_version: null
17 | profile: null
18 | sc: null
19 | workspace_type: Application
20 |
--------------------------------------------------------------------------------
/book/10-end/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-end/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/10-end/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-end/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/book/10-end/api/server/logger.ts:
--------------------------------------------------------------------------------
1 | import * as winston from 'winston';
2 |
3 | const dev = process.env.NODE_ENV !== 'production';
4 |
5 | const logger = winston.createLogger({
6 | format: winston.format.simple(),
7 | level: dev ? 'debug' : 'info',
8 | transports: [new winston.transports.Console()],
9 | });
10 |
11 | export default logger;
12 |
--------------------------------------------------------------------------------
/book/10-end/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/10-end/api/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 | Allow: /login
4 | Allow: /signup
5 | Disallow: /*
6 |
--------------------------------------------------------------------------------
/book/10-end/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/10-end/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "target": "es2020",
19 | "lib": ["es2020"],
20 | "module": "commonjs",
21 | "outDir": "production-server/",
22 | "downlevelIteration": true,
23 | },
24 | "include": ["./server/**/*.ts"]
25 | }
26 |
--------------------------------------------------------------------------------
/book/10-end/app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "class-properties": { "loose": true }
7 | }
8 | ]
9 | ],
10 | "plugins": [["@babel/plugin-proposal-private-property-in-object", { "loose": true }], ["@babel/plugin-proposal-private-methods", { "loose": true }]]
11 | }
12 |
--------------------------------------------------------------------------------
/book/10-end/app/.elasticbeanstalk/config.yml:
--------------------------------------------------------------------------------
1 | branch-defaults:
2 | default:
3 | environment: app-saas-boilerplate-env
4 | environment-defaults:
5 | app-saas-boilerplate-env:
6 | branch: null
7 | repository: null
8 | global:
9 | application_name: app-saas-boilerplate-app
10 | default_ec2_keyname: null
11 | default_platform: Node.js 18 running on 64bit Amazon Linux 2
12 | default_region: us-east-1
13 | include_git_submodules: true
14 | instance_profile: null
15 | platform_name: null
16 | platform_version: null
17 | profile: null
18 | sc: null
19 | workspace_type: Application
20 |
--------------------------------------------------------------------------------
/book/10-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/10-end/app/components/posts/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = { html: string };
4 |
5 | class PostContent extends React.Component {
6 | public render() {
7 | const { html } = this.props;
8 |
9 | return (
10 |
19 | );
20 | }
21 | }
22 |
23 | export default PostContent;
24 |
--------------------------------------------------------------------------------
/book/10-end/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/book/10-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/10-end/app/lib/gtag.ts:
--------------------------------------------------------------------------------
1 | const { NEXT_PUBLIC_GA_MEASUREMENT_ID } = process.env;
2 |
3 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages
4 | export const pageview = (url) => {
5 | (window as any).gtag('config', NEXT_PUBLIC_GA_MEASUREMENT_ID, {
6 | page_location: url,
7 | });
8 | };
9 |
10 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events
11 | export const event = ({ action, category, label }) => {
12 | (window as any).gtag('event', action, {
13 | event_category: category,
14 | event_label: label,
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/book/10-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | };
10 |
11 | const styleToolbar = {
12 | background: '#FFF',
13 | height: '64px',
14 | paddingRight: '20px',
15 | };
16 |
17 | const styleTextField = {
18 | color: '#222',
19 | fontWeight: '300',
20 | };
21 |
22 | const styleForm = {
23 | margin: '7% auto',
24 | width: '360px',
25 | };
26 |
27 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
28 |
--------------------------------------------------------------------------------
/book/10-end/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/book/10-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/10-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/10-end/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/10-end/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff
--------------------------------------------------------------------------------
/book/10-end/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff2
--------------------------------------------------------------------------------
/book/10-end/app/public/fonts/Roboto/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end/app/public/fonts/Roboto/Roboto-Regular.woff
--------------------------------------------------------------------------------
/book/10-end/app/public/fonts/Roboto/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end/app/public/fonts/Roboto/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/book/10-end/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/10-end/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/10-end/app/server/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 | Allow: /login
4 | Allow: /signup
5 | Disallow: /*
--------------------------------------------------------------------------------
/book/10-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/10-end/lambda/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/10-end/lambda/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .serverless/*
10 | .build/*
11 | .next
12 | .vscode/
13 | node_modules/
14 | .coverage
15 | .env
16 | .env.production
17 | now.json
18 | .note
19 |
20 | compiled/
21 | production-server/
22 |
23 | yarn-error.log
24 |
--------------------------------------------------------------------------------
/book/10-end/lambda/serverless.yml:
--------------------------------------------------------------------------------
1 | service: saas-boilerplate
2 |
3 | useDotenv: true
4 | provider:
5 | name: aws
6 | runtime: nodejs16.x
7 | stage: production
8 | region: us-east-1
9 | memorySize: 2048 # optional, in MB, default is 1024
10 | timeout: 30 # optional, in seconds, default is 6
11 | # profile: saas
12 |
13 |
14 | plugins:
15 | - serverless-plugin-typescript
16 | - serverless-dotenv-plugin
17 |
18 | custom:
19 | dotenv:
20 | include:
21 | - NODE_ENV
22 | - MONGO_URL_TEST
23 | - MONGO_URL
24 | - AWS_ACCESSKEYID
25 | - AWS_SECRETACCESSKEY
26 | - EMAIL_SUPPORT_FROM_ADDRESS
27 | - URL_APP
28 | - PRODUCTION_URL_APP
29 |
30 | functions:
31 | sendEmailForNewPost:
32 | handler: handler.sendEmailForNewPost
33 |
--------------------------------------------------------------------------------
/book/10-end/lambda/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"],
19 | "module": "commonjs",
20 | "outDir": ".build/",
21 | "rootDir": "./"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/book/2-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/2-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/2-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/2-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/2-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/2-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/2-begin/app/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import App from 'next/app';
2 | import React from 'react';
3 |
4 | class MyApp extends App {
5 | public render() {
6 | const { Component, pageProps } = this.props;
7 |
8 | return ;
9 | }
10 | }
11 |
12 | export default MyApp;
13 |
--------------------------------------------------------------------------------
/book/2-begin/app/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Head, Html, Main, NextScript } from 'next/document';
2 | import React from 'react';
3 |
4 | class MyDocument extends Document {
5 | public render() {
6 | console.log(process.env.NEXT_PUBLIC_URL_APP);
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default MyDocument;
24 |
--------------------------------------------------------------------------------
/book/2-begin/app/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Head from 'next/head';
3 |
4 | const Index = () => (
5 |
6 |
7 |
Index page
8 |
9 |
10 |
11 |
Content on Index page
12 |
13 |
14 | );
15 |
16 | export default Index;
17 |
--------------------------------------------------------------------------------
/book/2-begin/app/server/app.ts:
--------------------------------------------------------------------------------
1 | const a = 'someString';
2 |
3 | // some comment
4 |
5 | export default a;
6 |
--------------------------------------------------------------------------------
/book/2-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/2-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/2-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/2-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/2-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/2-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/2-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/2-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/2-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/2-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | typescript: {
4 | ignoreBuildErrors: true,
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/book/2-end/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/2-end/app/server/app.ts:
--------------------------------------------------------------------------------
1 | const a = 'someString';
2 |
3 | // some comment
4 |
5 | export default a;
6 |
--------------------------------------------------------------------------------
/book/2-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/3-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/3-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/3-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/3-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/3-begin/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/3-begin/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/3-begin/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/3-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/3-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | typescript: {
4 | ignoreBuildErrors: true,
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/book/3-begin/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/3-begin/app/server/app.ts:
--------------------------------------------------------------------------------
1 | const a = 'someString';
2 |
3 | // some comment
4 |
5 | export default a;
6 |
--------------------------------------------------------------------------------
/book/3-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/3-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/3-end/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/3-end/api/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | extends: ["plugin:@typescript-eslint/recommended", "prettier"],
4 | env: {
5 | "es6": true,
6 | "node": true,
7 | },
8 | plugins: ["prettier"],
9 | rules: {
10 | 'prettier/prettier': [
11 | 'error',
12 | {
13 | singleQuote: true,
14 | trailingComma: 'all',
15 | arrowParens: 'always',
16 | printWidth: 100,
17 | semi: true,
18 | },
19 | ],
20 | '@typescript-eslint/no-unused-vars': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | 'prefer-arrow-callback': 'error',
24 | '@typescript-eslint/explicit-module-boundary-types': 'off',
25 | },
26 | }
--------------------------------------------------------------------------------
/book/3-end/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/3-end/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/3-end/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "3-end-api",
3 | "version": "1",
4 | "license": "MIT",
5 | "scripts": {
6 | "dev": "nodemon server/server.ts",
7 | "lint": "eslint . --ext .ts,.tsx"
8 | },
9 | "dependencies": {
10 | "dotenv": "^16.4.7",
11 | "express": "^4.21.2",
12 | "typescript": "^5.8.2"
13 | },
14 | "devDependencies": {
15 | "@types/dotenv": "^8.2.3",
16 | "@types/express": "^5.0.1",
17 | "@types/node": "^22.13.10",
18 | "@typescript-eslint/eslint-plugin": "^8.27.0",
19 | "@typescript-eslint/parser": "^8.27.0",
20 | "eslint": "^9.22.0",
21 | "eslint-config-prettier": "^10.1.1",
22 | "eslint-plugin-prettier": "^5.2.3",
23 | "nodemon": "^3.1.9",
24 | "prettier": "^3.5.3",
25 | "ts-node": "^10.9.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/book/3-end/api/server/server.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | // eslint-disable-next-line
4 | require('dotenv').config();
5 |
6 | const server = express();
7 |
8 | server.use(express.json());
9 |
10 | server.get('/api/v1/public/get-user', (_, res) => {
11 | console.log('API server got request from APP server or browser');
12 | res.json({ user: { email: 'team@builderbook.org' } });
13 | });
14 |
15 | server.get('*', (_, res) => {
16 | res.sendStatus(403);
17 | });
18 |
19 | console.log(process.env.PORT_API, process.env.URL_API);
20 |
21 | server.listen(process.env.PORT_API, () => {
22 | console.log(`> Ready on ${process.env.URL_API}`);
23 | });
24 |
--------------------------------------------------------------------------------
/book/3-end/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/3-end/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/3-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/3-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/3-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/3-end/app/lib/api/public.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/public';
4 |
5 | export const getUserApiMethod = (request) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/get-user`, {
7 | request,
8 | method: 'GET',
9 | });
10 |
--------------------------------------------------------------------------------
/book/3-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/3-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/3-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/3-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/3-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/3-end/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/3-end/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/3-end/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/3-end/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/3-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/4-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/4-begin/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/4-begin/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/4-begin/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/4-begin/api/server/server.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | // eslint-disable-next-line
4 | require('dotenv').config();
5 |
6 | const server = express();
7 |
8 | server.use(express.json());
9 |
10 | server.get('/api/v1/public/get-user', (_, res) => {
11 | console.log('API server got request from APP server or browser');
12 | res.json({ user: { email: 'team@builderbook.org' } });
13 | });
14 |
15 | server.get('*', (_, res) => {
16 | res.sendStatus(403);
17 | });
18 |
19 | console.log(process.env.PORT_API, process.env.URL_API);
20 |
21 | server.listen(process.env.PORT_API, () => {
22 | console.log(`> Ready on ${process.env.URL_API}`);
23 | });
24 |
--------------------------------------------------------------------------------
/book/4-begin/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/4-begin/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/4-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/4-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/4-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/4-begin/app/lib/api/public.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/public';
4 |
5 | export const getUserApiMethod = (request) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/get-user`, {
7 | request,
8 | method: 'GET',
9 | });
10 |
--------------------------------------------------------------------------------
/book/4-begin/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/4-begin/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/4-begin/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/4-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/4-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/4-begin/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/4-begin/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/4-begin/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/4-begin/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/4-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/4-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/4-end/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/4-end/api/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | extends: ["plugin:@typescript-eslint/recommended", "prettier"],
4 | env: {
5 | "es6": true,
6 | "node": true,
7 | },
8 | plugins: ["prettier"],
9 | rules: {
10 | 'prettier/prettier': [
11 | 'error',
12 | {
13 | singleQuote: true,
14 | trailingComma: 'all',
15 | arrowParens: 'always',
16 | printWidth: 100,
17 | semi: true,
18 | },
19 | ],
20 | '@typescript-eslint/no-unused-vars': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | 'prefer-arrow-callback': 'error',
24 | '@typescript-eslint/explicit-module-boundary-types': 'off',
25 | },
26 | }
--------------------------------------------------------------------------------
/book/4-end/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/4-end/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/4-end/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 |
6 | function handleError(err, _, res, __) {
7 | console.error(err.stack);
8 |
9 | res.json({ error: err.message || err.toString() });
10 | }
11 |
12 | export default function api(server: express.Express) {
13 | server.use('/api/v1/public', publicExpressRoutes, handleError);
14 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
15 | }
16 |
--------------------------------------------------------------------------------
/book/4-end/api/server/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { signRequestForUpload } from '../aws-s3';
4 |
5 | const router = express.Router();
6 |
7 | // Get signed request from AWS S3 server
8 | router.post('/aws/get-signed-request-for-upload-to-s3', async (req, res, next) => {
9 | try {
10 | const { fileName, fileType, prefix, bucket } = req.body;
11 |
12 | const returnData = await signRequestForUpload({
13 | fileName,
14 | fileType,
15 | prefix,
16 | bucket,
17 | });
18 |
19 | console.log(returnData);
20 |
21 | res.json(returnData);
22 | } catch (err) {
23 | next(err);
24 | }
25 | });
26 |
27 | export default router;
28 |
--------------------------------------------------------------------------------
/book/4-end/api/server/server.ts:
--------------------------------------------------------------------------------
1 | import * as cors from 'cors';
2 | import * as express from 'express';
3 | import * as mongoose from 'mongoose';
4 |
5 | import api from './api';
6 |
7 | // eslint-disable-next-line
8 | require('dotenv').config();
9 |
10 | mongoose.connect(process.env.MONGO_URL_TEST);
11 |
12 | const server = express();
13 |
14 | server.use(
15 | cors({
16 | origin: process.env.URL_APP,
17 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
18 | credentials: true,
19 | }),
20 | );
21 |
22 | server.use(express.json());
23 |
24 | api(server);
25 |
26 | server.get('*', (_, res) => {
27 | res.sendStatus(403);
28 | });
29 |
30 | server.listen(process.env.PORT_API, () => {
31 | console.log(`> Ready on ${process.env.URL_API}`);
32 | });
33 |
--------------------------------------------------------------------------------
/book/4-end/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/4-end/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/4-end/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/4-end/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/4-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/4-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/4-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/4-end/app/lib/api/public.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/public';
4 |
5 | export const getUserApiMethod = (request) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/get-user`, {
7 | request,
8 | method: 'GET',
9 | });
10 |
11 | export const getUserBySlugApiMethod = (slug) =>
12 | sendRequestAndGetResponse(`${BASE_PATH}/get-user-by-slug`, {
13 | body: JSON.stringify({ slug }),
14 | });
15 |
16 | export const updateProfileApiMethod = (data) =>
17 | sendRequestAndGetResponse(`${BASE_PATH}/user/update-profile`, {
18 | body: JSON.stringify(data),
19 | });
20 |
--------------------------------------------------------------------------------
/book/4-end/app/lib/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/team-member';
4 |
5 | export const getSignedRequestForUploadApiMethod = ({ fileName, fileType, prefix, bucket }) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/aws/get-signed-request-for-upload-to-s3`, {
7 | body: JSON.stringify({ fileName, fileType, prefix, bucket }),
8 | });
9 |
10 | export const uploadFileUsingSignedPutRequestApiMethod = (file, signedRequest, headers = {}) =>
11 | sendRequestAndGetResponse(signedRequest, {
12 | externalServer: true,
13 | method: 'PUT',
14 | body: file,
15 | headers,
16 | });
17 |
--------------------------------------------------------------------------------
/book/4-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/4-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/4-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/4-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/4-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/4-end/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/4-end/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/4-end/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/4-end/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/4-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/5-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/5-begin/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/5-begin/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/5-begin/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/5-begin/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 |
6 | function handleError(err, _, res, __) {
7 | console.error(err.stack);
8 |
9 | res.json({ error: err.message || err.toString() });
10 | }
11 |
12 | export default function api(server: express.Express) {
13 | server.use('/api/v1/public', publicExpressRoutes, handleError);
14 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
15 | }
16 |
--------------------------------------------------------------------------------
/book/5-begin/api/server/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { signRequestForUpload } from '../aws-s3';
4 |
5 | const router = express.Router();
6 |
7 | // Get signed request from AWS S3 server
8 | router.post('/aws/get-signed-request-for-upload-to-s3', async (req, res, next) => {
9 | try {
10 | const { fileName, fileType, prefix, bucket } = req.body;
11 |
12 | const returnData = await signRequestForUpload({
13 | fileName,
14 | fileType,
15 | prefix,
16 | bucket,
17 | });
18 |
19 | console.log(returnData);
20 |
21 | res.json(returnData);
22 | } catch (err) {
23 | next(err);
24 | }
25 | });
26 |
27 | export default router;
28 |
--------------------------------------------------------------------------------
/book/5-begin/api/server/server.ts:
--------------------------------------------------------------------------------
1 | import * as cors from 'cors';
2 | import * as express from 'express';
3 | import * as mongoose from 'mongoose';
4 |
5 | import api from './api';
6 |
7 | // eslint-disable-next-line
8 | require('dotenv').config();
9 |
10 | mongoose.connect(process.env.MONGO_URL_TEST);
11 |
12 | const server = express();
13 |
14 | server.use(
15 | cors({
16 | origin: process.env.URL_APP,
17 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
18 | credentials: true,
19 | }),
20 | );
21 |
22 | server.use(express.json());
23 |
24 | api(server);
25 |
26 | server.get('*', (_, res) => {
27 | res.sendStatus(403);
28 | });
29 |
30 | server.listen(process.env.PORT_API, () => {
31 | console.log(`> Ready on ${process.env.URL_API}`);
32 | });
33 |
--------------------------------------------------------------------------------
/book/5-begin/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/5-begin/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/5-begin/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/5-begin/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/5-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/5-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/5-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/5-begin/app/lib/api/public.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/public';
4 |
5 | export const getUserApiMethod = (request) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/get-user`, {
7 | request,
8 | method: 'GET',
9 | });
10 |
11 | export const getUserBySlugApiMethod = (slug) =>
12 | sendRequestAndGetResponse(`${BASE_PATH}/get-user-by-slug`, {
13 | body: JSON.stringify({ slug }),
14 | });
15 |
16 | export const updateProfileApiMethod = (data) =>
17 | sendRequestAndGetResponse(`${BASE_PATH}/user/update-profile`, {
18 | body: JSON.stringify(data),
19 | });
20 |
--------------------------------------------------------------------------------
/book/5-begin/app/lib/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/team-member';
4 |
5 | export const getSignedRequestForUploadApiMethod = ({ fileName, fileType, prefix, bucket }) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/aws/get-signed-request-for-upload-to-s3`, {
7 | body: JSON.stringify({ fileName, fileType, prefix, bucket }),
8 | });
9 |
10 | export const uploadFileUsingSignedPutRequestApiMethod = (file, signedRequest, headers = {}) =>
11 | sendRequestAndGetResponse(signedRequest, {
12 | externalServer: true,
13 | method: 'PUT',
14 | body: file,
15 | headers,
16 | });
17 |
--------------------------------------------------------------------------------
/book/5-begin/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/5-begin/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/5-begin/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/5-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/5-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/5-begin/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/5-begin/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/5-begin/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/5-begin/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/5-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/5-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/5-end/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/5-end/api/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | extends: ["plugin:@typescript-eslint/recommended", "prettier"],
4 | env: {
5 | "es6": true,
6 | "node": true,
7 | },
8 | plugins: ["prettier"],
9 | rules: {
10 | 'prettier/prettier': [
11 | 'error',
12 | {
13 | singleQuote: true,
14 | trailingComma: 'all',
15 | arrowParens: 'always',
16 | printWidth: 100,
17 | semi: true,
18 | },
19 | ],
20 | '@typescript-eslint/no-unused-vars': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | 'prefer-arrow-callback': 'error',
24 | '@typescript-eslint/explicit-module-boundary-types': 'off',
25 | },
26 | }
--------------------------------------------------------------------------------
/book/5-end/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/5-end/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/5-end/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 |
6 | function handleError(err, _, res, __) {
7 | console.error(err.stack);
8 |
9 | res.json({ error: err.message || err.toString() });
10 | }
11 |
12 | export default function api(server: express.Express) {
13 | server.use('/api/v1/public', publicExpressRoutes, handleError);
14 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
15 | }
16 |
--------------------------------------------------------------------------------
/book/5-end/api/server/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { signRequestForUpload } from '../aws-s3';
4 |
5 | const router = express.Router();
6 |
7 | // Get signed request from AWS S3 server
8 | router.post('/aws/get-signed-request-for-upload-to-s3', async (req, res, next) => {
9 | try {
10 | const { fileName, fileType, prefix, bucket } = req.body;
11 |
12 | const returnData = await signRequestForUpload({
13 | fileName,
14 | fileType,
15 | prefix,
16 | bucket,
17 | });
18 |
19 | console.log(returnData);
20 |
21 | res.json(returnData);
22 | } catch (err) {
23 | next(err);
24 | }
25 | });
26 |
27 | export default router;
28 |
--------------------------------------------------------------------------------
/book/5-end/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/5-end/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/5-end/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/5-end/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/5-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/5-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/5-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/5-end/app/components/common/LoginButton.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 |
4 | class LoginButton extends React.PureComponent {
5 | public render() {
6 | const url = `${process.env.NEXT_PUBLIC_URL_API}/auth/google`;
7 |
8 | console.log(url);
9 |
10 | return (
11 |
12 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default LoginButton;
24 |
--------------------------------------------------------------------------------
/book/5-end/app/lib/api/public.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/public';
4 |
5 | export const getUserApiMethod = (request) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/get-user`, {
7 | request,
8 | method: 'GET',
9 | });
10 |
11 | export const getUserBySlugApiMethod = (slug) =>
12 | sendRequestAndGetResponse(`${BASE_PATH}/get-user-by-slug`, {
13 | body: JSON.stringify({ slug }),
14 | });
15 |
16 | export const updateProfileApiMethod = (data) =>
17 | sendRequestAndGetResponse(`${BASE_PATH}/user/update-profile`, {
18 | body: JSON.stringify(data),
19 | });
20 |
--------------------------------------------------------------------------------
/book/5-end/app/lib/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/team-member';
4 |
5 | export const getSignedRequestForUploadApiMethod = ({ fileName, fileType, prefix, bucket }) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/aws/get-signed-request-for-upload-to-s3`, {
7 | body: JSON.stringify({ fileName, fileType, prefix, bucket }),
8 | });
9 |
10 | export const uploadFileUsingSignedPutRequestApiMethod = (file, signedRequest, headers = {}) =>
11 | sendRequestAndGetResponse(signedRequest, {
12 | externalServer: true,
13 | method: 'PUT',
14 | body: file,
15 | headers,
16 | });
17 |
--------------------------------------------------------------------------------
/book/5-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/5-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/5-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/5-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/5-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/5-end/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/5-end/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/5-end/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/5-end/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/5-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/6-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/6-begin/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/6-begin/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/6-begin/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/6-begin/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 |
6 | function handleError(err, _, res, __) {
7 | console.error(err.stack);
8 |
9 | res.json({ error: err.message || err.toString() });
10 | }
11 |
12 | export default function api(server: express.Express) {
13 | server.use('/api/v1/public', publicExpressRoutes, handleError);
14 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
15 | }
16 |
--------------------------------------------------------------------------------
/book/6-begin/api/server/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import { signRequestForUpload } from '../aws-s3';
4 |
5 | const router = express.Router();
6 |
7 | // Get signed request from AWS S3 server
8 | router.post('/aws/get-signed-request-for-upload-to-s3', async (req, res, next) => {
9 | try {
10 | const { fileName, fileType, prefix, bucket } = req.body;
11 |
12 | const returnData = await signRequestForUpload({
13 | fileName,
14 | fileType,
15 | prefix,
16 | bucket,
17 | });
18 |
19 | console.log(returnData);
20 |
21 | res.json(returnData);
22 | } catch (err) {
23 | next(err);
24 | }
25 | });
26 |
27 | export default router;
28 |
--------------------------------------------------------------------------------
/book/6-begin/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/6-begin/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/6-begin/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/6-begin/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/6-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/6-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/6-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/6-begin/app/components/common/LoginButton.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 |
4 | class LoginButton extends React.PureComponent {
5 | public render() {
6 | const url = `${process.env.NEXT_PUBLIC_URL_API}/auth/google`;
7 |
8 | console.log(url);
9 |
10 | return (
11 |
12 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default LoginButton;
24 |
--------------------------------------------------------------------------------
/book/6-begin/app/lib/api/public.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/public';
4 |
5 | export const getUserApiMethod = (request) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/get-user`, {
7 | request,
8 | method: 'GET',
9 | });
10 |
11 | export const getUserBySlugApiMethod = (slug) =>
12 | sendRequestAndGetResponse(`${BASE_PATH}/get-user-by-slug`, {
13 | body: JSON.stringify({ slug }),
14 | });
15 |
16 | export const updateProfileApiMethod = (data) =>
17 | sendRequestAndGetResponse(`${BASE_PATH}/user/update-profile`, {
18 | body: JSON.stringify(data),
19 | });
20 |
--------------------------------------------------------------------------------
/book/6-begin/app/lib/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/team-member';
4 |
5 | export const getSignedRequestForUploadApiMethod = ({ fileName, fileType, prefix, bucket }) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/aws/get-signed-request-for-upload-to-s3`, {
7 | body: JSON.stringify({ fileName, fileType, prefix, bucket }),
8 | });
9 |
10 | export const uploadFileUsingSignedPutRequestApiMethod = (file, signedRequest, headers = {}) =>
11 | sendRequestAndGetResponse(signedRequest, {
12 | externalServer: true,
13 | method: 'PUT',
14 | body: file,
15 | headers,
16 | });
17 |
--------------------------------------------------------------------------------
/book/6-begin/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/6-begin/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/6-begin/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/6-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/6-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/6-begin/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/6-begin/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/6-begin/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/6-begin/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/6-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/6-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/6-end/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/6-end/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/6-end/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/6-end/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 |
6 | function handleError(err, _, res, __) {
7 | console.error(err.stack);
8 |
9 | res.json({ error: err.message || err.toString() });
10 | }
11 |
12 | export default function api(server: express.Express) {
13 | server.use('/api/v1/public', publicExpressRoutes, handleError);
14 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
15 | }
16 |
--------------------------------------------------------------------------------
/book/6-end/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/6-end/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/6-end/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/6-end/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/6-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/6-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/6-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/6-end/app/lib/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/team-member';
4 |
5 | export const getSignedRequestForUploadApiMethod = ({ fileName, fileType, prefix, bucket }) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/aws/get-signed-request-for-upload-to-s3`, {
7 | body: JSON.stringify({ fileName, fileType, prefix, bucket }),
8 | });
9 |
10 | export const uploadFileUsingSignedPutRequestApiMethod = (file, signedRequest, headers = {}) =>
11 | sendRequestAndGetResponse(signedRequest, {
12 | externalServer: true,
13 | method: 'PUT',
14 | body: file,
15 | headers,
16 | });
17 |
--------------------------------------------------------------------------------
/book/6-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/6-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/6-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/6-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/6-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/6-end/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/6-end/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/6-end/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/6-end/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/6-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/7-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/7-begin/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/7-begin/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/7-begin/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/7-begin/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 |
6 | function handleError(err, _, res, __) {
7 | console.error(err.stack);
8 |
9 | res.json({ error: err.message || err.toString() });
10 | }
11 |
12 | export default function api(server: express.Express) {
13 | server.use('/api/v1/public', publicExpressRoutes, handleError);
14 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
15 | }
16 |
--------------------------------------------------------------------------------
/book/7-begin/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/7-begin/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/7-begin/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/7-begin/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/7-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/7-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/7-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/7-begin/app/lib/api/team-member.ts:
--------------------------------------------------------------------------------
1 | import sendRequestAndGetResponse from './sendRequestAndGetResponse';
2 |
3 | const BASE_PATH = '/api/v1/team-member';
4 |
5 | export const getSignedRequestForUploadApiMethod = ({ fileName, fileType, prefix, bucket }) =>
6 | sendRequestAndGetResponse(`${BASE_PATH}/aws/get-signed-request-for-upload-to-s3`, {
7 | body: JSON.stringify({ fileName, fileType, prefix, bucket }),
8 | });
9 |
10 | export const uploadFileUsingSignedPutRequestApiMethod = (file, signedRequest, headers = {}) =>
11 | sendRequestAndGetResponse(signedRequest, {
12 | externalServer: true,
13 | method: 'PUT',
14 | body: file,
15 | headers,
16 | });
17 |
--------------------------------------------------------------------------------
/book/7-begin/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/7-begin/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/7-begin/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/7-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/7-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/7-begin/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/7-begin/app/pages/csr-page.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@mui/material/Button";
2 | import React from "react";
3 | import Head from "next/head";
4 |
5 | const CSRPage = () => (
6 |
7 |
8 |
CSR page
9 |
10 |
11 |
12 |
Content on CSR page
13 |
14 |
15 |
16 | );
17 |
18 | console.log(process.env.NEXT_PUBLIC_URL_APP);
19 |
20 | export default CSRPage;
21 |
--------------------------------------------------------------------------------
/book/7-begin/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/7-begin/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/7-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/7-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/7-end/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/7-end/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/7-end/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/7-end/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/book/7-end/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/7-end/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/7-end/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/7-end/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/7-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/7-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/7-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/7-end/app/components/common/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loading = ({ text }: { text: string }) => {
4 | const IS_DEV = process.env.NODE_ENV !== 'production';
5 |
6 | if (IS_DEV) {
7 | return ;
8 | }
9 |
10 | return {text}
;
11 | };
12 |
13 | export default Loading;
14 |
--------------------------------------------------------------------------------
/book/7-end/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/book/7-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/7-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/7-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/7-end/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/book/7-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/7-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/7-end/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/7-end/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/7-end/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/7-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/8-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/8-begin/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/8-begin/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/8-begin/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/8-begin/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/book/8-begin/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/8-begin/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/8-begin/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/8-begin/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/8-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/8-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/8-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/8-begin/app/components/common/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loading = ({ text }: { text: string }) => {
4 | const IS_DEV = process.env.NODE_ENV !== 'production';
5 |
6 | if (IS_DEV) {
7 | return ;
8 | }
9 |
10 | return {text}
;
11 | };
12 |
13 | export default Loading;
14 |
--------------------------------------------------------------------------------
/book/8-begin/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/book/8-begin/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/8-begin/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/8-begin/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/8-begin/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/book/8-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/8-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/8-begin/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/8-begin/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/8-begin/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/8-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/8-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/8-end/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/8-end/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/8-end/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/8-end/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/book/8-end/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/8-end/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/8-end/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/8-end/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/8-end/app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "class-properties": { "loose": true }
7 | }
8 | ]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/book/8-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/8-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/8-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/8-end/app/components/posts/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = { html: string };
4 |
5 | class PostContent extends React.Component {
6 | public render() {
7 | const { html } = this.props;
8 |
9 | return (
10 |
14 | );
15 | }
16 | }
17 |
18 | export default PostContent;
19 |
--------------------------------------------------------------------------------
/book/8-end/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/book/8-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/8-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/8-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export {
30 | styleBigAvatar,
31 | styleRaisedButton,
32 | styleToolbar,
33 | styleTextField,
34 | styleForm,
35 | };
36 |
--------------------------------------------------------------------------------
/book/8-end/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/book/8-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/8-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/8-end/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/8-end/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/8-end/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/8-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/9-begin/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/9-begin/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/9-begin/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/9-begin/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/9-begin/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/book/9-begin/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/9-begin/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/9-begin/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/9-begin/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/9-begin/app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "class-properties": { "loose": true }
7 | }
8 | ]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/book/9-begin/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/9-begin/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/9-begin/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/9-begin/app/components/posts/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = { html: string };
4 |
5 | class PostContent extends React.Component {
6 | public render() {
7 | const { html } = this.props;
8 |
9 | return (
10 |
14 | );
15 | }
16 | }
17 |
18 | export default PostContent;
19 |
--------------------------------------------------------------------------------
/book/9-begin/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/book/9-begin/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/9-begin/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/9-begin/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export {
30 | styleBigAvatar,
31 | styleRaisedButton,
32 | styleToolbar,
33 | styleTextField,
34 | styleForm,
35 | };
36 |
--------------------------------------------------------------------------------
/book/9-begin/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/book/9-begin/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/9-begin/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/9-begin/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/9-begin/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/9-begin/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/9-begin/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/9-end/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/book/9-end/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/9-end/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/9-end/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/9-end/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/book/9-end/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/book/9-end/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/book/9-end/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"]
19 | },
20 | "exclude": ["production-server", "node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/book/9-end/api/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/"
6 | },
7 | "include": ["./server/**/*.ts"],
8 | "exclude": ["./server/**/*.test.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/book/9-end/app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "class-properties": { "loose": true }
7 | }
8 | ]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/book/9-end/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/9-end/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/book/9-end/app/README.md:
--------------------------------------------------------------------------------
1 | # app
--------------------------------------------------------------------------------
/book/9-end/app/components/posts/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = { html: string };
4 |
5 | class PostContent extends React.Component {
6 | public render() {
7 | const { html } = this.props;
8 |
9 | return (
10 |
14 | );
15 | }
16 | }
17 |
18 | export default PostContent;
19 |
--------------------------------------------------------------------------------
/book/9-end/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/book/9-end/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/book/9-end/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/book/9-end/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | font: '14px Roboto',
10 | };
11 |
12 | const styleToolbar = {
13 | background: '#FFF',
14 | height: '64px',
15 | paddingRight: '20px',
16 | };
17 |
18 | const styleTextField = {
19 | font: '15px Roboto',
20 | color: '#222',
21 | fontWeight: '300',
22 | };
23 |
24 | const styleForm = {
25 | margin: '7% auto',
26 | width: '360px',
27 | };
28 |
29 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
30 |
--------------------------------------------------------------------------------
/book/9-end/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/book/9-end/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/book/9-end/app/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | poweredByHeader: false,
3 | webpack5: true,
4 | typescript: {
5 | ignoreBuildErrors: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/book/9-end/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/book/9-end/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/book/9-end/app/public/pepe.jpg
--------------------------------------------------------------------------------
/book/9-end/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/book/9-end/lambda/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/book/9-end/lambda/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | extends: ["plugin:@typescript-eslint/recommended", "prettier"],
4 | env: {
5 | "es6": true,
6 | "node": true,
7 | },
8 | rules: {
9 | 'prettier/prettier': [
10 | 'error',
11 | {
12 | singleQuote: true,
13 | trailingComma: 'all',
14 | arrowParens: 'always',
15 | printWidth: 100,
16 | semi: true,
17 | },
18 | ],
19 | '@typescript-eslint/no-unused-vars': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | 'prefer-arrow-callback': 'error',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | },
24 | plugins: [
25 | "prettier"
26 | ]
27 | }
--------------------------------------------------------------------------------
/book/9-end/lambda/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .serverless/*
10 | .build/*
11 | .next
12 | .vscode/
13 | node_modules/
14 | .coverage
15 | .env
16 | .env.production
17 | now.json
18 | .note
19 |
20 | compiled/
21 | production-server/
22 |
23 | yarn-error.log
24 |
--------------------------------------------------------------------------------
/book/9-end/lambda/serverless.yml:
--------------------------------------------------------------------------------
1 | service: saas-boilerplate
2 |
3 | provider:
4 | name: aws
5 | runtime: nodejs16.x
6 | stage: production
7 | region: us-east-1
8 | memorySize: 2048 # optional, in MB, default is 1024
9 | timeout: 30 # optional, in seconds, default is 6
10 |
11 | plugins:
12 | - serverless-plugin-typescript
13 | - serverless-dotenv-plugin
14 |
15 | custom:
16 | dotenv:
17 | include:
18 | - NODE_ENV
19 | - MONGO_URL_TEST
20 | - MONGO_URL
21 | - AWS_ACCESSKEYID
22 | - AWS_SECRETACCESSKEY
23 | - EMAIL_SUPPORT_FROM_ADDRESS
24 | - URL_APP
25 | - PRODUCTION_URL_APP
26 |
27 | functions:
28 | sendEmailForNewPost:
29 | handler: handler.sendEmailForNewPost
30 |
--------------------------------------------------------------------------------
/book/9-end/lambda/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"],
19 | "module": "commonjs",
20 | "outDir": ".build/",
21 | "rootDir": "./"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/saas/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/saas/api/.elasticbeanstalk/config.yml:
--------------------------------------------------------------------------------
1 | branch-defaults:
2 | default:
3 | environment: api-saas-boilerplate-env
4 | environment-defaults:
5 | api-saas-boilerplate-env:
6 | branch: null
7 | repository: null
8 | global:
9 | application_name: api-saas-boilerplate-app
10 | default_ec2_keyname: null
11 | default_platform: Node.js 14 running on 64bit Amazon Linux 2
12 | default_region: us-east-1
13 | include_git_submodules: true
14 | instance_profile: null
15 | platform_name: null
16 | platform_version: null
17 | profile: null
18 | sc: null
19 | workspace_type: Application
20 |
--------------------------------------------------------------------------------
/saas/api/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/saas/api/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | extends: ["plugin:@typescript-eslint/recommended", "prettier"],
4 | env: {
5 | "es6": true,
6 | "node": true,
7 | },
8 | plugins: ["prettier"],
9 | rules: {
10 | 'prettier/prettier': [
11 | 'error',
12 | {
13 | singleQuote: true,
14 | trailingComma: 'all',
15 | arrowParens: 'always',
16 | printWidth: 100,
17 | semi: true,
18 | },
19 | ],
20 | '@typescript-eslint/no-unused-vars': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | 'prefer-arrow-callback': 'error',
24 | '@typescript-eslint/explicit-module-boundary-types': 'off',
25 | },
26 | }
--------------------------------------------------------------------------------
/saas/api/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/saas/api/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/saas/api/server/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 |
3 | import publicExpressRoutes from './public';
4 | import teamMemberExpressRoutes from './team-member';
5 | import teamLeaderApi from './team-leader';
6 |
7 | function handleError(err, _, res, __) {
8 | console.error(err.stack);
9 |
10 | res.json({ error: err.message || err.toString() });
11 | }
12 |
13 | export default function api(server: express.Express) {
14 | server.use('/api/v1/public', publicExpressRoutes, handleError);
15 | server.use('/api/v1/team-member', teamMemberExpressRoutes, handleError);
16 | server.use('/api/v1/team-leader', teamLeaderApi, handleError);
17 | }
18 |
--------------------------------------------------------------------------------
/saas/api/server/logger.ts:
--------------------------------------------------------------------------------
1 | import * as winston from 'winston';
2 |
3 | const dev = process.env.NODE_ENV !== 'production';
4 |
5 | const logger = winston.createLogger({
6 | format: winston.format.simple(),
7 | level: dev ? 'debug' : 'info',
8 | transports: [new winston.transports.Console()],
9 | });
10 |
11 | export default logger;
12 |
--------------------------------------------------------------------------------
/saas/api/server/utils/sum.ts:
--------------------------------------------------------------------------------
1 | function sum(a, b) {
2 | return a + b;
3 | }
4 |
5 | export { sum };
6 |
--------------------------------------------------------------------------------
/saas/api/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 | Allow: /login
4 | Allow: /signup
5 | Disallow: /*
6 |
--------------------------------------------------------------------------------
/saas/api/test/server/utils/sum.test.ts:
--------------------------------------------------------------------------------
1 | import { sum } from '../../../server/utils/sum';
2 |
3 | console.log(sum(1, 2));
4 |
5 | describe.skip('testing sum function', () => {
6 | test('adds 1 + 2 to equal 3', () => {
7 | expect(sum(1, 2)).toBe(3);
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/saas/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "target": "es2020",
19 | "lib": ["es2020"],
20 | "module": "commonjs",
21 | "outDir": "production-server/",
22 | "downlevelIteration": true,
23 | },
24 | "include": ["./server/**/*.ts"]
25 | }
26 |
--------------------------------------------------------------------------------
/saas/app/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "next/babel",
5 | {
6 | "class-properties": { "loose": true }
7 | }
8 | ]
9 | ],
10 | "plugins": [["@babel/plugin-proposal-private-property-in-object", { "loose": true }], ["@babel/plugin-proposal-private-methods", { "loose": true }]]
11 | }
12 |
--------------------------------------------------------------------------------
/saas/app/.elasticbeanstalk/config.yml:
--------------------------------------------------------------------------------
1 | branch-defaults:
2 | default:
3 | environment: app-saas-boilerplate-env
4 | environment-defaults:
5 | app-saas-boilerplate-env:
6 | branch: null
7 | repository: null
8 | global:
9 | application_name: app-saas-boilerplate-app
10 | default_ec2_keyname: null
11 | default_platform: Node.js 14 running on 64bit Amazon Linux 2
12 | default_region: us-east-1
13 | include_git_submodules: true
14 | instance_profile: null
15 | platform_name: null
16 | platform_version: null
17 | profile: null
18 | sc: null
19 | workspace_type: Application
20 |
--------------------------------------------------------------------------------
/saas/app/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLEKEY="pk_test_xxxxxxxxxxxxxxx"
2 | NEXT_PUBLIC_STRIPE_LIVE_PUBLISHABLEKEY="pk_live_xxxxxxxxxxxxxxx"
3 |
4 | NEXT_PUBLIC_BUCKET_FOR_POSTS=
5 | NEXT_PUBLIC_BUCKET_FOR_TEAM_AVATARS=
6 | NEXT_PUBLIC_BUCKET_FOR_TEAM_LOGOS=
7 |
8 | NEXT_PUBLIC_URL_APP="http://localhost:3000"
9 | NEXT_PUBLIC_URL_API="http://localhost:8000"
10 | NEXT_PUBLIC_PRODUCTION_URL_APP=
11 | NEXT_PUBLIC_PRODUCTION_URL_API=
12 |
13 |
14 | NEXT_PUBLIC_API_GATEWAY_ENDPOINT=
15 | NEXT_PUBLIC_GA_MEASUREMENT_ID=
16 |
--------------------------------------------------------------------------------
/saas/app/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/saas/app/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .build/*
10 | .next
11 | .vscode/
12 | node_modules/
13 | .coverage
14 | .env
15 | now.json
16 | .note
17 |
18 | compiled/
19 | production-server/
20 |
21 | yarn-error.log
22 |
--------------------------------------------------------------------------------
/saas/app/components/posts/PostContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type Props = { html: string };
4 |
5 | class PostContent extends React.Component {
6 | public render() {
7 | const { html } = this.props;
8 |
9 | return (
10 |
19 | );
20 | }
21 | }
22 |
23 | export default PostContent;
24 |
--------------------------------------------------------------------------------
/saas/app/lib/api/makeQueryString.ts:
--------------------------------------------------------------------------------
1 | function makeQueryString(params) {
2 | const query = Object.keys(params)
3 | .filter((k) => !!params[k])
4 | .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
5 | .join('&');
6 |
7 | return query;
8 | }
9 |
10 | export { makeQueryString };
11 |
--------------------------------------------------------------------------------
/saas/app/lib/confirm.ts:
--------------------------------------------------------------------------------
1 | import { openConfirmDialogExternal } from '../components/common/Confirmer';
2 |
3 | export default function confirm({
4 | title,
5 | message,
6 | onAnswer,
7 | }: {
8 | title: string;
9 | message: string;
10 | onAnswer: (answer) => void;
11 | }) {
12 | openConfirmDialogExternal({ title, message, onAnswer });
13 | }
14 |
--------------------------------------------------------------------------------
/saas/app/lib/gtag.ts:
--------------------------------------------------------------------------------
1 | const { NEXT_PUBLIC_GA_MEASUREMENT_ID } = process.env;
2 |
3 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages
4 | export const pageview = (url) => {
5 | (window as any).gtag('config', NEXT_PUBLIC_GA_MEASUREMENT_ID, {
6 | page_location: url,
7 | });
8 | };
9 |
10 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events
11 | export const event = ({ action, category, label }) => {
12 | (window as any).gtag('event', action, {
13 | event_category: category,
14 | event_label: label,
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/saas/app/lib/notify.ts:
--------------------------------------------------------------------------------
1 | import { openSnackbarExternal } from '../components/common/Notifier';
2 |
3 | export default function notify(obj) {
4 | openSnackbarExternal({ message: obj.message || obj.toString() });
5 | }
6 |
--------------------------------------------------------------------------------
/saas/app/lib/sharedStyles.ts:
--------------------------------------------------------------------------------
1 | const styleBigAvatar = {
2 | width: '80px',
3 | height: '80px',
4 | margin: '0px auto 15px',
5 | };
6 |
7 | const styleRaisedButton = {
8 | margin: '15px',
9 | };
10 |
11 | const styleToolbar = {
12 | background: '#FFF',
13 | height: '64px',
14 | paddingRight: '20px',
15 | };
16 |
17 | const styleTextField = {
18 | color: '#222',
19 | fontWeight: '300',
20 | };
21 |
22 | const styleForm = {
23 | margin: '7% auto',
24 | width: '360px',
25 | };
26 |
27 | export { styleBigAvatar, styleRaisedButton, styleToolbar, styleTextField, styleForm };
28 |
--------------------------------------------------------------------------------
/saas/app/lib/store/invitation.ts:
--------------------------------------------------------------------------------
1 | class Invitation {
2 | public _id: string;
3 | public teamId: string;
4 | public email: string;
5 | public createdAt: Date;
6 |
7 | constructor(params) {
8 | Object.assign(this, params);
9 | }
10 | }
11 |
12 | export { Invitation };
13 |
--------------------------------------------------------------------------------
/saas/app/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/saas/app/next.config.js:
--------------------------------------------------------------------------------
1 | const withTM = require('next-transpile-modules')([ // eslint-disable-line
2 | '@mui/material',
3 | '@mui/icons-material',
4 | ]);
5 |
6 | module.exports = withTM({
7 | typescript: {
8 | ignoreBuildErrors: true,
9 | },
10 | poweredByHeader: false,
11 | swcMinify: true,
12 | experimental: {
13 | forceSwcTransforms: true,
14 | },
15 | modularizeImports: {
16 | '@mui/material/?(((\\w*)?/?)*)': {
17 | transform: '@mui/material/{{ matches.[1] }}/{{member}}',
18 | },
19 | '@mui/icons-material/?(((\\w*)?/?)*)': {
20 | transform: '@mui/icons-material/{{ matches.[1] }}/{{member}}',
21 | },
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/saas/app/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["server"],
3 | "exec": "ts-node --project tsconfig.server.json",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/saas/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/saas/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff
--------------------------------------------------------------------------------
/saas/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/saas/app/public/fonts/IBM-Plex-Mono/IBMPlexMono-Regular.woff2
--------------------------------------------------------------------------------
/saas/app/public/fonts/Roboto/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/saas/app/public/fonts/Roboto/Roboto-Regular.woff
--------------------------------------------------------------------------------
/saas/app/public/fonts/Roboto/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/saas/app/public/fonts/Roboto/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/saas/app/public/pepe.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/async-labs/saas/4b27b6a0a4bb876562da6de93f0c0dd509fd5ffe/saas/app/public/pepe.jpg
--------------------------------------------------------------------------------
/saas/app/server/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 | Allow: /login
4 | Allow: /signup
5 | Disallow: /*
--------------------------------------------------------------------------------
/saas/app/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "outDir": "production-server/",
6 | "target": "es2017",
7 | "isolatedModules": false,
8 | "noEmit": false
9 | },
10 | "exclude": ["./server/types.d.ts"],
11 | "include": ["./server/**/*.ts"],
12 | "typeRoots": ["./node_modules/@types", "./server/types.d.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/saas/lambda/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | production-server
3 | node_modules
4 |
--------------------------------------------------------------------------------
/saas/lambda/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.swp
3 | tmp/
4 | npm-debug.log
5 | .DS_Store
6 |
7 |
8 |
9 | .serverless/*
10 | .build/*
11 | .next
12 | .vscode/
13 | node_modules/
14 | .coverage
15 | .env
16 | .env.production
17 | now.json
18 | .note
19 |
20 | compiled/
21 | production-server/
22 |
23 | yarn-error.log
24 |
--------------------------------------------------------------------------------
/saas/lambda/api:
--------------------------------------------------------------------------------
1 | /Users/batamar/Documents/works/saas/book/9-end/api
--------------------------------------------------------------------------------
/saas/lambda/serverless.yml:
--------------------------------------------------------------------------------
1 | service: saas-boilerplate
2 |
3 | useDotenv: true
4 | provider:
5 | name: aws
6 | runtime: nodejs16.x
7 | stage: production
8 | region: us-east-1
9 | memorySize: 2048 # optional, in MB, default is 1024
10 | timeout: 30 # optional, in seconds, default is 6
11 | # profile: saas
12 |
13 |
14 | plugins:
15 | - serverless-plugin-typescript
16 | - serverless-dotenv-plugin
17 |
18 | custom:
19 | dotenv:
20 | include:
21 | - NODE_ENV
22 | - MONGO_URL_TEST
23 | - MONGO_URL
24 | - AWS_ACCESSKEYID
25 | - AWS_SECRETACCESSKEY
26 | - EMAIL_SUPPORT_FROM_ADDRESS
27 | - URL_APP
28 | - PRODUCTION_URL_APP
29 |
30 | functions:
31 | sendEmailForNewPost:
32 | handler: handler.sendEmailForNewPost
33 |
--------------------------------------------------------------------------------
/saas/lambda/symlink:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | echo "Creating symlinks for api:" $1
4 |
5 | echo ln -s $1/$var $var
6 | ln -s $1/$var $var
--------------------------------------------------------------------------------
/saas/lambda/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "allowJs": true,
6 | "alwaysStrict": true,
7 | "moduleResolution": "node",
8 | "allowSyntheticDefaultImports": true,
9 | "removeComments": false,
10 | "preserveConstEnums": true,
11 | "noUnusedLocals": true,
12 | "noUnusedParameters": true,
13 | "sourceMap": true,
14 | "skipLibCheck": true,
15 | "baseUrl": ".",
16 | "experimentalDecorators": true,
17 | "typeRoots": ["./node_modules/@types"],
18 | "lib": ["es2015", "es2016"],
19 | "module": "commonjs",
20 | "outDir": ".build/",
21 | "rootDir": "./"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------