├── .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 | --------------------------------------------------------------------------------